diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dfe7a6..e406cfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: matrix: include: - php: '8.1' - moodle-branch: 'master' + moodle-branch: 'main' database: 'pgsql' - php: '8.0' moodle-branch: 'MOODLE_402_STABLE' @@ -94,6 +94,7 @@ jobs: run: moodle-plugin-ci phpcs --max-warnings 0 - name: Moodle PHPDoc Checker + continue-on-error: true if: ${{ always() }} run: moodle-plugin-ci phpdoc --max-warnings 0 diff --git a/amd/build/editor.min.js b/amd/build/editor.min.js new file mode 100644 index 0000000..fb706de --- /dev/null +++ b/amd/build/editor.min.js @@ -0,0 +1,10 @@ +define("editor_ousupsub/editor",["exports"],(function(_exports){function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} +/** + * OUSupSub Editor Manager. + * + * @module editor_ousupsub/editor + * @copyright 2024 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.loadEditor=void 0;const defaultActions={sup:{name:"superscript",tag:"sup",class:"ousupsub_superscript_button_superscript"},sub:{name:"subscript",tag:"sub",class:"ousupsub_subscript_button_subscript"}};class OUSupSubEditor{constructor(settings){_defineProperty(this,"defaultSetting",{element:"",type:"both",classes:{wrap:"ousupsub-wrap",editor:"editor_ousupsub",contentWrap:"editor_ousupsub_content_wrap",content:"editor_ousupsub_content",toolbar:"editor_ousupsub_toolbar",toolbarGroup:"ousupsub_group",button:"ousupsub-button"},custom:{editor:"",content:"",toolbar:"",button:"",contentWrap:"",wrap:"",toolbarGroup:""}}),_defineProperty(this,"history",[]),_defineProperty(this,"historyIndex",-1),_defineProperty(this,"initEditorContent",(()=>{var _custom$content,_custom$contentWrap;const{classes:classes,custom:custom}=this.settings,contentElement=this.createElement("div",{class:(classes.content+" "+(null!==(_custom$content=custom.content)&&void 0!==_custom$content?_custom$content:"")).trim(),contenteditable:!0,autocapitalize:"none",autocorrect:"off",role:"textbox",spellcheck:!1,"aria-live":"off",id:"".concat(this.settings.element.replace(/:/g,":"),"editable")});contentElement.addEventListener("blur",(()=>{this.saveHistory()})),document.addEventListener("selectionchange",(()=>this.handleSelectionChange())),contentElement.addEventListener("keydown",(event=>{const range=window.getSelection().getRangeAt(0),keyMap={key:{ArrowUp:"sup",94:"sup",ArrowDown:"sub",95:"sub"},shiftKey:{"^":"sup",_:"sub"}};if((keyMap.key[event.key]||event.shiftKey&&keyMap.shiftKey[event.key])&&(event.preventDefault(),this.handleSupSubHotKey(keyMap.key[event.key]||keyMap.shiftKey[event.key])),event.ctrlKey&&this.saveHistory(),"Enter"===event.key&&event.preventDefault(),event.ctrlKey&&"z"===event.key&&(event.preventDefault(),this.handleUndo()),event.ctrlKey&&"y"===event.key&&(event.preventDefault(),this.handleRedo()),""===this.cleanHTML(event.target.innerHTML)&&!this.isSelectionInsideSubSup()){const emptyText=document.createTextNode("\ufeff");range.insertNode(emptyText)}this.getTextArea().value=this.getCleanHTML()})),contentElement.addEventListener("paste",(event=>{this.handlePaste(event)}));const wrapContent=this.createElement("div",{class:(classes.contentWrap+" "+(null!==(_custom$contentWrap=custom.contentWrap)&&void 0!==_custom$contentWrap?_custom$contentWrap:"")).trim()});return wrapContent.appendChild(contentElement),wrapContent})),this.settings=Object.assign(this.defaultSetting,settings),this.init()}init(){var _custom$editor;const textareaElement=this.getTextArea(),{classes:classes,custom:custom}=this.settings;if(!textareaElement)return;textareaElement.style.display="none";const editorElement=this.createElement("div",{class:(classes.editor+" "+(null!==(_custom$editor=null==custom?void 0:custom.editor)&&void 0!==_custom$editor?_custom$editor:"")).trim(),id:classes.editor+"-"+this.settings.element}),editorWrap=this.createElement("div",{class:(classes.wrap+" "+custom.wrap).trim()}),toolbarEl=this.initEditorToolbar();editorWrap.appendChild(toolbarEl);const contentElementWrap=this.initEditorContent(),contentEditor=contentElementWrap.querySelector(".".concat(this.settings.classes.content));editorWrap.appendChild(contentElementWrap),editorElement.appendChild(editorWrap);const width=6*this.getTextArea().getAttribute("cols")+41+"px";contentEditor.style.width=width,contentEditor.style.minWidth=width,contentEditor.style.maxWidth=width;const height=6*this.getTextArea().getAttribute("rows")+13,heightEditor="".concat(height-10,"px"),lineHeightEditor="".concat(height-6,"px");contentEditor.style.height=heightEditor,contentEditor.style.minHeight=heightEditor,contentEditor.style.maxHeight=heightEditor,contentEditor.style.lineHeight=lineHeightEditor;const heightContent="".concat(height+1,"px");contentElementWrap.style.minHeight=heightContent;const textareaLabel=document.querySelector('[for="'+this.settings.element+'"]');if(textareaLabel.style.display="inline-block",textareaLabel.style.margin=0,textareaLabel.style.height=heightContent,textareaLabel.style.minHeight=heightContent,textareaLabel.style.maxHeight=heightContent,textareaLabel.classList.contains("accesshide"))textareaLabel.classList.remove("accesshide"),textareaLabel.style.visibility="hidden",editorElement.style.marginLeft="-".concat(parseInt(textareaLabel.offsetWidth),"px");else{textareaLabel.parentNode.style.paddingBottom=heightEditor,textareaLabel.style.verticalAlign="bottom"}textareaElement.insertAdjacentElement("beforebegin",editorElement),this.getEditorContent().innerHTML=this.getContent(),this.saveHistory(),requestAnimationFrame((()=>{textareaLabel.style.lineHeight=contentEditor.style.lineHeight;const heightWrapper=height+1+parseInt(toolbarEl.offsetHeight);editorElement.style.height=heightWrapper+"px",editorElement.style.minHeight=heightWrapper+"px",editorElement.style.maxHeight=heightWrapper+"px"})),document.addEventListener("click",(e=>{if(!editorElement.contains(e.target)){const cleanData=this.getCleanHTML();this.getTextArea().value=cleanData,this.getEditorContent().innerHTML=cleanData,this.setActiveButton(!1)}}))}handlePaste(event){event.preventDefault();const types=event.clipboardData.types;let content,isHTML=!1;null!=types&&types.contains?isHTML=types.contains("text/html"):null!=types&&types.includes&&(isHTML=types.includes("text/html")),content=isHTML?this.cleanPasteHTML(event.clipboardData.getData("text/html")):event.clipboardData.getData("text");const cleanData=content.replaceAll(/[\r\n]+/g,"");document.execCommand("insertHTML",!1,cleanData),this.saveHistory(),this.getTextArea().value=this.getCleanHTML()}handleUndo(){this.historyIndex>0&&(this.historyIndex--,this.getEditorContent().innerHTML=this.history[this.historyIndex],this.getTextArea().value=this.history[this.historyIndex])}handleRedo(){this.historyIndex1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElement(tag);for(let attribute in attributes)element.setAttribute(attribute,attributes[attribute]);return element}isSelectionInsideSubSup(){const selection=window.getSelection();if(0===selection.rangeCount)return!1;const range=selection.getRangeAt(0),tagName=range.commonAncestorContainer.parentNode.nodeName;if(selection.isCollapsed)return!!this.isSupSubTag(tagName)&&range.commonAncestorContainer.parentNode;let nodeNames;const selectionNodes=range.cloneContents().childNodes;for(let node of selectionNodes){const nodeName=node.nodeName;if(""!==node.textContent){if(!this.isSupSubTag(nodeName)&&"#text"===nodeName&&!this.isSupSubTag(tagName))return!1;if(nodeNames||(nodeNames=node),!nodeNames.isEqualNode(node))return!1}}return"#text"===nodeNames.nodeName||this.isSupSubTag(tagName)?range.commonAncestorContainer.parentNode:nodeNames}isSupSubTag(tagName){return["SUB","SUP"].includes(tagName)}setActiveButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;var _this$getSupSubButton,_this$getSupSubButton2;(this.getEditor().querySelectorAll(".".concat(toolbar," .").concat(button)).forEach((button=>button.classList.remove("highlight"))),!1!==type)&&(null===(_this$getSupSubButton=this.getSupSubButton(type))||void 0===_this$getSupSubButton||null===(_this$getSupSubButton2=_this$getSupSubButton.classList)||void 0===_this$getSupSubButton2||_this$getSupSubButton2.add("highlight"))}initEditorToolbar(){var _this$settings$custom,_this$settings,_this$settings$custom2,_this$settings$custom3,_this$settings3,_this$settings3$custo;const toolbarGroup=this.createElement("div",{class:(this.settings.classes.toolbarGroup+" "+(null!==(_this$settings$custom=null===(_this$settings=this.settings)||void 0===_this$settings||null===(_this$settings$custom2=_this$settings.custom)||void 0===_this$settings$custom2?void 0:_this$settings$custom2.toolbarGroup)&&void 0!==_this$settings$custom?_this$settings$custom:"")).trim()});this.getActions(this.settings.type).forEach((action=>{var _this$settings2;const button=this.createElement("button",{class:(null===(_this$settings2=this.settings)||void 0===_this$settings2?void 0:_this$settings2.classes.button)+" "+action.class,title:this.settings.buttons[action.name].title,type:"button","data-action":action.name});button.innerHTML=this.settings.buttons[action.name].icon,button.setAttribute("type","button"),button.onclick=()=>{const selection=window.getSelection(),nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed&&!1!==nodeEl&&nodeEl.nodeName.toLowerCase()!==action.tag)return button.blur(),void this.getEditorContent().focus();this.getEditorContent().focus(),this.setFormat(action)},toolbarGroup.appendChild(button)}));const toolbarEl=this.createElement("div",{class:(this.settings.classes.toolbar+" "+(null!==(_this$settings$custom3=null===(_this$settings3=this.settings)||void 0===_this$settings3||null===(_this$settings3$custo=_this$settings3.custom)||void 0===_this$settings3$custo?void 0:_this$settings3$custo.toolbar)&&void 0!==_this$settings$custom3?_this$settings$custom3:"")).trim()});return toolbarEl.appendChild(toolbarGroup),toolbarEl}setFormat(action){const selection=window.getSelection(),range=selection.getRangeAt(0),{tag:tag}=action,nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed){const parentNode=range.commonAncestorContainer.parentNode;if(parentNode.nodeName.toLowerCase()===tag){const beforeText=this.createElement(tag);beforeText.innerText=parentNode.textContent.slice(0,range.startOffset);const emptyText=document.createTextNode("\ufeff"),afterText=this.createElement(tag);afterText.innerText=parentNode.textContent.slice(range.startOffset),""!==afterText.innerHTML&&parentNode.parentNode.insertBefore(afterText,parentNode.nextSibling),parentNode.parentNode.insertBefore(emptyText,parentNode.nextSibling),""!==beforeText.innerHTML&&parentNode.parentNode.insertBefore(beforeText,parentNode.nextSibling),parentNode.remove(),range.setStart(emptyText,1),range.setEnd(emptyText,1),selection.removeAllRanges(),selection.addRange(range)}else{const node=this.createElement(tag);node.appendChild(document.createTextNode("\ufeff")),range.insertNode(node),range.setStart(node.firstChild,1),range.setEnd(node.firstChild,1),selection.removeAllRanges(),selection.addRange(range)}}else if(nodeEl){const selectedText=range.toString(),parentElement=nodeEl,nextSibling=parentElement.nextSibling,beforeText=parentElement.textContent.slice(0,range.startOffset),afterText=parentElement.textContent.slice(range.endOffset);if(beforeText){const start=this.createElement(parentElement.nodeName.toLowerCase());start.textContent=beforeText,parentElement.parentNode.insertBefore(start,nextSibling)}const textNode=document.createTextNode(selectedText);if(parentElement.parentNode.insertBefore(textNode,nextSibling),afterText){const end=this.createElement(parentElement.nodeName.toLowerCase());end.textContent=afterText,parentElement.parentNode.insertBefore(end,nextSibling)}parentElement.remove(),range.setStart(textNode,0),range.setEnd(textNode,selectedText.length),selection.removeAllRanges(),selection.addRange(range),this.getTextArea().value=this.getCleanHTML()}else{const selectedText=range.toString();range.deleteContents();const previousNode=range.commonAncestorContainer.previousSibling,nextNode=range.commonAncestorContainer.nextSibling;if(previousNode||nextNode){var _previousNode$nodeNam,_nextNode$nodeName;const newNode=this.createElement(tag);let startOffset=0,endOffset=0,content="";if(previousNode&&(null==previousNode||null===(_previousNode$nodeNam=previousNode.nodeName)||void 0===_previousNode$nodeNam?void 0:_previousNode$nodeNam.toLowerCase())===tag&&(content=previousNode.textContent,startOffset=content.length,previousNode.remove()),content+=selectedText,endOffset=content.length,nextNode&&(null==nextNode||null===(_nextNode$nodeName=nextNode.nodeName)||void 0===_nextNode$nodeName?void 0:_nextNode$nodeName.toLowerCase())===tag&&(content+=nextNode.textContent,nextNode.remove()),newNode.textContent=content,content!==selectedText)return range.insertNode(newNode),range.setStart(newNode.firstChild,startOffset),range.setEnd(newNode.firstChild,endOffset),void(this.getTextArea().value=this.getCleanHTML())}const newNode=document.createElement(tag);newNode.appendChild(document.createTextNode(selectedText)),selection.removeAllRanges(),range.insertNode(newNode),range.selectNodeContents(newNode.firstChild),selection.addRange(range),this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.getTextArea().value=this.getCleanHTML()}this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.saveHistory()}saveHistory(){const content=this.getCleanHTML();-1!==this.historyIndex&&content===this.history[this.historyIndex]||(this.history.splice(this.historyIndex+1),this.history.push(content),this.historyIndex++)}cleanPasteHTML(content){if(!content||0===content.length)return"";let rules=[{regex:/<\s*\/html\s*>([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}];if(content=this.filterContentWithRules(content,rules),0===(content=this.cleanHTML(content)).length||!/\S/.test(content))return content;const holder=document.createElement("div");return holder.innerHTML=content,content=holder.innerHTML,holder.innerHTML="",rules=[{regex:/(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}],content=this.filterContentWithRules(content,rules),this.cleanHTML(content)}isSupportSupSub(action){const{type:type}=this.settings;return"both"===type||type===action}filterContentWithRules(content,rules){for(const element of rules)content=content.replace(element.regex,element.replace);return content}cleanHTML(content){return this.filterContentWithRules(content,[{regex:/]*>( |\s)*<\/p>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/ /gi,replace:" "},{regex:/<\/sup>(\s*)+/gi,replace:"$1"},{regex:/<\/sub>(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+<\/sup>/gi,replace:"$1"},{regex:/(\s*)+<\/sub>/gi,replace:"$1"},{regex:/
/gi,replace:""},{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/]*>[\s\S]*?<\/script>/gi,replace:""},{regex:/<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi,replace:""},{regex:/<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi,replace:""},{regex:/<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi,replace:""},{regex:/<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi,replace:""},{regex:/<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi,replace:""},{regex:/<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi,replace:""},{regex:/<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi,replace:""},{regex:/<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi,replace:""},{regex:/<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi,replace:""},{regex:/<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi,replace:""},{regex:/<\/?(?:var|wbr|video)[^>]*?>/gi,replace:""},{regex:/<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi,replace:""},{regex:/<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:jsl|nobr)[^>]*?>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*>( |\s)*<\/sup>/gi,replace:""},{regex:/]*>( |\s)*<\/sub>/gi,replace:""},{regex:/(.*?)<\/xmlns.*?>/gi,replace:"$1"},{regex:/\uFEFF/gi,replace:""}])}getCleanHTML(){let html;html=this.getEditorContent().cloneNode(!0).innerHTML;return["

","


","
",'

','


','

','


',"

 

","


 

",'

 

','


 

','

 

','


 

'].includes(html)?"":this.cleanHTML(html)}getEditorContent(){return this.getEditor().querySelector(".".concat(this.settings.classes.content))}getEditor(){return document.getElementById("".concat(this.settings.classes.editor,"-").concat(this.settings.element))}getSupSubButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;return this.getEditor().querySelector(".".concat(toolbar," .").concat(button,'[data-action^="').concat(type,'"]'))}getActions(type){return defaultActions[type]?[defaultActions[type]]:Object.values(defaultActions)}getButtonContainer(){return this.getEditor().querySelectorAll(".".concat(this.settings.classes.toolbar," .").concat(this.settings.classes.button))}getContent(){return this.getTextArea().value}getEditorId(){return this.settings.element}getTextArea(){return document.getElementById(this.settings.element)}}_exports.loadEditor=settings=>{const editor=new OUSupSubEditor(settings);window.OUSupSubEditor?window.OUSupSubEditor.addEditor(editor):window.OUSupSubEditor={instances:{[settings.element]:editor},addEditor:function(editor){this.instances[editor.getEditorId()]=editor},getEditorById:function(editorId){return this.instances[editorId]}}}})); + +//# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/amd/build/editor.min.js.map b/amd/build/editor.min.js.map new file mode 100644 index 0000000..a8ccbb0 --- /dev/null +++ b/amd/build/editor.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * OUSupSub Editor Manager.\n *\n * @module editor_ousupsub/editor\n * @copyright 2024 The Open University.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst defaultActions = {\n sup: {\n name: 'superscript',\n tag: 'sup',\n 'class': 'ousupsub_superscript_button_superscript',\n },\n sub: {\n name: 'subscript',\n tag: 'sub',\n 'class': 'ousupsub_subscript_button_subscript',\n },\n};\n\nclass OUSupSubEditor {\n\n // The editor's initial default settings.\n defaultSetting = {\n element: '',\n type: 'both',\n classes: {\n wrap: 'ousupsub-wrap',\n editor: 'editor_ousupsub',\n contentWrap: 'editor_ousupsub_content_wrap',\n content: 'editor_ousupsub_content',\n toolbar: 'editor_ousupsub_toolbar',\n toolbarGroup: 'ousupsub_group',\n button: 'ousupsub-button',\n },\n custom: {\n editor: '',\n content: '',\n toolbar: '',\n button: '',\n contentWrap: '',\n wrap: '',\n toolbarGroup: '',\n },\n };\n\n // Support for undo/redo with history data and history index.\n history = [];\n historyIndex = -1;\n\n /**\n * Constructor of the editor.\n * @constructor\n *\n * @param {Object} settings - The editor settings.\n */\n constructor(settings) {\n this.settings = Object.assign(this.defaultSetting, settings);\n this.init();\n }\n\n /**\n * Initial the editor.\n */\n init() {\n const textareaElement = this.getTextArea();\n const {classes, custom} = this.settings;\n\n if (!textareaElement) {\n return;\n }\n // Hidden origin text area.\n textareaElement.style.display = 'none';\n\n const editorElement = this.createElement('div', {\n 'class': (classes.editor + ' ' + (custom?.editor ?? '')).trim(),\n id: classes.editor + '-' + this.settings.element,\n });\n\n // Make editor container.\n const editorWrap = this.createElement('div', {\n 'class': (classes.wrap + ' ' + custom.wrap).trim(),\n });\n\n // Make toolbar containers.\n const toolbarEl = this.initEditorToolbar();\n editorWrap.appendChild(toolbarEl);\n\n // Make the content for editor.\n const contentElementWrap = this.initEditorContent();\n const contentEditor = contentElementWrap.querySelector(`.${this.settings.classes.content}`);\n\n // Append the editor's elements to the DOM.\n editorWrap.appendChild(contentElementWrap);\n editorElement.appendChild(editorWrap);\n\n // Calculate the editor size based on the attributes 'cols' and 'rows'.\n const width = (this.getTextArea().getAttribute('cols') * 6 + 41) + 'px';\n contentEditor.style.width = width;\n contentEditor.style.minWidth = width;\n contentEditor.style.maxWidth = width;\n\n const rows = this.getTextArea().getAttribute('rows');\n const height = (rows * 6 + 13);\n const heightEditor = `${height - 10}px`;\n const lineHeightEditor = `${height - 6}px`;\n\n // Set the size of the editor.\n contentEditor.style.height = heightEditor;\n contentEditor.style.minHeight = heightEditor;\n contentEditor.style.maxHeight = heightEditor;\n contentEditor.style.lineHeight = lineHeightEditor;\n\n const heightContent = `${height + 1}px`;\n contentElementWrap.style.minHeight = heightContent;\n\n const textareaLabel = document.querySelector('[for=\"' + this.settings.element + '\"]');\n\n textareaLabel.style.display = 'inline-block';\n textareaLabel.style.margin = 0;\n textareaLabel.style.height = heightContent;\n textareaLabel.style.minHeight = heightContent;\n textareaLabel.style.maxHeight = heightContent;\n\n // Align for the case using Supsub on the editor.\n if (textareaLabel.classList.contains('accesshide')) {\n textareaLabel.classList.remove('accesshide');\n textareaLabel.style.visibility = 'hidden';\n editorElement.style.marginLeft = `-${parseInt(textareaLabel.offsetWidth)}px`;\n } else {\n // Get parent node of the label.\n const labelParentNode = textareaLabel.parentNode;\n labelParentNode.style.paddingBottom = heightEditor;\n textareaLabel.style.verticalAlign = 'bottom';\n }\n\n textareaElement.insertAdjacentElement('beforebegin', editorElement);\n // Set the editor's content for the first time.\n this.getEditorContent().innerHTML = this.getContent();\n\n // Save the history from the beginning.\n this.saveHistory();\n\n // Wait until the editor element is added to the DOM before calculating\n // its size to ensure it aligns with the others elements.\n requestAnimationFrame(() => {\n textareaLabel.style.lineHeight = contentEditor.style.lineHeight;\n const heightWrapper = height + 1 + parseInt(toolbarEl.offsetHeight);\n editorElement.style.height = heightWrapper + 'px';\n editorElement.style.minHeight = heightWrapper + 'px';\n editorElement.style.maxHeight = heightWrapper + 'px';\n });\n\n document.addEventListener('click', (e) => {\n if (!editorElement.contains(e.target)) {\n // Get clean data.\n const cleanData = this.getCleanHTML();\n // Set it in both the hidden text area and the editor content.\n this.getTextArea().value = cleanData;\n this.getEditorContent().innerHTML = cleanData;\n this.setActiveButton(false);\n }\n });\n }\n\n /**\n * Make a content area for the editor.\n *\n * @return {HTMLElement} The content area element.\n */\n initEditorContent = () => {\n const {classes, custom} = this.settings;\n const contentElement = this.createElement('div', {\n 'class': (classes.content + ' ' + (custom.content ?? '')).trim(),\n contenteditable: true,\n autocapitalize: 'none',\n autocorrect: 'off',\n role: 'textbox',\n spellcheck: false,\n 'aria-live': 'off',\n id: `${this.settings.element.replace(/:/g, \":\")}editable`,\n });\n\n contentElement.addEventListener('blur', () => {\n this.saveHistory();\n });\n\n // Listen for the selection change event.\n document.addEventListener('selectionchange', () => this.handleSelectionChange());\n\n // Set up hotkeys for the editor and prevent the Enter key from making the text content a single line.\n contentElement.addEventListener('keydown', (event) => {\n // Selection range.\n const selection = window.getSelection();\n const range = selection.getRangeAt(0);\n const keyMap = {\n key: {\n 'ArrowUp': 'sup',\n '94': 'sup',\n 'ArrowDown': 'sub',\n '95': 'sub',\n\n },\n shiftKey: {\n '^': 'sup',\n '_': 'sub',\n }\n };\n if (keyMap.key[event.key] || (event.shiftKey && keyMap.shiftKey[event.key])) {\n event.preventDefault();\n this.handleSupSubHotKey(keyMap.key[event.key] || keyMap.shiftKey[event.key]);\n }\n\n if (event.ctrlKey) {\n this.saveHistory();\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n }\n\n // Handle undo/redo action.\n if (event.ctrlKey && event.key === 'z') {\n event.preventDefault();\n this.handleUndo();\n }\n\n if (event.ctrlKey && event.key === 'y') {\n event.preventDefault();\n this.handleRedo();\n }\n\n // In case the editor is empty we need to reset format\n // to prevent it remember the previous format.\n if (this.cleanHTML(event.target.innerHTML) === '' &&\n !this.isSelectionInsideSubSup()) {\n const emptyText = document.createTextNode('\\uFEFF');\n range.insertNode(emptyText);\n }\n\n this.getTextArea().value = this.getCleanHTML();\n });\n\n contentElement.addEventListener('paste', event => {\n this.handlePaste(event);\n });\n\n const wrapContent = this.createElement('div', {\n 'class': (classes.contentWrap + ' ' + (custom.contentWrap ?? '')).trim(),\n });\n\n wrapContent.appendChild(contentElement);\n\n return wrapContent;\n };\n\n /**\n * Handle event paste.\n *\n * @param {Event} event Event object.\n */\n handlePaste(event) {\n event.preventDefault();\n const types = event.clipboardData.types;\n let isHTML = false;\n\n // Check for different methods to determine if 'text/html' is present\n if (types?.contains) {\n isHTML = types.contains('text/html');\n } else if (types?.includes) {\n isHTML = types.includes('text/html');\n }\n\n let content;\n if (isHTML) {\n content = this.cleanPasteHTML(event.clipboardData.getData('text/html'));\n } else {\n content = event.clipboardData.getData('text');\n }\n\n // We need to clean the data before inserting it into the editor.\n const cleanData = content.replaceAll(/[\\r\\n]+/g, '');\n document.execCommand('insertHTML', false, cleanData);\n this.saveHistory();\n this.getTextArea().value = this.getCleanHTML();\n }\n\n /**\n * Handle event undo.\n */\n handleUndo() {\n if (this.historyIndex > 0) {\n this.historyIndex--;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event redo.\n */\n handleRedo() {\n if (this.historyIndex < this.history.length - 1) {\n this.historyIndex++;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event sup/sub.\n *\n * @param {String} action The sup/sub action.\n */\n handleSupSubHotKey(action) {\n const nodeEl = this.isSelectionInsideSubSup();\n if (nodeEl) {\n const nodeName = nodeEl.nodeName.toLowerCase();\n if (nodeName !== action) {\n this.setFormat(this.getActions(nodeName)[0]);\n }\n return;\n }\n if (this.isSupportSupSub(action)) {\n this.setFormat(this.getActions(action)[0]);\n }\n }\n\n /**\n * Based on the user's selection change, we will detect the pointer position to determine whether\n * the cursor is inside the sup/sub tag. Depending on this result, we will activate the corresponding button.\n */\n handleSelectionChange() {\n const selection = window.getSelection();\n\n // When the user makes a selection change inside the editor.\n if (this.getEditorContent().contains(selection.anchorNode)) {\n // Detect whether the pointer is inside the sup/sub tag.\n const node = this.isSelectionInsideSubSup();\n\n if (node) {\n // Activate the corresponding button in the toolbar.\n this.setActiveButton(node.nodeName.toLowerCase());\n } else {\n // Deactivate all the buttons.\n this.setActiveButton(false);\n }\n }\n }\n\n /**\n * Utility function to create a element with attributes.\n *\n * @param {String} tag - HTML tag name.\n * @param {Object} attributes - The attributes of the element, such as class, id, etc.\n * @return {HTMLElement} The element that was created.\n */\n createElement(tag, attributes = {}) {\n const element = document.createElement(tag);\n for (let attribute in attributes) {\n element.setAttribute(attribute, attributes[attribute]);\n }\n\n return element;\n }\n\n /**\n * Utility function to check whether the current selection is inside the sup/sub tag. Returns false if it's not.\n *\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSelectionInsideSubSup() {\n const selection = window.getSelection();\n if (selection.rangeCount === 0) {\n return false;\n }\n const range = selection.getRangeAt(0);\n const tagName = range.commonAncestorContainer.parentNode.nodeName;\n // If user doesn't select any text.\n if (selection.isCollapsed) {\n if (this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n return false;\n }\n let nodeNames;\n const selectionNodes = range.cloneContents().childNodes;\n for (let node of selectionNodes) {\n const nodeName = node.nodeName;\n if (node.textContent === '') {\n continue;\n }\n if (!(this.isSupSubTag(nodeName)) &&\n (nodeName === '#text' && !this.isSupSubTag(tagName))) {\n return false;\n }\n if (!nodeNames) {\n nodeNames = node;\n }\n if (!nodeNames.isEqualNode(node)) {\n return false;\n }\n }\n\n if (nodeNames.nodeName === '#text' || this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n\n return nodeNames;\n }\n\n /**\n * Check if the given tag name is 'sup' or 'sub'. Return true if it is.\n *\n * @param {String} tagName - Tag name need to check.\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSupSubTag(tagName) {\n return ['SUB', 'SUP'].includes(tagName);\n }\n\n /**\n * Utility function to highlight the sup/sub button.\n *\n * @param {String|Boolean} type - The type of the button: sup or sub.\n */\n setActiveButton(type) {\n const {toolbar, button} = this.settings.classes;\n // Deactivate all the existing buttons.\n this.getEditor().querySelectorAll(`.${toolbar} .${button}`)\n .forEach(button => button.classList.remove('highlight'));\n if (type !== false) {\n this.getSupSubButton(type)?.classList?.add('highlight');\n }\n }\n\n /**\n * Utility function to create a toolbar element that contains sup and sub buttons.\n *\n * @return {HTMLElement} The toolbar element.\n */\n initEditorToolbar() {\n const toolbarGroup = this.createElement('div', {\n 'class': (this.settings.classes.toolbarGroup + ' ' + (this.settings?.custom?.toolbarGroup ?? '')).trim(),\n });\n this.getActions(this.settings.type).forEach((action) => {\n const button = this.createElement('button', {\n 'class': this.settings?.classes.button + ' ' + action.class,\n title: this.settings.buttons[action.name].title,\n type: 'button',\n 'data-action': action.name,\n });\n\n button.innerHTML = this.settings.buttons[action.name].icon;\n button.setAttribute('type', 'button');\n button.onclick = () => {\n const selection = window.getSelection();\n const nodeEl = this.isSelectionInsideSubSup();\n if (selection.isCollapsed && nodeEl !== false) {\n if (nodeEl.nodeName.toLowerCase() !== action.tag) {\n button.blur();\n this.getEditorContent().focus();\n return;\n }\n }\n\n this.getEditorContent().focus();\n this.setFormat(action);\n };\n\n toolbarGroup.appendChild(button);\n });\n const toolbarEl = this.createElement('div', {\n 'class': (this.settings.classes.toolbar + ' ' + (this.settings?.custom?.toolbar ?? '')).trim(),\n });\n toolbarEl.appendChild(toolbarGroup);\n\n return toolbarEl;\n }\n\n /**\n * Based on the action (sup/sub), this function will format the selected text accordingly.\n *\n * @param {Object} action - The sup/sub action object.\n */\n setFormat(action) {\n // Selection text.\n const selection = window.getSelection();\n // Selection range.\n const range = selection.getRangeAt(0);\n const {tag} = action;\n const nodeEl = this.isSelectionInsideSubSup();\n // In case the user doesn't select any text.\n if (selection.isCollapsed) {\n // We need to check whether the current position of the pointer is inside a sub or sup tag.\n const parentNode = range.commonAncestorContainer.parentNode;\n if (parentNode.nodeName.toLowerCase() === tag) {\n // In this case, the pointer is inside a sub or sup tag, so we need to select all the text within the tag.\n // Then, we will slice it into two parts, using the current position of the cursor as the border.\n // The first part will extend from position 0 to the border, and the second part will span from\n // the border to the end.\n // After that, we will wrap each part in the corresponding sub or sup tag. The result will be:\n // First and Second.\n // Finally, we will create a text node with empty content (\\uFEFF) and place it at the border\n // of the two parts, resulting in:\n // First#textnode#Second.\n // Create the first part.\n const beforeText = this.createElement(tag);\n beforeText.innerText = parentNode.textContent.slice(0, range.startOffset);\n // Make an empty textnode.\n const emptyText = document.createTextNode('\\uFEFF');\n // Create an empty text node.\n const afterText = this.createElement(tag);\n afterText.innerText = parentNode.textContent.slice(range.startOffset);\n // Insert it into the DOM next to the parent node.\n if (afterText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(afterText, parentNode.nextSibling);\n }\n parentNode.parentNode.insertBefore(emptyText, parentNode.nextSibling);\n if (beforeText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(beforeText, parentNode.nextSibling);\n }\n\n // Remove the parent node.\n parentNode.remove();\n // We set the position of the cursor to be in the empty text node.\n range.setStart(emptyText, 1);\n range.setEnd(emptyText, 1);\n selection.removeAllRanges();\n selection.addRange(range);\n } else {\n // In case the user didn't select anything, we must create a sup/sub\n // tag with an empty string and move the cursor into it.\n const node = this.createElement(tag);\n // Zero-width space to keep the tag visible.\n node.appendChild(document.createTextNode('\\uFEFF'));\n // Update the new range within the existing one.\n range.insertNode(node);\n // Set the selection index at the next available space.\n range.setStart(node.firstChild, 1);\n range.setEnd(node.firstChild, 1);\n // Remove all existing ranges from the selection.\n selection.removeAllRanges();\n // Add the updated range object to the current selection.\n selection.addRange(range);\n }\n } else if (nodeEl) {\n // This means the user is selecting some text that is inside the sub or sup tag.\n // In this case, we only need to move the selected text inside the sub/sup tag outside of it.\n // For example: 123[456]789. If the selected text is 456, we will\n // move it outside the tag, resulting in 123456789.\n // Retrieve the selected text.\n // Retrieve the current tag (sub/sup) that wraps the selection\n const selectedText = range.toString();\n const parentElement = nodeEl;\n const nextSibling = parentElement.nextSibling;\n const beforeText = parentElement.textContent.slice(0, range.startOffset);\n const afterText = parentElement.textContent.slice(range.endOffset);\n if (beforeText) {\n const start = this.createElement(parentElement.nodeName.toLowerCase());\n start.textContent = beforeText;\n parentElement.parentNode.insertBefore(start, nextSibling);\n }\n // Create a text node based on the selected text.\n const textNode = document.createTextNode(selectedText);\n parentElement.parentNode.insertBefore(textNode, nextSibling);\n if (afterText) {\n const end = this.createElement(parentElement.nodeName.toLowerCase());\n end.textContent = afterText;\n parentElement.parentNode.insertBefore(end, nextSibling);\n }\n\n parentElement.remove();\n\n // Create a new range to select the inserted content\n range.setStart(textNode, 0);\n range.setEnd(textNode, selectedText.length);\n selection.removeAllRanges();\n selection.addRange(range);\n this.getTextArea().value = this.getCleanHTML();\n } else {\n // This case is user select a text that is not inside subsup.\n // We retrieve the selected text and then delete it in DOM.\n const selectedText = range.toString();\n range.deleteContents();\n const previousNode = range.commonAncestorContainer.previousSibling;\n const nextNode = range.commonAncestorContainer.nextSibling;\n // In addition, we will merge adjacent sup/sub tags into a single sup/sub tag.\n if (previousNode || nextNode) {\n const newNode = this.createElement(tag);\n let startOffset = 0;\n let endOffset = 0;\n let content = '';\n if (previousNode && previousNode?.nodeName?.toLowerCase() === tag) {\n content = previousNode.textContent;\n startOffset = content.length;\n previousNode.remove();\n }\n content += selectedText;\n endOffset = content.length;\n if (nextNode && nextNode?.nodeName?.toLowerCase() === tag) {\n content += nextNode.textContent;\n nextNode.remove();\n }\n newNode.textContent = content;\n if (content !== selectedText) {\n range.insertNode(newNode);\n range.setStart(newNode.firstChild, startOffset);\n range.setEnd(newNode.firstChild, endOffset);\n this.getTextArea().value = this.getCleanHTML();\n return;\n }\n }\n\n // Create a sup/sub tag that wrap the selected text.\n const newNode = document.createElement(tag);\n newNode.appendChild(document.createTextNode(selectedText));\n // Make a selection to the selected text.\n selection.removeAllRanges();\n // Insert it into DOM.\n range.insertNode(newNode);\n range.selectNodeContents(newNode.firstChild);\n selection.addRange(range);\n // Clean up all the empty text.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.getTextArea().value = this.getCleanHTML();\n }\n // Clean up.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.saveHistory();\n }\n\n /**\n * Save history for undo/redo actions.\n */\n saveHistory() {\n const content = this.getCleanHTML();\n if (this.historyIndex === -1 || content !== this.history[this.historyIndex]) {\n this.history.splice(this.historyIndex + 1);\n this.history.push(content);\n this.historyIndex++;\n }\n }\n\n /**\n * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.\n *\n * @param {String} content - The content data need to be clean.\n * @return {String} The clean text.\n */\n cleanPasteHTML(content) {\n // Return an empty string if passed an invalid or empty object.\n if (!content || content.length === 0) {\n return \"\";\n }\n\n // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc.).\n let rules = [\n {regex: /<\\s*\\/html\\s*>([\\s\\S]+)$/gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: /]*>[\\s\\S]*?<\\/xml>/gi, replace: \"\"},\n {regex: /<\\?xml[^>]*>[\\s\\S]*?<\\\\\\?xml>/gi, replace: \"\"},\n {regex: /<\\/?\\w+:[^>]*>/gi, replace: \"\"}\n ];\n\n // Apply the first set of harsher rules.\n content = this.filterContentWithRules(content, rules);\n\n // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.\n content = this.cleanHTML(content);\n\n // Check if the string is empty or only contains whitespace.\n if (content.length === 0 || !/\\S/.test(content)) {\n return content;\n }\n\n // Normalize the code by loading it into the DOM.\n const holder = document.createElement('div');\n holder.innerHTML = content;\n content = holder.innerHTML;\n\n // Free up the DOM memory.\n holder.innerHTML = \"\";\n\n // Run some more rules that care about quotes and whitespace.\n rules = [\n {regex: /(<[^>]*?style\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[-:][^>;\"]*;?)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*Apple-[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /
]*?name\\s*?=\\s*?\"OLE_LINK\\d*?\"[^>]*?>\\s*?<\\/a>/gi, replace: \"\"},\n ];\n\n // Apply the rules.\n content = this.filterContentWithRules(content, rules);\n\n // Reapply the standard cleaner to the content.\n return this.cleanHTML(content);\n }\n\n /**\n * Check if the editor allows the use of sub or sup features.\n *\n * @param {String} action - Sub/sup action to check.\n * @return {Boolean} The result after verifying whether it is allowed.\n */\n isSupportSupSub(action) {\n const {type} = this.settings;\n return type === 'both' || type === action;\n }\n\n /**\n * Utility function to filter the content based on the given rules.\n *\n * @param {String} content - The content need to be filtered.\n * @param {Object} rules - The rules list.\n * @return {String} The cleaned content will be returned.\n */\n filterContentWithRules(content, rules) {\n for (const element of rules) {\n content = content.replace(element.regex, element.replace);\n }\n return content;\n }\n\n /**\n * Utility function to clean the HTML.\n *\n * @param {String} content - The content need to be filter.\n * @return {String} The cleaned content will be returned.\n */\n cleanHTML(content) {\n // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.\n\n const rules = [\n // Remove empty paragraphs.\n {regex: /]*>( |\\s)*<\\/p>/gi, replace: \"\"},\n\n // Remove attributes on sup and sub tags.\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n\n // Replace   with space.\n {regex: / /gi, replace: \" \"},\n\n // Combine matching tags with spaces in between.\n {regex: /<\\/sup>(\\s*)+/gi, replace: \"$1\"},\n {regex: /<\\/sub>(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces after start sup and sub tags to before.\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces before end sup and sub tags to after.\n {regex: /(\\s*)+<\\/sup>/gi, replace: \"$1\"},\n {regex: /(\\s*)+<\\/sub>/gi, replace: \"$1\"},\n\n // Remove empty br tags.\n {regex: /
/gi, replace: \"\"},\n\n // Remove any style blocks. Some browsers do not work well with them in a contenteditable.\n // Plus style blocks are not allowed in body html, except with \"scoped\", which most browsers don't support as of 2015.\n // Reference: \"http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work\"\n {regex: /]*>[\\s\\S]*?<\\/style>/gi, replace: \"\"},\n\n // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.\n {regex: /)/gi, replace: \"\"},\n\n // Remove elements that can not contain visible text.\n {regex: /]*>[\\s\\S]*?<\\/script>/gi, replace: \"\"},\n\n // Source: \"http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html\"\n // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.\n {regex: /<\\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi, replace: \"\"},\n // Source:\"https://developer.mozilla.org/en/docs/Web/HTML/Element\"\n // Remove all elements except sup and sub.\n {regex: /<\\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:var|wbr|video)[^>]*?>/gi, replace: \"\"},\n\n // Deprecated elements that might still be used by older sites.\n {regex: /<\\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi, replace: \"\"},\n\n // Elements from common sites including google.com.\n {regex: /<\\/?(?:jsl|nobr)[^>]*?>/gi, replace: \"\"},\n\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty spans, but not ones from Rangy.\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>( |\\s)*<\\/span>/gi, replace: \"\"},\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty sup and sub tags that appear after pasting text.\n {regex: /]*>( |\\s)*<\\/sup>/gi, replace: \"\"},\n {regex: /]*>( |\\s)*<\\/sub>/gi, replace: \"\"},\n\n // Remove special xml namespace tag xmlns generate by browser plugin.\n {regex: /(.*?)<\\/xmlns.*?>/gi, replace: \"$1\"},\n {regex: /\\uFEFF/gi, replace: \"\"}\n ];\n\n return this.filterContentWithRules(content, rules);\n }\n\n /**\n * Clean the generated HTML content without modifying the editor content.\n *\n * This includes removing all YUI IDs from the generated content.\n *\n * @return {string} The cleaned HTML content.\n */\n getCleanHTML() {\n // Clone the editor so that we don't actually modify the real content.\n const editorClone = this.getEditorContent().cloneNode(true);\n let html;\n\n html = editorClone.innerHTML;\n\n // Define contents that are considered empty.\n const emptyContents = [\n '

',\n '


',\n '
',\n '

',\n '


',\n '

',\n '


',\n '

 

',\n '


 

',\n '

 

',\n '


 

',\n '

 

',\n '


 

'\n ];\n\n if (emptyContents.includes(html)) {\n return '';\n }\n\n // Clean the HTML content.\n return this.cleanHTML(html);\n }\n\n\n /**\n * Utility function to get the content element of the editor.\n *\n * @return {HTMLElement} The editor content element.\n */\n getEditorContent() {\n return this.getEditor().querySelector(`.${this.settings.classes.content}`);\n }\n\n /**\n * Utility function to get the editor element. This element will contain all the components of the editor.\n *\n * @return {HTMLElement} The editor element.\n */\n getEditor() {\n return document.getElementById(`${this.settings.classes.editor}-${this.settings.element}`);\n }\n\n /**\n * Utility function to retrieve the button element based on the given type.\n *\n * @param {String} type - The type of the button: sup or sub.\n * @return {HTMLElement} The corresponding button.\n */\n getSupSubButton(type) {\n const {toolbar, button} = this.settings.classes;\n return this.getEditor().querySelector(`.${toolbar} .${button}[data-action^=\"${type}\"]`);\n }\n\n /**\n * Utility function to get button settings (sup/sub) based on the given type.\n *\n * @param {String} type - The type of the button can be either sup or sub.\n * @return {Object} The settings for the given button.\n */\n getActions(type) {\n if (defaultActions[type]) {\n return [defaultActions[type]];\n }\n\n return Object.values(defaultActions);\n }\n\n /**\n * Utility to get the button container for the editor.\n *\n * @return {HTMLElement} The button container.\n */\n getButtonContainer() {\n return this.getEditor().querySelectorAll(`.${this.settings.classes.toolbar} .${this.settings.classes.button}`);\n }\n\n /**\n * Utility function to get the content of the original textarea.\n *\n * @return {String} The content.\n */\n getContent() {\n return this.getTextArea().value;\n }\n\n /**\n * Utility function to get id of the element.\n *\n * @return {String} The element id.\n */\n getEditorId() {\n return this.settings.element;\n }\n\n /**\n * Return the text area element.\n *\n * @return {HTMLElement} Text area element.\n */\n getTextArea() {\n return document.getElementById(this.settings.element);\n }\n\n}\n\n/**\n * Load editor based on the given setting.\n *\n * @param {Object} settings - The editor setting.\n */\nexport const loadEditor = settings => {\n const editor = new OUSupSubEditor(settings);\n // We need to do this for a specific reason, currently only for the Behat test.\n // We can easily utilize the editor's API.\n if (!window.OUSupSubEditor) {\n window.OUSupSubEditor = {\n instances: {\n [settings.element]: editor,\n },\n addEditor: function(editor) {\n this.instances[editor.getEditorId()] = editor;\n },\n getEditorById: function(editorId) {\n return this.instances[editorId];\n },\n };\n } else {\n window.OUSupSubEditor.addEditor(editor);\n }\n};\n"],"names":["defaultActions","sup","name","tag","sub","OUSupSubEditor","constructor","settings","element","type","classes","wrap","editor","contentWrap","content","toolbar","toolbarGroup","button","custom","this","contentElement","createElement","trim","contenteditable","autocapitalize","autocorrect","role","spellcheck","id","replace","addEventListener","saveHistory","document","handleSelectionChange","event","range","window","getSelection","getRangeAt","keyMap","key","shiftKey","preventDefault","handleSupSubHotKey","ctrlKey","handleUndo","handleRedo","cleanHTML","target","innerHTML","isSelectionInsideSubSup","emptyText","createTextNode","insertNode","getTextArea","value","getCleanHTML","handlePaste","wrapContent","appendChild","Object","assign","defaultSetting","init","textareaElement","style","display","editorElement","editorWrap","toolbarEl","initEditorToolbar","contentElementWrap","initEditorContent","contentEditor","querySelector","width","getAttribute","minWidth","maxWidth","height","heightEditor","lineHeightEditor","minHeight","maxHeight","lineHeight","heightContent","textareaLabel","margin","classList","contains","remove","visibility","marginLeft","parseInt","offsetWidth","parentNode","paddingBottom","verticalAlign","insertAdjacentElement","getEditorContent","getContent","requestAnimationFrame","heightWrapper","offsetHeight","e","cleanData","setActiveButton","types","clipboardData","isHTML","includes","cleanPasteHTML","getData","replaceAll","execCommand","historyIndex","history","length","action","nodeEl","nodeName","toLowerCase","setFormat","getActions","isSupportSupSub","selection","anchorNode","node","attributes","attribute","setAttribute","rangeCount","tagName","commonAncestorContainer","isCollapsed","isSupSubTag","nodeNames","selectionNodes","cloneContents","childNodes","textContent","isEqualNode","getEditor","querySelectorAll","forEach","getSupSubButton","add","_this$settings","_this$settings$custom2","class","title","buttons","icon","onclick","blur","focus","_this$settings3","_this$settings3$custo","beforeText","innerText","slice","startOffset","afterText","insertBefore","nextSibling","setStart","setEnd","removeAllRanges","addRange","firstChild","selectedText","toString","parentElement","endOffset","start","textNode","end","deleteContents","previousNode","previousSibling","nextNode","newNode","selectNodeContents","el","splice","push","rules","regex","filterContentWithRules","test","holder","html","cloneNode","getElementById","values","getButtonContainer","getEditorId","addEditor","instances","getEditorById","editorId"],"mappings":";;;;;;;8FAuBMA,eAAiB,CACnBC,IAAK,CACDC,KAAM,cACNC,IAAK,YACI,2CAEbC,IAAK,CACDF,KAAM,YACNC,IAAK,YACI,8CAIXE,eAoCFC,YAAYC,gDAjCK,CACbC,QAAS,GACTC,KAAM,OACNC,QAAS,CACLC,KAAM,gBACNC,OAAQ,kBACRC,YAAa,+BACbC,QAAS,0BACTC,QAAS,0BACTC,aAAc,iBACdC,OAAQ,mBAEZC,OAAQ,CACJN,OAAQ,GACRE,QAAS,GACTC,QAAS,GACTE,OAAQ,GACRJ,YAAa,GACbF,KAAM,GACNK,aAAc,qCAKZ,yCACM,6CA0HI,mDACVN,QAACA,QAADQ,OAAUA,QAAUC,KAAKZ,SACzBa,eAAiBD,KAAKE,cAAc,MAAO,QACnCX,QAAQI,QAAU,6BAAOI,OAAOJ,mDAAW,KAAKQ,OAC1DC,iBAAiB,EACjBC,eAAgB,OAChBC,YAAa,MACbC,KAAM,UACNC,YAAY,cACC,MACbC,aAAOT,KAAKZ,SAASC,QAAQqB,QAAQ,KAAM,mBAG/CT,eAAeU,iBAAiB,QAAQ,UAC/BC,iBAITC,SAASF,iBAAiB,mBAAmB,IAAMX,KAAKc,0BAGxDb,eAAeU,iBAAiB,WAAYI,cAGlCC,MADYC,OAAOC,eACDC,WAAW,GAC7BC,OAAS,CACXC,IAAK,SACU,SACL,gBACO,SACP,OAGVC,SAAU,KACD,QACA,YAGTF,OAAOC,IAAIN,MAAMM,MAASN,MAAMO,UAAYF,OAAOE,SAASP,MAAMM,QAClEN,MAAMQ,sBACDC,mBAAmBJ,OAAOC,IAAIN,MAAMM,MAAQD,OAAOE,SAASP,MAAMM,OAGvEN,MAAMU,cACDb,cAGS,UAAdG,MAAMM,KACNN,MAAMQ,iBAINR,MAAMU,SAAyB,MAAdV,MAAMM,MACvBN,MAAMQ,sBACDG,cAGLX,MAAMU,SAAyB,MAAdV,MAAMM,MACvBN,MAAMQ,sBACDI,cAKsC,KAA3C3B,KAAK4B,UAAUb,MAAMc,OAAOC,aAC3B9B,KAAK+B,0BAA2B,OAC3BC,UAAYnB,SAASoB,eAAe,UAC1CjB,MAAMkB,WAAWF,gBAGhBG,cAAcC,MAAQpC,KAAKqC,kBAGpCpC,eAAeU,iBAAiB,SAASI,aAChCuB,YAAYvB,gBAGfwB,YAAcvC,KAAKE,cAAc,MAAO,QAChCX,QAAQG,YAAc,iCAAOK,OAAOL,+DAAe,KAAKS,gBAGtEoC,YAAYC,YAAYvC,gBAEjBsC,oBApMFnD,SAAWqD,OAAOC,OAAO1C,KAAK2C,eAAgBvD,eAC9CwD,OAMTA,gCACUC,gBAAkB7C,KAAKmC,eACvB5C,QAACA,QAADQ,OAAUA,QAAUC,KAAKZ,aAE1ByD,uBAILA,gBAAgBC,MAAMC,QAAU,aAE1BC,cAAgBhD,KAAKE,cAAc,MAAO,QAClCX,QAAQE,OAAS,4BAAOM,MAAAA,cAAAA,OAAQN,gDAAU,KAAKU,OACzDM,GAAIlB,QAAQE,OAAS,IAAMO,KAAKZ,SAASC,UAIvC4D,WAAajD,KAAKE,cAAc,MAAO,QAC/BX,QAAQC,KAAO,IAAMO,OAAOP,MAAMW,SAI1C+C,UAAYlD,KAAKmD,oBACvBF,WAAWT,YAAYU,iBAGjBE,mBAAqBpD,KAAKqD,oBAC1BC,cAAgBF,mBAAmBG,yBAAkBvD,KAAKZ,SAASG,QAAQI,UAGjFsD,WAAWT,YAAYY,oBACvBJ,cAAcR,YAAYS,kBAGpBO,MAAmD,EAA1CxD,KAAKmC,cAAcsB,aAAa,QAAc,GAAM,KACnEH,cAAcR,MAAMU,MAAQA,MAC5BF,cAAcR,MAAMY,SAAWF,MAC/BF,cAAcR,MAAMa,SAAWH,YAGzBI,OAAiB,EADV5D,KAAKmC,cAAcsB,aAAa,QAClB,GACrBI,uBAAkBD,OAAS,SAC3BE,2BAAsBF,OAAS,QAGrCN,cAAcR,MAAMc,OAASC,aAC7BP,cAAcR,MAAMiB,UAAYF,aAChCP,cAAcR,MAAMkB,UAAYH,aAChCP,cAAcR,MAAMmB,WAAaH,uBAE3BI,wBAAmBN,OAAS,QAClCR,mBAAmBN,MAAMiB,UAAYG,oBAE/BC,cAAgBtD,SAAS0C,cAAc,SAAWvD,KAAKZ,SAASC,QAAU,SAEhF8E,cAAcrB,MAAMC,QAAU,eAC9BoB,cAAcrB,MAAMsB,OAAS,EAC7BD,cAAcrB,MAAMc,OAASM,cAC7BC,cAAcrB,MAAMiB,UAAYG,cAChCC,cAAcrB,MAAMkB,UAAYE,cAG5BC,cAAcE,UAAUC,SAAS,cACjCH,cAAcE,UAAUE,OAAO,cAC/BJ,cAAcrB,MAAM0B,WAAa,SACjCxB,cAAcF,MAAM2B,sBAAiBC,SAASP,cAAcQ,uBACzD,CAEqBR,cAAcS,WACtB9B,MAAM+B,cAAgBhB,aACtCM,cAAcrB,MAAMgC,cAAgB,SAGxCjC,gBAAgBkC,sBAAsB,cAAe/B,oBAEhDgC,mBAAmBlD,UAAY9B,KAAKiF,kBAGpCrE,cAILsE,uBAAsB,KAClBf,cAAcrB,MAAMmB,WAAaX,cAAcR,MAAMmB,iBAC/CkB,cAAgBvB,OAAS,EAAIc,SAASxB,UAAUkC,cACtDpC,cAAcF,MAAMc,OAASuB,cAAgB,KAC7CnC,cAAcF,MAAMiB,UAAYoB,cAAgB,KAChDnC,cAAcF,MAAMkB,UAAYmB,cAAgB,QAGpDtE,SAASF,iBAAiB,SAAU0E,QAC3BrC,cAAcsB,SAASe,EAAExD,QAAS,OAE7ByD,UAAYtF,KAAKqC,oBAElBF,cAAcC,MAAQkD,eACtBN,mBAAmBlD,UAAYwD,eAC/BC,iBAAgB,OAqGjCjD,YAAYvB,OACRA,MAAMQ,uBACAiE,MAAQzE,MAAM0E,cAAcD,UAU9B7F,QATA+F,QAAS,EAGTF,MAAAA,OAAAA,MAAOlB,SACPoB,OAASF,MAAMlB,SAAS,aACjBkB,MAAAA,OAAAA,MAAOG,WACdD,OAASF,MAAMG,SAAS,cAKxBhG,QADA+F,OACU1F,KAAK4F,eAAe7E,MAAM0E,cAAcI,QAAQ,cAEhD9E,MAAM0E,cAAcI,QAAQ,cAIpCP,UAAY3F,QAAQmG,WAAW,WAAY,IACjDjF,SAASkF,YAAY,cAAc,EAAOT,gBACrC1E,mBACAuB,cAAcC,MAAQpC,KAAKqC,eAMpCX,aACQ1B,KAAKgG,aAAe,SACfA,oBACAhB,mBAAmBlD,UAAY9B,KAAKiG,QAAQjG,KAAKgG,mBACjD7D,cAAcC,MAAQpC,KAAKiG,QAAQjG,KAAKgG,eAOrDrE,aACQ3B,KAAKgG,aAAehG,KAAKiG,QAAQC,OAAS,SACrCF,oBACAhB,mBAAmBlD,UAAY9B,KAAKiG,QAAQjG,KAAKgG,mBACjD7D,cAAcC,MAAQpC,KAAKiG,QAAQjG,KAAKgG,eASrDxE,mBAAmB2E,cACTC,OAASpG,KAAK+B,6BAChBqE,cACMC,SAAWD,OAAOC,SAASC,cAC7BD,WAAaF,aACRI,UAAUvG,KAAKwG,WAAWH,UAAU,SAI7CrG,KAAKyG,gBAAgBN,cAChBI,UAAUvG,KAAKwG,WAAWL,QAAQ,IAQ/CrF,8BACU4F,UAAYzF,OAAOC,kBAGrBlB,KAAKgF,mBAAmBV,SAASoC,UAAUC,YAAa,OAElDC,KAAO5G,KAAK+B,0BAEd6E,UAEKrB,gBAAgBqB,KAAKP,SAASC,oBAG9Bf,iBAAgB,IAYjCrF,cAAclB,SAAK6H,kEAAa,SACtBxH,QAAUwB,SAASX,cAAclB,SAClC,IAAI8H,aAAaD,WAClBxH,QAAQ0H,aAAaD,UAAWD,WAAWC,mBAGxCzH,QAQX0C,gCACU2E,UAAYzF,OAAOC,kBACI,IAAzBwF,UAAUM,kBACH,QAELhG,MAAQ0F,UAAUvF,WAAW,GAC7B8F,QAAUjG,MAAMkG,wBAAwBtC,WAAWyB,YAErDK,UAAUS,oBACNnH,KAAKoH,YAAYH,UACVjG,MAAMkG,wBAAwBtC,eAIzCyC,gBACEC,eAAiBtG,MAAMuG,gBAAgBC,eACxC,IAAIZ,QAAQU,eAAgB,OACvBjB,SAAWO,KAAKP,YACG,KAArBO,KAAKa,iBAGHzH,KAAKoH,YAAYf,WACL,UAAbA,WAAyBrG,KAAKoH,YAAYH,gBACpC,KAENI,YACDA,UAAYT,OAEXS,UAAUK,YAAYd,aAChB,SAIY,UAAvBS,UAAUhB,UAAwBrG,KAAKoH,YAAYH,SAC5CjG,MAAMkG,wBAAwBtC,WAGlCyC,UASXD,YAAYH,eACD,CAAC,MAAO,OAAOtB,SAASsB,SAQnC1B,gBAAgBjG,YACNM,QAACA,QAADE,OAAUA,QAAUE,KAAKZ,SAASG,+DAEnCoI,YAAYC,4BAAqBhI,qBAAYE,SAC7C+H,SAAQ/H,QAAUA,OAAOuE,UAAUE,OAAO,gBAClC,IAATjF,2CACKwI,gBAAgBxI,6FAAO+E,oEAAW0D,IAAI,cASnD5E,uJACUtD,aAAeG,KAAKE,cAAc,MAAO,QACjCF,KAAKZ,SAASG,QAAQM,aAAe,0DAAOG,KAAKZ,mEAAL4I,eAAejI,gDAAfkI,uBAAuBpI,oEAAgB,KAAKM,cAEjGqG,WAAWxG,KAAKZ,SAASE,MAAMuI,SAAS1B,mCACnCrG,OAASE,KAAKE,cAAc,SAAU,qCAC1Bd,2DAAUG,QAAQO,QAAS,IAAMqG,OAAO+B,MACtDC,MAAOnI,KAAKZ,SAASgJ,QAAQjC,OAAOpH,MAAMoJ,MAC1C7I,KAAM,uBACS6G,OAAOpH,OAG1Be,OAAOgC,UAAY9B,KAAKZ,SAASgJ,QAAQjC,OAAOpH,MAAMsJ,KACtDvI,OAAOiH,aAAa,OAAQ,UAC5BjH,OAAOwI,QAAU,WACP5B,UAAYzF,OAAOC,eACnBkF,OAASpG,KAAK+B,6BAChB2E,UAAUS,cAA0B,IAAXf,QACrBA,OAAOC,SAASC,gBAAkBH,OAAOnH,WACzCc,OAAOyI,iBACFvD,mBAAmBwD,aAK3BxD,mBAAmBwD,aACnBjC,UAAUJ,SAGnBtG,aAAa2C,YAAY1C,iBAEvBoD,UAAYlD,KAAKE,cAAc,MAAO,QAC9BF,KAAKZ,SAASG,QAAQK,QAAU,4DAAOI,KAAKZ,mEAALqJ,gBAAe1I,+CAAf2I,sBAAuB9I,iEAAW,KAAKO,gBAE5F+C,UAAUV,YAAY3C,cAEfqD,UAQXqD,UAAUJ,cAEAO,UAAYzF,OAAOC,eAEnBF,MAAQ0F,UAAUvF,WAAW,IAC7BnC,IAACA,KAAOmH,OACRC,OAASpG,KAAK+B,6BAEhB2E,UAAUS,YAAa,OAEjBvC,WAAa5D,MAAMkG,wBAAwBtC,cAC7CA,WAAWyB,SAASC,gBAAkBtH,IAAK,OAWrC2J,WAAa3I,KAAKE,cAAclB,KACtC2J,WAAWC,UAAYhE,WAAW6C,YAAYoB,MAAM,EAAG7H,MAAM8H,mBAEvD9G,UAAYnB,SAASoB,eAAe,UAEpC8G,UAAY/I,KAAKE,cAAclB,KACrC+J,UAAUH,UAAYhE,WAAW6C,YAAYoB,MAAM7H,MAAM8H,aAE7B,KAAxBC,UAAUjH,WACV8C,WAAWA,WAAWoE,aAAaD,UAAWnE,WAAWqE,aAE7DrE,WAAWA,WAAWoE,aAAahH,UAAW4C,WAAWqE,aAC5B,KAAzBN,WAAW7G,WACX8C,WAAWA,WAAWoE,aAAaL,WAAY/D,WAAWqE,aAI9DrE,WAAWL,SAEXvD,MAAMkI,SAASlH,UAAW,GAC1BhB,MAAMmI,OAAOnH,UAAW,GACxB0E,UAAU0C,kBACV1C,UAAU2C,SAASrI,WAChB,OAGG4F,KAAO5G,KAAKE,cAAclB,KAEhC4H,KAAKpE,YAAY3B,SAASoB,eAAe,WAEzCjB,MAAMkB,WAAW0E,MAEjB5F,MAAMkI,SAAStC,KAAK0C,WAAY,GAChCtI,MAAMmI,OAAOvC,KAAK0C,WAAY,GAE9B5C,UAAU0C,kBAEV1C,UAAU2C,SAASrI,aAEpB,GAAIoF,OAAQ,OAOTmD,aAAevI,MAAMwI,WACrBC,cAAgBrD,OAChB6C,YAAcQ,cAAcR,YAC5BN,WAAac,cAAchC,YAAYoB,MAAM,EAAG7H,MAAM8H,aACtDC,UAAYU,cAAchC,YAAYoB,MAAM7H,MAAM0I,cACpDf,WAAY,OACNgB,MAAQ3J,KAAKE,cAAcuJ,cAAcpD,SAASC,eACxDqD,MAAMlC,YAAckB,WACpBc,cAAc7E,WAAWoE,aAAaW,MAAOV,mBAG3CW,SAAW/I,SAASoB,eAAesH,iBACzCE,cAAc7E,WAAWoE,aAAaY,SAAUX,aAC5CF,UAAW,OACLc,IAAM7J,KAAKE,cAAcuJ,cAAcpD,SAASC,eACtDuD,IAAIpC,YAAcsB,UAClBU,cAAc7E,WAAWoE,aAAaa,IAAKZ,aAG/CQ,cAAclF,SAGdvD,MAAMkI,SAASU,SAAU,GACzB5I,MAAMmI,OAAOS,SAAUL,aAAarD,QACpCQ,UAAU0C,kBACV1C,UAAU2C,SAASrI,YACdmB,cAAcC,MAAQpC,KAAKqC,mBAC7B,OAGGkH,aAAevI,MAAMwI,WAC3BxI,MAAM8I,uBACAC,aAAe/I,MAAMkG,wBAAwB8C,gBAC7CC,SAAWjJ,MAAMkG,wBAAwB+B,eAE3Cc,cAAgBE,SAAU,oDACpBC,QAAUlK,KAAKE,cAAclB,SAC/B8J,YAAc,EACdY,UAAY,EACZ/J,QAAU,MACVoK,eAAgBA,MAAAA,4CAAAA,aAAc1D,uEAAUC,iBAAkBtH,MAC1DW,QAAUoK,aAAatC,YACvBqB,YAAcnJ,QAAQuG,OACtB6D,aAAaxF,UAEjB5E,SAAW4J,aACXG,UAAY/J,QAAQuG,OAChB+D,WAAYA,MAAAA,qCAAAA,SAAU5D,iEAAUC,iBAAkBtH,MAClDW,SAAWsK,SAASxC,YACpBwC,SAAS1F,UAEb2F,QAAQzC,YAAc9H,QAClBA,UAAY4J,oBACZvI,MAAMkB,WAAWgI,SACjBlJ,MAAMkI,SAASgB,QAAQZ,WAAYR,aACnC9H,MAAMmI,OAAOe,QAAQZ,WAAYI,qBAC5BvH,cAAcC,MAAQpC,KAAKqC,sBAMlC6H,QAAUrJ,SAASX,cAAclB,KACvCkL,QAAQ1H,YAAY3B,SAASoB,eAAesH,eAE5C7C,UAAU0C,kBAEVpI,MAAMkB,WAAWgI,SACjBlJ,MAAMmJ,mBAAmBD,QAAQZ,YACjC5C,UAAU2C,SAASrI,YAEdgE,mBAAmBwC,WAAWK,SAAQuC,KACnB,UAAhBA,GAAG/D,UAA2C,KAAnB+D,GAAG3C,aAC9B2C,GAAG7F,iBAGNpC,cAAcC,MAAQpC,KAAKqC,oBAG/B2C,mBAAmBwC,WAAWK,SAAQuC,KACnB,UAAhBA,GAAG/D,UAA2C,KAAnB+D,GAAG3C,aAC9B2C,GAAG7F,iBAGN3D,cAMTA,oBACUjB,QAAUK,KAAKqC,gBACM,IAAvBrC,KAAKgG,cAAuBrG,UAAYK,KAAKiG,QAAQjG,KAAKgG,qBACrDC,QAAQoE,OAAOrK,KAAKgG,aAAe,QACnCC,QAAQqE,KAAK3K,cACbqG,gBAUbJ,eAAejG,aAENA,SAA8B,IAAnBA,QAAQuG,aACb,OAIPqE,MAAQ,CACR,CAACC,MAAO,6BAA8B9J,QAAS,IAC/C,CAAC8J,MAAO,+BAAgC9J,QAAS,IACjD,CAAC8J,MAAO,+BAAgC9J,QAAS,IACjD,CAAC8J,MAAO,8BAA+B9J,QAAS,IAChD,CAAC8J,MAAO,kCAAmC9J,QAAS,IACpD,CAAC8J,MAAO,mBAAoB9J,QAAS,QAIzCf,QAAUK,KAAKyK,uBAAuB9K,QAAS4K,OAMxB,KAHvB5K,QAAUK,KAAK4B,UAAUjC,UAGbuG,SAAiB,KAAKwE,KAAK/K,gBAC5BA,cAILgL,OAAS9J,SAASX,cAAc,cACtCyK,OAAO7I,UAAYnC,QACnBA,QAAUgL,OAAO7I,UAGjB6I,OAAO7I,UAAY,GAGnByI,MAAQ,CACJ,CAACC,MAAO,8DAA+D9J,QAAS,MAChF,CAAC8J,MAAO,+DAAgE9J,QAAS,MACjF,CAAC8J,MAAO,kEAAmE9J,QAAS,MACpF,CAAC8J,MAAO,yDAA0D9J,QAAS,KAI/Ef,QAAUK,KAAKyK,uBAAuB9K,QAAS4K,OAGxCvK,KAAK4B,UAAUjC,SAS1B8G,gBAAgBN,cACN7G,KAACA,MAAQU,KAAKZ,eACJ,SAATE,MAAmBA,OAAS6G,OAUvCsE,uBAAuB9K,QAAS4K,WACvB,MAAMlL,WAAWkL,MAClB5K,QAAUA,QAAQe,QAAQrB,QAAQmL,MAAOnL,QAAQqB,gBAE9Cf,QASXiC,UAAUjC,gBA+ECK,KAAKyK,uBAAuB9K,QA5ErB,CAEV,CAAC6K,MAAO,8BAA+B9J,QAAS,IAGhD,CAAC8J,MAAO,2BAA4B9J,QAAS,SAC7C,CAAC8J,MAAO,2BAA4B9J,QAAS,SAG7C,CAAC8J,MAAO,WAAY9J,QAAS,KAG7B,CAAC8J,MAAO,uBAAwB9J,QAAS,MACzC,CAAC8J,MAAO,uBAAwB9J,QAAS,MAGzC,CAAC8J,MAAO,gBAAiB9J,QAAS,WAClC,CAAC8J,MAAO,gBAAiB9J,QAAS,WAGlC,CAAC8J,MAAO,kBAAmB9J,QAAS,YACpC,CAAC8J,MAAO,kBAAmB9J,QAAS,YAGpC,CAAC8J,MAAO,SAAU9J,QAAS,IAK3B,CAAC8J,MAAO,kCAAmC9J,QAAS,IAGpD,CAAC8J,MAAO,wBAAyB9J,QAAS,IAG1C,CAAC8J,MAAO,oCAAqC9J,QAAS,IAItD,CAAC8J,MAAO,0EAA2E9J,QAAS,IAC5F,CAAC8J,MAAO,sCAAuC9J,QAAS,IAGxD,CAAC8J,MAAO,iFAAkF9J,QAAS,IACnG,CAAC8J,MAAO,6EAA8E9J,QAAS,IAC/F,CAAC8J,MAAO,kFAAmF9J,QAAS,IACpG,CAAC8J,MAAO,kFAAmF9J,QAAS,IACpG,CAAC8J,MAAO,gFAAiF9J,QAAS,IAClG,CAAC8J,MAAO,kFAAmF9J,QAAS,IACpG,CAAC8J,MAAO,4EAA6E9J,QAAS,IAC9F,CAAC8J,MAAO,gFAAiF9J,QAAS,IAClG,CAAC8J,MAAO,qFAAsF9J,QAAS,IACvG,CAAC8J,MAAO,iCAAkC9J,QAAS,IAGnD,CAAC8J,MAAO,uFAAwF9J,QAAS,IACzG,CAAC8J,MAAO,kEAAmE9J,QAAS,IAGpF,CAAC8J,MAAO,4BAA6B9J,QAAS,IAE9C,CAAC8J,MAAO,gFAAiF9J,QAAS,MAGlG,CAAC8J,MAAO,0EAA2E9J,QAAS,IAC5F,CAAC8J,MAAO,gFAAiF9J,QAAS,MAGlG,CAAC8J,MAAO,kCAAmC9J,QAAS,IACpD,CAAC8J,MAAO,kCAAmC9J,QAAS,IAGpD,CAAC8J,MAAO,gCAAiC9J,QAAS,MAClD,CAAC8J,MAAO,WAAY9J,QAAS,MAarC2B,mBAGQuI,KAEJA,KAHoB5K,KAAKgF,mBAAmB6F,WAAU,GAGnC/I,gBAGG,CAClB,UACA,cACA,OACA,+CACA,mDACA,8CACA,kDACA,gBACA,oBACA,qDACA,yDACA,oDACA,yDAGc6D,SAASiF,MAChB,GAIJ5K,KAAK4B,UAAUgJ,MAS1B5F,0BACWhF,KAAK2H,YAAYpE,yBAAkBvD,KAAKZ,SAASG,QAAQI,UAQpEgI,mBACW9G,SAASiK,yBAAkB9K,KAAKZ,SAASG,QAAQE,mBAAUO,KAAKZ,SAASC,UASpFyI,gBAAgBxI,YACNM,QAACA,QAADE,OAAUA,QAAUE,KAAKZ,SAASG,eACjCS,KAAK2H,YAAYpE,yBAAkB3D,qBAAYE,iCAAwBR,YASlFkH,WAAWlH,aACHT,eAAeS,MACR,CAACT,eAAeS,OAGpBmD,OAAOsI,OAAOlM,gBAQzBmM,4BACWhL,KAAK2H,YAAYC,4BAAqB5H,KAAKZ,SAASG,QAAQK,qBAAYI,KAAKZ,SAASG,QAAQO,SAQzGmF,oBACWjF,KAAKmC,cAAcC,MAQ9B6I,qBACWjL,KAAKZ,SAASC,QAQzB8C,qBACWtB,SAASiK,eAAe9K,KAAKZ,SAASC,8BAU3BD,iBAChBK,OAAS,IAAIP,eAAeE,UAG7B6B,OAAO/B,eAaR+B,OAAO/B,eAAegM,UAAUzL,QAZhCwB,OAAO/B,eAAiB,CACpBiM,UAAW,EACN/L,SAASC,SAAUI,QAExByL,UAAW,SAASzL,aACX0L,UAAU1L,OAAOwL,eAAiBxL,QAE3C2L,cAAe,SAASC,iBACbrL,KAAKmL,UAAUE"} \ No newline at end of file diff --git a/amd/src/editor.js b/amd/src/editor.js new file mode 100644 index 0000000..e9d6c86 --- /dev/null +++ b/amd/src/editor.js @@ -0,0 +1,983 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * OUSupSub Editor Manager. + * + * @module editor_ousupsub/editor + * @copyright 2024 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const defaultActions = { + sup: { + name: 'superscript', + tag: 'sup', + 'class': 'ousupsub_superscript_button_superscript', + }, + sub: { + name: 'subscript', + tag: 'sub', + 'class': 'ousupsub_subscript_button_subscript', + }, +}; + +class OUSupSubEditor { + + // The editor's initial default settings. + defaultSetting = { + element: '', + type: 'both', + classes: { + wrap: 'ousupsub-wrap', + editor: 'editor_ousupsub', + contentWrap: 'editor_ousupsub_content_wrap', + content: 'editor_ousupsub_content', + toolbar: 'editor_ousupsub_toolbar', + toolbarGroup: 'ousupsub_group', + button: 'ousupsub-button', + }, + custom: { + editor: '', + content: '', + toolbar: '', + button: '', + contentWrap: '', + wrap: '', + toolbarGroup: '', + }, + }; + + // Support for undo/redo with history data and history index. + history = []; + historyIndex = -1; + + /** + * Constructor of the editor. + * @constructor + * + * @param {Object} settings - The editor settings. + */ + constructor(settings) { + this.settings = Object.assign(this.defaultSetting, settings); + this.init(); + } + + /** + * Initial the editor. + */ + init() { + const textareaElement = this.getTextArea(); + const {classes, custom} = this.settings; + + if (!textareaElement) { + return; + } + // Hidden origin text area. + textareaElement.style.display = 'none'; + + const editorElement = this.createElement('div', { + 'class': (classes.editor + ' ' + (custom?.editor ?? '')).trim(), + id: classes.editor + '-' + this.settings.element, + }); + + // Make editor container. + const editorWrap = this.createElement('div', { + 'class': (classes.wrap + ' ' + custom.wrap).trim(), + }); + + // Make toolbar containers. + const toolbarEl = this.initEditorToolbar(); + editorWrap.appendChild(toolbarEl); + + // Make the content for editor. + const contentElementWrap = this.initEditorContent(); + const contentEditor = contentElementWrap.querySelector(`.${this.settings.classes.content}`); + + // Append the editor's elements to the DOM. + editorWrap.appendChild(contentElementWrap); + editorElement.appendChild(editorWrap); + + // Calculate the editor size based on the attributes 'cols' and 'rows'. + const width = (this.getTextArea().getAttribute('cols') * 6 + 41) + 'px'; + contentEditor.style.width = width; + contentEditor.style.minWidth = width; + contentEditor.style.maxWidth = width; + + const rows = this.getTextArea().getAttribute('rows'); + const height = (rows * 6 + 13); + const heightEditor = `${height - 10}px`; + const lineHeightEditor = `${height - 6}px`; + + // Set the size of the editor. + contentEditor.style.height = heightEditor; + contentEditor.style.minHeight = heightEditor; + contentEditor.style.maxHeight = heightEditor; + contentEditor.style.lineHeight = lineHeightEditor; + + const heightContent = `${height + 1}px`; + contentElementWrap.style.minHeight = heightContent; + + const textareaLabel = document.querySelector('[for="' + this.settings.element + '"]'); + + textareaLabel.style.display = 'inline-block'; + textareaLabel.style.margin = 0; + textareaLabel.style.height = heightContent; + textareaLabel.style.minHeight = heightContent; + textareaLabel.style.maxHeight = heightContent; + + // Align for the case using Supsub on the editor. + if (textareaLabel.classList.contains('accesshide')) { + textareaLabel.classList.remove('accesshide'); + textareaLabel.style.visibility = 'hidden'; + editorElement.style.marginLeft = `-${parseInt(textareaLabel.offsetWidth)}px`; + } else { + // Get parent node of the label. + const labelParentNode = textareaLabel.parentNode; + labelParentNode.style.paddingBottom = heightEditor; + textareaLabel.style.verticalAlign = 'bottom'; + } + + textareaElement.insertAdjacentElement('beforebegin', editorElement); + // Set the editor's content for the first time. + this.getEditorContent().innerHTML = this.getContent(); + + // Save the history from the beginning. + this.saveHistory(); + + // Wait until the editor element is added to the DOM before calculating + // its size to ensure it aligns with the others elements. + requestAnimationFrame(() => { + textareaLabel.style.lineHeight = contentEditor.style.lineHeight; + const heightWrapper = height + 1 + parseInt(toolbarEl.offsetHeight); + editorElement.style.height = heightWrapper + 'px'; + editorElement.style.minHeight = heightWrapper + 'px'; + editorElement.style.maxHeight = heightWrapper + 'px'; + }); + + document.addEventListener('click', (e) => { + if (!editorElement.contains(e.target)) { + // Get clean data. + const cleanData = this.getCleanHTML(); + // Set it in both the hidden text area and the editor content. + this.getTextArea().value = cleanData; + this.getEditorContent().innerHTML = cleanData; + this.setActiveButton(false); + } + }); + } + + /** + * Make a content area for the editor. + * + * @return {HTMLElement} The content area element. + */ + initEditorContent = () => { + const {classes, custom} = this.settings; + const contentElement = this.createElement('div', { + 'class': (classes.content + ' ' + (custom.content ?? '')).trim(), + contenteditable: true, + autocapitalize: 'none', + autocorrect: 'off', + role: 'textbox', + spellcheck: false, + 'aria-live': 'off', + id: `${this.settings.element.replace(/:/g, ":")}editable`, + }); + + contentElement.addEventListener('blur', () => { + this.saveHistory(); + }); + + // Listen for the selection change event. + document.addEventListener('selectionchange', () => this.handleSelectionChange()); + + // Set up hotkeys for the editor and prevent the Enter key from making the text content a single line. + contentElement.addEventListener('keydown', (event) => { + // Selection range. + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + const keyMap = { + key: { + 'ArrowUp': 'sup', + '94': 'sup', + 'ArrowDown': 'sub', + '95': 'sub', + + }, + shiftKey: { + '^': 'sup', + '_': 'sub', + } + }; + if (keyMap.key[event.key] || (event.shiftKey && keyMap.shiftKey[event.key])) { + event.preventDefault(); + this.handleSupSubHotKey(keyMap.key[event.key] || keyMap.shiftKey[event.key]); + } + + if (event.ctrlKey) { + this.saveHistory(); + } + + if (event.key === 'Enter') { + event.preventDefault(); + } + + // Handle undo/redo action. + if (event.ctrlKey && event.key === 'z') { + event.preventDefault(); + this.handleUndo(); + } + + if (event.ctrlKey && event.key === 'y') { + event.preventDefault(); + this.handleRedo(); + } + + // In case the editor is empty we need to reset format + // to prevent it remember the previous format. + if (this.cleanHTML(event.target.innerHTML) === '' && + !this.isSelectionInsideSubSup()) { + const emptyText = document.createTextNode('\uFEFF'); + range.insertNode(emptyText); + } + + this.getTextArea().value = this.getCleanHTML(); + }); + + contentElement.addEventListener('paste', event => { + this.handlePaste(event); + }); + + const wrapContent = this.createElement('div', { + 'class': (classes.contentWrap + ' ' + (custom.contentWrap ?? '')).trim(), + }); + + wrapContent.appendChild(contentElement); + + return wrapContent; + }; + + /** + * Handle event paste. + * + * @param {Event} event Event object. + */ + handlePaste(event) { + event.preventDefault(); + const types = event.clipboardData.types; + let isHTML = false; + + // Check for different methods to determine if 'text/html' is present + if (types?.contains) { + isHTML = types.contains('text/html'); + } else if (types?.includes) { + isHTML = types.includes('text/html'); + } + + let content; + if (isHTML) { + content = this.cleanPasteHTML(event.clipboardData.getData('text/html')); + } else { + content = event.clipboardData.getData('text'); + } + + // We need to clean the data before inserting it into the editor. + const cleanData = content.replaceAll(/[\r\n]+/g, ''); + document.execCommand('insertHTML', false, cleanData); + this.saveHistory(); + this.getTextArea().value = this.getCleanHTML(); + } + + /** + * Handle event undo. + */ + handleUndo() { + if (this.historyIndex > 0) { + this.historyIndex--; + this.getEditorContent().innerHTML = this.history[this.historyIndex]; + this.getTextArea().value = this.history[this.historyIndex]; + } + } + + /** + * Handle event redo. + */ + handleRedo() { + if (this.historyIndex < this.history.length - 1) { + this.historyIndex++; + this.getEditorContent().innerHTML = this.history[this.historyIndex]; + this.getTextArea().value = this.history[this.historyIndex]; + } + } + + /** + * Handle event sup/sub. + * + * @param {String} action The sup/sub action. + */ + handleSupSubHotKey(action) { + const nodeEl = this.isSelectionInsideSubSup(); + if (nodeEl) { + const nodeName = nodeEl.nodeName.toLowerCase(); + if (nodeName !== action) { + this.setFormat(this.getActions(nodeName)[0]); + } + return; + } + if (this.isSupportSupSub(action)) { + this.setFormat(this.getActions(action)[0]); + } + } + + /** + * Based on the user's selection change, we will detect the pointer position to determine whether + * the cursor is inside the sup/sub tag. Depending on this result, we will activate the corresponding button. + */ + handleSelectionChange() { + const selection = window.getSelection(); + + // When the user makes a selection change inside the editor. + if (this.getEditorContent().contains(selection.anchorNode)) { + // Detect whether the pointer is inside the sup/sub tag. + const node = this.isSelectionInsideSubSup(); + + if (node) { + // Activate the corresponding button in the toolbar. + this.setActiveButton(node.nodeName.toLowerCase()); + } else { + // Deactivate all the buttons. + this.setActiveButton(false); + } + } + } + + /** + * Utility function to create a element with attributes. + * + * @param {String} tag - HTML tag name. + * @param {Object} attributes - The attributes of the element, such as class, id, etc. + * @return {HTMLElement} The element that was created. + */ + createElement(tag, attributes = {}) { + const element = document.createElement(tag); + for (let attribute in attributes) { + element.setAttribute(attribute, attributes[attribute]); + } + + return element; + } + + /** + * Utility function to check whether the current selection is inside the sup/sub tag. Returns false if it's not. + * + * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false. + */ + isSelectionInsideSubSup() { + const selection = window.getSelection(); + if (selection.rangeCount === 0) { + return false; + } + const range = selection.getRangeAt(0); + const tagName = range.commonAncestorContainer.parentNode.nodeName; + // If user doesn't select any text. + if (selection.isCollapsed) { + if (this.isSupSubTag(tagName)) { + return range.commonAncestorContainer.parentNode; + } + return false; + } + let nodeNames; + const selectionNodes = range.cloneContents().childNodes; + for (let node of selectionNodes) { + const nodeName = node.nodeName; + if (node.textContent === '') { + continue; + } + if (!(this.isSupSubTag(nodeName)) && + (nodeName === '#text' && !this.isSupSubTag(tagName))) { + return false; + } + if (!nodeNames) { + nodeNames = node; + } + if (!nodeNames.isEqualNode(node)) { + return false; + } + } + + if (nodeNames.nodeName === '#text' || this.isSupSubTag(tagName)) { + return range.commonAncestorContainer.parentNode; + } + + return nodeNames; + } + + /** + * Check if the given tag name is 'sup' or 'sub'. Return true if it is. + * + * @param {String} tagName - Tag name need to check. + * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false. + */ + isSupSubTag(tagName) { + return ['SUB', 'SUP'].includes(tagName); + } + + /** + * Utility function to highlight the sup/sub button. + * + * @param {String|Boolean} type - The type of the button: sup or sub. + */ + setActiveButton(type) { + const {toolbar, button} = this.settings.classes; + // Deactivate all the existing buttons. + this.getEditor().querySelectorAll(`.${toolbar} .${button}`) + .forEach(button => button.classList.remove('highlight')); + if (type !== false) { + this.getSupSubButton(type)?.classList?.add('highlight'); + } + } + + /** + * Utility function to create a toolbar element that contains sup and sub buttons. + * + * @return {HTMLElement} The toolbar element. + */ + initEditorToolbar() { + const toolbarGroup = this.createElement('div', { + 'class': (this.settings.classes.toolbarGroup + ' ' + (this.settings?.custom?.toolbarGroup ?? '')).trim(), + }); + this.getActions(this.settings.type).forEach((action) => { + const button = this.createElement('button', { + 'class': this.settings?.classes.button + ' ' + action.class, + title: this.settings.buttons[action.name].title, + type: 'button', + 'data-action': action.name, + }); + + button.innerHTML = this.settings.buttons[action.name].icon; + button.setAttribute('type', 'button'); + button.onclick = () => { + const selection = window.getSelection(); + const nodeEl = this.isSelectionInsideSubSup(); + if (selection.isCollapsed && nodeEl !== false) { + if (nodeEl.nodeName.toLowerCase() !== action.tag) { + button.blur(); + this.getEditorContent().focus(); + return; + } + } + + this.getEditorContent().focus(); + this.setFormat(action); + }; + + toolbarGroup.appendChild(button); + }); + const toolbarEl = this.createElement('div', { + 'class': (this.settings.classes.toolbar + ' ' + (this.settings?.custom?.toolbar ?? '')).trim(), + }); + toolbarEl.appendChild(toolbarGroup); + + return toolbarEl; + } + + /** + * Based on the action (sup/sub), this function will format the selected text accordingly. + * + * @param {Object} action - The sup/sub action object. + */ + setFormat(action) { + // Selection text. + const selection = window.getSelection(); + // Selection range. + const range = selection.getRangeAt(0); + const {tag} = action; + const nodeEl = this.isSelectionInsideSubSup(); + // In case the user doesn't select any text. + if (selection.isCollapsed) { + // We need to check whether the current position of the pointer is inside a sub or sup tag. + const parentNode = range.commonAncestorContainer.parentNode; + if (parentNode.nodeName.toLowerCase() === tag) { + // In this case, the pointer is inside a sub or sup tag, so we need to select all the text within the tag. + // Then, we will slice it into two parts, using the current position of the cursor as the border. + // The first part will extend from position 0 to the border, and the second part will span from + // the border to the end. + // After that, we will wrap each part in the corresponding sub or sup tag. The result will be: + // First and Second. + // Finally, we will create a text node with empty content (\uFEFF) and place it at the border + // of the two parts, resulting in: + // First#textnode#Second. + // Create the first part. + const beforeText = this.createElement(tag); + beforeText.innerText = parentNode.textContent.slice(0, range.startOffset); + // Make an empty textnode. + const emptyText = document.createTextNode('\uFEFF'); + // Create an empty text node. + const afterText = this.createElement(tag); + afterText.innerText = parentNode.textContent.slice(range.startOffset); + // Insert it into the DOM next to the parent node. + if (afterText.innerHTML !== '') { + parentNode.parentNode.insertBefore(afterText, parentNode.nextSibling); + } + parentNode.parentNode.insertBefore(emptyText, parentNode.nextSibling); + if (beforeText.innerHTML !== '') { + parentNode.parentNode.insertBefore(beforeText, parentNode.nextSibling); + } + + // Remove the parent node. + parentNode.remove(); + // We set the position of the cursor to be in the empty text node. + range.setStart(emptyText, 1); + range.setEnd(emptyText, 1); + selection.removeAllRanges(); + selection.addRange(range); + } else { + // In case the user didn't select anything, we must create a sup/sub + // tag with an empty string and move the cursor into it. + const node = this.createElement(tag); + // Zero-width space to keep the tag visible. + node.appendChild(document.createTextNode('\uFEFF')); + // Update the new range within the existing one. + range.insertNode(node); + // Set the selection index at the next available space. + range.setStart(node.firstChild, 1); + range.setEnd(node.firstChild, 1); + // Remove all existing ranges from the selection. + selection.removeAllRanges(); + // Add the updated range object to the current selection. + selection.addRange(range); + } + } else if (nodeEl) { + // This means the user is selecting some text that is inside the sub or sup tag. + // In this case, we only need to move the selected text inside the sub/sup tag outside of it. + // For example: 123[456]789. If the selected text is 456, we will + // move it outside the tag, resulting in 123456789. + // Retrieve the selected text. + // Retrieve the current tag (sub/sup) that wraps the selection + const selectedText = range.toString(); + const parentElement = nodeEl; + const nextSibling = parentElement.nextSibling; + const beforeText = parentElement.textContent.slice(0, range.startOffset); + const afterText = parentElement.textContent.slice(range.endOffset); + if (beforeText) { + const start = this.createElement(parentElement.nodeName.toLowerCase()); + start.textContent = beforeText; + parentElement.parentNode.insertBefore(start, nextSibling); + } + // Create a text node based on the selected text. + const textNode = document.createTextNode(selectedText); + parentElement.parentNode.insertBefore(textNode, nextSibling); + if (afterText) { + const end = this.createElement(parentElement.nodeName.toLowerCase()); + end.textContent = afterText; + parentElement.parentNode.insertBefore(end, nextSibling); + } + + parentElement.remove(); + + // Create a new range to select the inserted content + range.setStart(textNode, 0); + range.setEnd(textNode, selectedText.length); + selection.removeAllRanges(); + selection.addRange(range); + this.getTextArea().value = this.getCleanHTML(); + } else { + // This case is user select a text that is not inside subsup. + // We retrieve the selected text and then delete it in DOM. + const selectedText = range.toString(); + range.deleteContents(); + const previousNode = range.commonAncestorContainer.previousSibling; + const nextNode = range.commonAncestorContainer.nextSibling; + // In addition, we will merge adjacent sup/sub tags into a single sup/sub tag. + if (previousNode || nextNode) { + const newNode = this.createElement(tag); + let startOffset = 0; + let endOffset = 0; + let content = ''; + if (previousNode && previousNode?.nodeName?.toLowerCase() === tag) { + content = previousNode.textContent; + startOffset = content.length; + previousNode.remove(); + } + content += selectedText; + endOffset = content.length; + if (nextNode && nextNode?.nodeName?.toLowerCase() === tag) { + content += nextNode.textContent; + nextNode.remove(); + } + newNode.textContent = content; + if (content !== selectedText) { + range.insertNode(newNode); + range.setStart(newNode.firstChild, startOffset); + range.setEnd(newNode.firstChild, endOffset); + this.getTextArea().value = this.getCleanHTML(); + return; + } + } + + // Create a sup/sub tag that wrap the selected text. + const newNode = document.createElement(tag); + newNode.appendChild(document.createTextNode(selectedText)); + // Make a selection to the selected text. + selection.removeAllRanges(); + // Insert it into DOM. + range.insertNode(newNode); + range.selectNodeContents(newNode.firstChild); + selection.addRange(range); + // Clean up all the empty text. + this.getEditorContent().childNodes.forEach(el => { + if (el.nodeName === '#text' && el.textContent === '') { + el.remove(); + } + }); + this.getTextArea().value = this.getCleanHTML(); + } + // Clean up. + this.getEditorContent().childNodes.forEach(el => { + if (el.nodeName === '#text' && el.textContent === '') { + el.remove(); + } + }); + this.saveHistory(); + } + + /** + * Save history for undo/redo actions. + */ + saveHistory() { + const content = this.getCleanHTML(); + if (this.historyIndex === -1 || content !== this.history[this.historyIndex]) { + this.history.splice(this.historyIndex + 1); + this.history.push(content); + this.historyIndex++; + } + } + + /** + * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip. + * + * @param {String} content - The content data need to be clean. + * @return {String} The clean text. + */ + cleanPasteHTML(content) { + // Return an empty string if passed an invalid or empty object. + if (!content || content.length === 0) { + return ""; + } + + // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc.). + let rules = [ + {regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""}, + {regex: //gi, replace: ""}, + {regex: //gi, replace: ""}, + {regex: /]*>[\s\S]*?<\/xml>/gi, replace: ""}, + {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""}, + {regex: /<\/?\w+:[^>]*>/gi, replace: ""} + ]; + + // Apply the first set of harsher rules. + content = this.filterContentWithRules(content, rules); + + // Apply the standard rules, which mainly cleans things like headers, links, and style blocks. + content = this.cleanHTML(content); + + // Check if the string is empty or only contains whitespace. + if (content.length === 0 || !/\S/.test(content)) { + return content; + } + + // Normalize the code by loading it into the DOM. + const holder = document.createElement('div'); + holder.innerHTML = content; + content = holder.innerHTML; + + // Free up the DOM memory. + holder.innerHTML = ""; + + // Run some more rules that care about quotes and whitespace. + rules = [ + {regex: /(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi, replace: "$1"}, + {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9-]*)+/gi, replace: "$1"}, + {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9-]*)+/gi, replace: "$1"}, + {regex: /
]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""}, + ]; + + // Apply the rules. + content = this.filterContentWithRules(content, rules); + + // Reapply the standard cleaner to the content. + return this.cleanHTML(content); + } + + /** + * Check if the editor allows the use of sub or sup features. + * + * @param {String} action - Sub/sup action to check. + * @return {Boolean} The result after verifying whether it is allowed. + */ + isSupportSupSub(action) { + const {type} = this.settings; + return type === 'both' || type === action; + } + + /** + * Utility function to filter the content based on the given rules. + * + * @param {String} content - The content need to be filtered. + * @param {Object} rules - The rules list. + * @return {String} The cleaned content will be returned. + */ + filterContentWithRules(content, rules) { + for (const element of rules) { + content = content.replace(element.regex, element.replace); + } + return content; + } + + /** + * Utility function to clean the HTML. + * + * @param {String} content - The content need to be filter. + * @return {String} The cleaned content will be returned. + */ + cleanHTML(content) { + // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc. + + const rules = [ + // Remove empty paragraphs. + {regex: /]*>( |\s)*<\/p>/gi, replace: ""}, + + // Remove attributes on sup and sub tags. + {regex: /]*( |\s)*>/gi, replace: ""}, + {regex: /]*( |\s)*>/gi, replace: ""}, + + // Replace   with space. + {regex: / /gi, replace: " "}, + + // Combine matching tags with spaces in between. + {regex: /<\/sup>(\s*)+/gi, replace: "$1"}, + {regex: /<\/sub>(\s*)+/gi, replace: "$1"}, + + // Move spaces after start sup and sub tags to before. + {regex: /(\s*)+/gi, replace: "$1"}, + {regex: /(\s*)+/gi, replace: "$1"}, + + // Move spaces before end sup and sub tags to after. + {regex: /(\s*)+<\/sup>/gi, replace: "$1"}, + {regex: /(\s*)+<\/sub>/gi, replace: "$1"}, + + // Remove empty br tags. + {regex: /
/gi, replace: ""}, + + // Remove any style blocks. Some browsers do not work well with them in a contenteditable. + // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015. + // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work" + {regex: /]*>[\s\S]*?<\/style>/gi, replace: ""}, + + // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout. + {regex: /)/gi, replace: ""}, + + // Remove elements that can not contain visible text. + {regex: /]*>[\s\S]*?<\/script>/gi, replace: ""}, + + // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html" + // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link. + {regex: /<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi, replace: ""}, + // Source:"https://developer.mozilla.org/en/docs/Web/HTML/Element" + // Remove all elements except sup and sub. + {regex: /<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:var|wbr|video)[^>]*?>/gi, replace: ""}, + + // Deprecated elements that might still be used by older sites. + {regex: /<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi, replace: ""}, + {regex: /<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi, replace: ""}, + + // Elements from common sites including google.com. + {regex: /<\/?(?:jsl|nobr)[^>]*?>/gi, replace: ""}, + + {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi, replace: "$1"}, + + // Remove empty spans, but not ones from Rangy. + {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi, replace: ""}, + {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi, replace: "$1"}, + + // Remove empty sup and sub tags that appear after pasting text. + {regex: /]*>( |\s)*<\/sup>/gi, replace: ""}, + {regex: /]*>( |\s)*<\/sub>/gi, replace: ""}, + + // Remove special xml namespace tag xmlns generate by browser plugin. + {regex: /(.*?)<\/xmlns.*?>/gi, replace: "$1"}, + {regex: /\uFEFF/gi, replace: ""} + ]; + + return this.filterContentWithRules(content, rules); + } + + /** + * Clean the generated HTML content without modifying the editor content. + * + * This includes removing all YUI IDs from the generated content. + * + * @return {string} The cleaned HTML content. + */ + getCleanHTML() { + // Clone the editor so that we don't actually modify the real content. + const editorClone = this.getEditorContent().cloneNode(true); + let html; + + html = editorClone.innerHTML; + + // Define contents that are considered empty. + const emptyContents = [ + '

', + '


', + '
', + '

', + '


', + '

', + '


', + '

 

', + '


 

', + '

 

', + '


 

', + '

 

', + '


 

' + ]; + + if (emptyContents.includes(html)) { + return ''; + } + + // Clean the HTML content. + return this.cleanHTML(html); + } + + + /** + * Utility function to get the content element of the editor. + * + * @return {HTMLElement} The editor content element. + */ + getEditorContent() { + return this.getEditor().querySelector(`.${this.settings.classes.content}`); + } + + /** + * Utility function to get the editor element. This element will contain all the components of the editor. + * + * @return {HTMLElement} The editor element. + */ + getEditor() { + return document.getElementById(`${this.settings.classes.editor}-${this.settings.element}`); + } + + /** + * Utility function to retrieve the button element based on the given type. + * + * @param {String} type - The type of the button: sup or sub. + * @return {HTMLElement} The corresponding button. + */ + getSupSubButton(type) { + const {toolbar, button} = this.settings.classes; + return this.getEditor().querySelector(`.${toolbar} .${button}[data-action^="${type}"]`); + } + + /** + * Utility function to get button settings (sup/sub) based on the given type. + * + * @param {String} type - The type of the button can be either sup or sub. + * @return {Object} The settings for the given button. + */ + getActions(type) { + if (defaultActions[type]) { + return [defaultActions[type]]; + } + + return Object.values(defaultActions); + } + + /** + * Utility to get the button container for the editor. + * + * @return {HTMLElement} The button container. + */ + getButtonContainer() { + return this.getEditor().querySelectorAll(`.${this.settings.classes.toolbar} .${this.settings.classes.button}`); + } + + /** + * Utility function to get the content of the original textarea. + * + * @return {String} The content. + */ + getContent() { + return this.getTextArea().value; + } + + /** + * Utility function to get id of the element. + * + * @return {String} The element id. + */ + getEditorId() { + return this.settings.element; + } + + /** + * Return the text area element. + * + * @return {HTMLElement} Text area element. + */ + getTextArea() { + return document.getElementById(this.settings.element); + } + +} + +/** + * Load editor based on the given setting. + * + * @param {Object} settings - The editor setting. + */ +export const loadEditor = settings => { + const editor = new OUSupSubEditor(settings); + // We need to do this for a specific reason, currently only for the Behat test. + // We can easily utilize the editor's API. + if (!window.OUSupSubEditor) { + window.OUSupSubEditor = { + instances: { + [settings.element]: editor, + }, + addEditor: function(editor) { + this.instances[editor.getEditorId()] = editor; + }, + getEditorById: function(editorId) { + return this.instances[editorId]; + }, + }; + } else { + window.OUSupSubEditor.addEditor(editor); + } +}; diff --git a/buildstandalone.php b/buildstandalone.php deleted file mode 100644 index 24bd58d..0000000 --- a/buildstandalone.php +++ /dev/null @@ -1,276 +0,0 @@ -. - -/** - * Builds a standalone demonstration version of the ousupsub editor - * - * This script is designed to run from the command line and is safe to re-run - * at any time when the plugin is updated. - * - * @package editor_ousupsub - * @copyright 2015 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define('CLI_SCRIPT', true); -define('CACHE_DISABLE_ALL', true); - -require_once(__DIR__ . '/../../../config.php'); -require_once($CFG->libdir . '/filelib.php'); - -error_reporting(E_ALL | E_STRICT); -error_reporting(-1); -ini_set('display_errors', true); -ini_set('display_startup_errors', true); - -ousupsub_texteditor_standalone_builder::create_standalone(); - -/** - * Creates demonstration editor. - */ -class ousupsub_texteditor_standalone_builder { - private static $paths = array( - 'root' => 'standalone', - 'index' => 'index.html', - 'ousupsubjs' => 'ousupsub.js', - 'stylecss' => 'styles.css', - 'readme' => 'readme.txt', - 'readmestandalone' => 'standalone-src/readme.txt', - 'yuiversion' => '3.18.1', - 'wwwroot' => '../../..' - ); - private static $yuisuffix = '-min'; - - public static function create_standalone () { - self::delete_standalone(); - self::create_standalone_folder(); - self::create_readme_file(); - self::create_index_page(); - self::copy_icons(); - self::create_css_file(); - self::create_javascript_file(); - } - - public static function delete_standalone () { - $path = self::create_path('root'); - if (fulldelete($path)) { - self::echo_result("Emptied standalone folder."); - } - } - - /** - * Create the root folder. - */ - public static function create_standalone_folder() { - $path = self::create_path('root/ousupsub'); - self::create_folder($path); - } - - /** - * Create the language string. - */ - public static function create_language_string() { - $components = array( - 'moodle' => array('error', 'morehelp'), - 'editor_ousupsub' => array('editor_command_keycode', 'editor_control_keycode', - 'editor_shift_keycode', 'plugin_title_shortcut', - 'subscript', 'superscript', 'undo', 'redo'), - ); - - $output = array(); - foreach ($components as $component => $keys) { - $output[$component] = array(); - foreach ($keys as $key) { - $output[$component][$key] = get_string($key, $component); - } - } - self::echo_result("Create language strings."); - return json_encode($output); - } - - /** - * Create readme file. - */ - public static function create_readme_file() { - - // Create the readme file. - $pathfrom = self::create_path('readmestandalone'); - $contents = file_get_contents($pathfrom); - - // Path to save file to. - $pathto = self::create_path('root/readme'); - if (file_put_contents($pathto, $contents, 0)) { - self::echo_result("Created readme.txt."); - } - } - - /** - * Create the index page. - */ - public static function create_index_page() { - $replacements = array( - '%%jsurl%%' => self::create_path('ousupsub/ousupsubjs'), - '%%stylesurl%%' => self::create_path('ousupsub/stylecss'), - ); - - $html = file_get_contents(self::create_path('standalone-src/index.html')); - $html = str_replace(array_keys($replacements), array_values($replacements), $html); - - $path = self::create_path('root/index'); - if (file_put_contents($path, $html, 0)) { - self::echo_result("Create index file."); - } - } - - /** - * Copy button icons. - */ - public static function copy_icons() { - global $CFG; - $names = array('subscript', 'superscript'); - $preferredlocation = $CFG->dirroot . '/theme/ou/pix/editor/'; - $fallbacklocation = $CFG->dirroot . '/pix/e/'; - - // OU sup sub icons. - foreach ($names as $name) { - $source = $preferredlocation . $name . '.svg'; - if (!is_readable($source)) { - $source = $fallbacklocation . $name . '.svg'; - } - $destination = self::create_path('root/ousupsub/'.$name.'.svg'); - if (copy($source, $destination)) { - self::echo_result("Copy ousupsub ".$name." icon."); - } - } - } - - /** - * Create CSS file. - */ - public static function create_css_file() { - - // Create the static file. The unconventional indenting is required to produce conventional - // indenting in the file produced. - $contents = ' -body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 20px; - color: #333; - background-color: #fff; -} -'; - $contents .= file_get_contents(self::create_path('stylecss')); - - // Path to save file to. - if (file_put_contents(self::create_path('root/ousupsub/stylecss'), $contents, 0)) { - self::echo_result("Created styles.css."); - } - } - - /** - * Copy the javascript files required by the editor. - */ - public static function create_javascript_file() { - $replacements = array( - '%%yuilibraries%%' => self::create_yui_javascript(), - '%%ousupsubcode%%' => self::create_supsub_javascript(), - '%%langstrings%%' => self::create_language_string(), - ); - - $js = file_get_contents(self::create_path('standalone-src/standalone.js')); - $js = str_replace(array_keys($replacements), array_values($replacements), $js); - - if (file_put_contents(self::create_path('root/ousupsub/ousupsubjs'), $js, 0)) { - self::echo_result("Created editor javascript file."); - } - } - - /** - * Get the javascript that makes up the editor. - */ - public static function create_supsub_javascript() { - $supsubjs = ''; - $editorcodepath = 'yui/build/moodle-editor_ousupsub-%%PART%%/moodle-editor_ousupsub-%%PART%%' . - self::$yuisuffix . '.js'; - $names = array('rangy', 'editor'); - foreach ($names as $name) { - $supsubjs .= file_get_contents(str_replace('%%PART%%', $name, $editorcodepath)); - } - return $supsubjs; - } - - /** - * Copy YUI js files. - */ - public static function create_yui_javascript() { - $yuijs = ''; - - $source = self::create_path('wwwroot/lib/yuilib/yuiversion'); - $names = array('yui', 'attribute-base', 'attribute-core', 'attribute-extras', - 'attribute-observable', 'base-base', 'base-build', 'base-core', - 'base-observable', 'base-pluginhost', 'dom-base', - 'dom-core', 'dom-screen', 'dom-style', 'event-base', 'event-custom-base', - 'event-custom-complex', 'event-delegate', 'event-flick', 'event-focus', 'event-hover', 'event-key', - 'event-mousewheel', 'event-mouseenter', 'event-move', 'event-outside', - 'event-resize', 'event-synthetic', 'event-tap', 'event-touch', - 'event-valuechange', 'node-base', 'node-core', - 'node-event-delegate', 'node-pluginhost', 'node-screen', 'node-style', 'oop', - 'pluginhost-base', 'pluginhost-config', - 'selector', 'selector-native'); - foreach ($names as $name) { - $newjs = file_get_contents($source . '/' . $name . '/' . $name . self::$yuisuffix . '.js'); - // We don't acutally load anything from this URL, but the presence of the - // non-https URL causes a suprious error in IE8, so we change it. - // Note, the https version of this URL does not actually work. - $yuijs .= str_replace('http://yui.yahooapis.com/', 'https://yui.yahooapis.com/', $newjs); - } - - return $yuijs; - } - - /** - * Create a folder on the file system give a path. - */ - public static function create_folder($path) { - global $CFG; - if (!file_exists($path)) { - mkdir($path, $CFG->directorypermissions, true); - } - - return true; - } - - /** - * Create a php folder path given keys from the $paths array. - */ - public static function create_path($ids) { - $keys = explode('/', $ids); - $path = ''; - foreach ($keys as $key) { - $path .= strlen($path) ? '/' : ''; - $path .= array_key_exists($key, self::$paths) ? self::$paths[$key] : $key; - } - return $path; - } - - /** - * Create a folder on the file system give a path. - */ - public static function echo_result($msg) { - echo $msg."\r\n"; - } -} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index f0ef75d..e066beb 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + /** * Privacy Subsystem implementation for editor_ousupsub. * @@ -23,8 +24,6 @@ namespace editor_ousupsub\privacy; -defined('MOODLE_INTERNAL') || die(); - /** * Privacy Subsystem for editor_ousupsub implementing null_provider. @@ -39,7 +38,7 @@ class provider implements \core_privacy\local\metadata\null_provider { * * @return string */ - public static function get_reason() : string { + public static function get_reason(): string { return 'privacy:metadata'; } } diff --git a/db/upgrade.php b/db/upgrade.php index 0f02961..197c275 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -22,8 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - /** * Run all OU sup-sub editor upgrade steps between the current DB version and * the current version on disk. diff --git a/lang/en/editor_ousupsub.php b/lang/en/editor_ousupsub.php index 35857ff..3332fd8 100644 --- a/lang/en/editor_ousupsub.php +++ b/lang/en/editor_ousupsub.php @@ -22,16 +22,19 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['button_sub_title'] = 'Subscript [Shift + _ or Down arrow]'; +$string['button_sup_title'] = 'Superscript [Shift + ^ or Up arrow]'; $string['editor_command_keycode'] = 'Cmd + {$a}'; $string['editor_control_keycode'] = 'Ctrl + {$a}'; $string['editor_shift_keycode'] = 'Shift + {$a}'; $string['plugin_title_shortcut'] = '{$a->title} [{$a->shortcut}]'; $string['pluginname'] = 'Superscript/subscript editor'; $string['privacy:metadata'] = 'The Superscript/subscript editor plugin does not store any personal data.'; +$string['redo'] = 'Redo'; $string['settings'] = 'Superscript/subscript editor settings'; $string['subscript'] = 'Subscript'; $string['superscript'] = 'Superscript'; +$string['undo'] = 'Undo'; $string['useeditor'] = 'Use this editor'; $string['useeditor_desc'] = 'Use this editor in question types, in preference to the old superscript/subscript editor, if both are installed.'; -$string['redo'] = 'Redo'; -$string['undo'] = 'Undo'; + diff --git a/lib.php b/lib.php index a907ed6..0f93803 100644 --- a/lib.php +++ b/lib.php @@ -15,15 +15,13 @@ // along with Moodle. If not, see . /** - * YUI text editor integration. + * New OU text editor integration. * * @package editor_ousupsub * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - /** * This is the texteditor implementation. @@ -48,7 +46,7 @@ public function supported_by_browser() { */ public function get_supported_formats() { // FORMAT_MOODLE is not supported here, sorry. - return array(FORMAT_HTML => FORMAT_HTML); + return [FORMAT_HTML => FORMAT_HTML]; } /** @@ -74,8 +72,8 @@ public function supports_repositories() { * @param array $options * @param null $fpoptions */ - public function use_editor($elementid, array $options = null, $fpoptions = null) { - global $PAGE; + public function use_editor($elementid, ?array $options = null, $fpoptions = null) { + global $PAGE, $OUTPUT; if (empty($options['context'])) { $options['context'] = context_system::instance(); @@ -84,81 +82,26 @@ public function use_editor($elementid, array $options = null, $fpoptions = null) $options['supsub'] = 'both'; } - switch ($options['supsub']) { - case 'both': - $groups = array('style1' => array('superscript', 'subscript')); - break; - - case 'sup': - $groups = array('style1' => array('superscript')); - break; - - case 'sub': - $groups = array('style1' => array('subscript')); - break; - - default: - throw new coding_exception("Invalid value '" .$options['supsub'] . - "' for option 'supsub'. Must be one of 'both', 'sup' or 'sub'."); - } - - $groupplugins = array(); - foreach ($groups['style1'] as $plugin) { - $groupplugins[] = array('name' => $plugin, 'params' => array()); - } - $jsplugins = array(array('group' => 'style1', 'plugins' => $groupplugins)); - - $PAGE->requires->strings_for_js(array( - 'editor_command_keycode', - 'editor_control_keycode', - 'editor_shift_keycode', - 'plugin_title_shortcut', - 'subscript', - 'superscript', - 'redo', - 'undo' - ), 'editor_ousupsub'); - $PAGE->requires->strings_for_js(array( - 'warning', - 'info' - ), 'moodle'); - - $PAGE->requires->yui_module(array('moodle-editor_ousupsub-editor'), - 'Y.M.editor_ousupsub.createEditor', - array($this->get_init_params($elementid, $options, $fpoptions, $jsplugins))); - - } - - /** - * Create a params array to init the editor. - * - * @param string $elementid - * @param array $options - * @param array $fpoptions - */ - protected function get_init_params($elementid, array $options = null, array $fpoptions = null, $plugins = null) { - global $PAGE; - - $directionality = get_string('thisdirection', 'langconfig'); - $strtime = get_string('strftimetime'); - $strdate = get_string('strftimedaydate'); - $lang = current_language(); - $contentcss = $PAGE->theme->editor_css_url()->out(false); - - $params = array( - 'elementid' => $elementid, - 'content_css' => $contentcss, - 'contextid' => $options['context']->id, - 'language' => $lang, - 'directionality' => $directionality, - 'filepickeroptions' => array(), - 'plugins' => $plugins, - 'pageHash' => sha1($PAGE->url) - ); - if ($fpoptions) { - $params['filepickeroptions'] = $fpoptions; + if (!in_array($options['supsub'], ['sup', 'sub', 'both'])) { + throw new coding_exception("Invalid value '" .$options['supsub'] . + "' for option 'supsub'. Must be one of 'both', 'sup' or 'sub'."); } - return $params; + $PAGE->requires->js_call_amd('editor_ousupsub/editor', 'loadEditor', [ + [ + 'element' => $elementid, + 'type' => $options['supsub'], + 'buttons' => [ + 'superscript' => [ + 'icon' => $OUTPUT->pix_icon('e/superscript', '', 'core'), + 'title' => get_string('button_sup_title', 'editor_ousupsub'), + ], + 'subscript' => [ + 'icon' => $OUTPUT->pix_icon('e/subscript', '', 'core'), + 'title' => get_string('button_sub_title', 'editor_ousupsub'), + ], + ], + ], + ]); } } diff --git a/standalone/ousupsub/subscript.svg b/pix/subscript.svg similarity index 98% rename from standalone/ousupsub/subscript.svg rename to pix/subscript.svg index db18652..96f8ddd 100644 --- a/standalone/ousupsub/subscript.svg +++ b/pix/subscript.svg @@ -1,21 +1,21 @@ - - - -]> - - - - - + + + +]> + + + + + diff --git a/standalone/ousupsub/superscript.svg b/pix/superscript.svg similarity index 98% rename from standalone/ousupsub/superscript.svg rename to pix/superscript.svg index 2ebde58..c60586b 100644 --- a/standalone/ousupsub/superscript.svg +++ b/pix/superscript.svg @@ -1,21 +1,21 @@ - - - -]> - - - - - + + + +]> + + + + + diff --git a/readme.md b/readme.md index 0487a52..b18bb69 100644 --- a/readme.md +++ b/readme.md @@ -53,28 +53,9 @@ However, this only works if the surrounding text is styled not to extremely. We * https://moodle.org/plugins/qtype_varnumunit * https://moodle.org/plugins/qtype_combined (Only work if the surrounding text is styled not to extremely) -## Standalone version - -More details are in readme_standalone.txt that gets added to the /standalone folder -A standalone/offline version of the editor is also provided in the /standalone folder. This provides all the -functionality of the editor in a package that works in an ereader or mobile app or on a desktop to demonstrate -the functionality of the editor outside of moodle. There feature is currently in beta. - -The standalone version is kept up to date by running the buildstandalone.php in the same way you run a behat script -The full command we use is php lib/editor/ousupsub/buildstandalone.php - -Running this script outputs a list of files and features that have been created. First it deletes the contents of the -standalone folder and then it recreates the standalone files. This ensures the standalone version is as up to date with -the plugin. This task is performed during development of the editor. If you are using it out of the box you shouldn't -need to run this script. - ## Testing -Automated testing is through behat and custom javascript unit tests. There is a behat test for the moodle plugin and an -identical test for the standalone version - -The javascript unit tests run in a browser. Load /tests/fixtures/testcleanup.html in a specific browser to see if the -tests pass in that browser. +Automated testing is through behat and custom javascript unit tests. The editor will work any where moodle editors work but it's designed to be used with specific OU question types The main places to test are: @@ -98,16 +79,3 @@ Then we check that subscript was applied correctly. Then I should see "Superscript and Subscript" in the "Description" ousupsub editor That is how you read the behat tests and how you know what to expect the editor to do. - -## Third-party code - -Thanks to the creators of the rangy software library, which we use. - -1) Rangy (version 1.2.3) - * Download the latest stable release; - * Copy the content of the 'currentrelease/uncompressed' folder into yui/src/rangy/js - * Run shifter against yui/src/rangy - - Notes: - * We have patched 1.2.3 with a backport fix from the next release of Rangy which addresses an incompatibility - between Rangy and HTML5Shiv which is used in the bootstrapclean theme. See MDL-44798 for further information. diff --git a/standalone-src/index.html b/standalone-src/index.html deleted file mode 100644 index 020f198..0000000 --- a/standalone-src/index.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - OU SupSub demo - - - - - - - - -
- - - - - -
- - - - - diff --git a/standalone-src/readme.txt b/standalone-src/readme.txt deleted file mode 100644 index bb97fa0..0000000 --- a/standalone-src/readme.txt +++ /dev/null @@ -1,26 +0,0 @@ -This folder contains the files for the standalone superscript subscript editor. -index.html contains a demonstration of the editor and the required resources are -in the resources folder - -To view a demonstration of the editor, download the standalone folder to your -desktop and open the index.html file in a browser. - -You will then see a text editor with two buttons. One for superscript and one -for subscript. - -The editor features are summarised as: -* A text editor with sup/sub buttons based on the moodle atto editor. -* Allow only alphanumeric text. No html tags except and -* Provide a superscript or subscript button or both along with related functionality -* Prevent nesting of superscript and subscript tags -* No text wrapping is allowed along with no paragraphs. Everything is on one line -* Configurable height and width of editor -* Provide a standalone version of the same editor for offline situations such as ereaders -* Editor can placed where required including inline with text - -Features delivered -The features that have been delivered are: -* a text editor with sup/sub buttons based on the moodle atto editor. -* a standalone demonstration. -* remove features and trim resources: Atto editor has multiple buttons and - toolbars that aren't needed along with scripts and icons. \ No newline at end of file diff --git a/standalone-src/standalone.js b/standalone-src/standalone.js deleted file mode 100644 index 62a9274..0000000 --- a/standalone-src/standalone.js +++ /dev/null @@ -1,71 +0,0 @@ -// We encapsulate all our JavaScript inside an anonymouse function which just -// returns the init_ousupsub function that we want to be available. -(function () { - -%%yuilibraries%% - -%%ousupsubcode%% - -M = { - str: %%langstrings%%, - iconRootUrl: null, // Set in a minute. - util: { - pending_js: [], - image_url: function (imageName) { - return M.iconRootUrl + imageName.replace("e/", "/") + ".svg"; - }, - get_string: function(identifier, component, a) { - var stringvalue; - - if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) { - stringvalue = '[[' + identifier + ',' + component + ']]'; - if (M.cfg.developerdebug) { - console.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string'); - } - return stringvalue; - } - - stringvalue = M.str[component][identifier]; - - if (typeof a === 'undefined') { - // no placeholder substitution requested - return stringvalue; - } - - if (typeof a === 'number' || typeof a === 'string') { - // replace all occurrences of {$a} with the placeholder value - stringvalue = stringvalue.replace(/\{\$a\}/g, a); - return stringvalue; - } - - if (typeof a === 'object') { - // replace {$a->key} placeholders - for (var key in a) { - if (typeof a[key] != 'number' && typeof a[key] != 'string') { - if (M.cfg.developerdebug) { - console.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string'); - } - continue; - } - var search = '{$a->' + key + '}'; - stringvalue = stringvalue.replace(search, a[key]); - } - return stringvalue; - } - - if (M.cfg.developerdebug) { - console.log('incorrect placeholder type', 'warn', 'M.util.get_string'); - } - return stringvalue; - } - }, -}; - -var thisScriptUrl = document.getElementById('ousupsubloader').getAttribute('src'); -M.iconRootUrl = thisScriptUrl.substring(0, thisScriptUrl.lastIndexOf("/")); - -YUI().use("moodle-editor_ousupsub-editor", function(Y) { - window.editor_ousupsub = Y.M.editor_ousupsub; -}); - -}()); diff --git a/standalone/index.html b/standalone/index.html deleted file mode 100644 index eaa40f1..0000000 --- a/standalone/index.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - OU SupSub demo - - - - - - - - -
- - - - - -
- - - - - diff --git a/standalone/ousupsub/ousupsub.js b/standalone/ousupsub/ousupsub.js deleted file mode 100644 index a0047e9..0000000 --- a/standalone/ousupsub/ousupsub.js +++ /dev/null @@ -1,153 +0,0 @@ -// We encapsulate all our JavaScript inside an anonymouse function which just -// returns the init_ousupsub function that we want to be available. -(function () { - -typeof YUI!="undefined"&&(YUI._YUI=YUI);var YUI=function(){var e=0,t=this,n=arguments,r=n.length,i=function(e,t){return e&&e.hasOwnProperty&&e instanceof t},s=typeof YUI_config!="undefined"&&YUI_config;i(t,YUI)?(t._init(),YUI.GlobalConfig&&t.applyConfig(YUI.GlobalConfig),s&&t.applyConfig(s),r||(t._afterConfig(),t._setup())):t=new YUI;if(r){for(;e-1&&(n="3.5.0"),e={applyConfig:function(e){e=e||u;var t,n,r=this.config,i=r.modules,s=r.groups,o=r.aliases,a=this.Env._loader;for(n in e)e.hasOwnProperty(n)&&(t=e[n],i&&n=="modules"?S(i,t):o&&n=="aliases"?S(o,t):s&&n=="groups"?S(s,t):n=="win"?(r[n]=t&&t.contentWindow||t,r.doc=r[n]?r[n].document:null):n!="_yuid"&&(r[n]=t));a&&a._config(e)},_config:function(e){this.applyConfig(e)},_init:function(){var e,t,r=this,s=YUI.Env,u=r.Env,a;r.version=n;if(!u){r.Env={core:["get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup","loader-yui3"],loaderExtras:["loader-rollup","loader-yui3"],mods:{},versions:{},base:i,cdn:i+n+"/",_idx:0,_used:{},_attached:{},_exported:{},_missed:[],_yidx:0,_uidx:0,_guidp:"y",_loaded:{},_BASE_RE:/(?:\?(?:[^&]*&)*([^&]*))?\b(yui(?:-\w+)?)\/\2(?:-(min|debug))?\.js/,parseBasePath:function(e,t){var n=e.match(t),r,i;return n&&(r=RegExp.leftContext||e.slice(0,e.indexOf(n[0])),i=n[3],n[1]&&(r+="?"+n[1]),r={filter:i,path:r}),r},getBase:s&&s.getBase||function(t){var n=h&&h.getElementsByTagName("script")||[],i=u.cdn,s,o,a,f;for(o=0,a=n.length;o',YUI.Env.cssStampEl=t.firstChild,h.body?h.body.appendChild(YUI.Env.cssStampEl):p.insertBefore(YUI.Env.cssStampEl,p.firstChild)):h&&h.getElementById(o)&&!YUI.Env.cssStampEl&&(YUI.Env.cssStampEl=h.getElementById(o)),r.config.lang=r.config.lang||"en-US",r.config.base=YUI.config.base||YUI.config.defaultBase&&YUI.config.root&&YUI.config.defaultBase+YUI.config.root||r.Env.getBase(r.Env._BASE_RE);if(!e||!"mindebug".indexOf(e))e="min";e=e?"-"+e:e,r.config.loaderPath=YUI.config.loaderPath||"loader/loader"+e+".js"},_afterConfig:function(){var e=this;e.config.hasOwnProperty("global")||(e.config.global=Function("return this")())},_setup:function(){var e,t=this,n=[],r=YUI.Env.mods,i=t.config.extendedCore||[],s=t.config.core||[].concat(YUI.Env.core).concat(i);for(e=0;e-1){s=o.split(r);for(i=s[0]=="YAHOO"?1:0;i-1?n(t,r):t[r];return typeof i=="undefined"?e:i}):e},n.trim=n._isNative(r.trim)&&!u.trim()?function(e){return e&&e.trim?e.trim():e}:function(e){try{return e.replace(c,"")}catch(t){return e}},n.trimLeft=n._isNative(r.trimLeft)&&!u.trimLeft()?function(e){return e.trimLeft()}:function(e){return e.replace(f,"")},n.trimRight=n._isNative(r.trimRight)&&!u.trimRight()?function(e){return e.trimRight()}:function(e){return e.replace(l,"")},n.type=function(e){return s[typeof e]||s[i.call(e)]||(e?"object":"null")};var p=e.Lang,d=Array.prototype,v=Object.prototype.hasOwnProperty;e.Array=m,m.dedupe=p._isNative(Object.create)?function(e){var t=Object.create(null),n=[],r,i,s;for(r=0,s=e.length;ri&&i in t?t[i]:!0);return n},m.indexOf=p._isNative(d.indexOf)?function(e,t,n){return d.indexOf.call(e,t,n)}:function(e,t,n){var r=e.length;n=+n||0,n=(n>0||-1)*Math.floor(Math.abs(n)),n<0&&(n+=r,n<0&&(n=0));for(;n1?Array.prototype.join.call(arguments,y):String(r);if(!(i in t)||n&&t[i]==n)t[i]=e.apply(e,arguments);return t[i]}},e.getLocation=function(){var t=e.config.win;return t&&t.location},e.merge=function(){var e=0,t=arguments.length,n={},r,i;for(;e-1},E.each=function(t,n,r,i){var s;for(s in t)(i||N(t,s))&&n.call(r||e,t[s],s,t);return e},E.some=function(t,n,r,i){var s;for(s in t)if(i||N(t,s))if(n.call(r||e,t[s],s,t))return!0;return!1},E.getValue=function(t,n){if(!p.isObject(t))return w;var r,i=e.Array(n),s=i.length;for(r=0;t!==w&&r=0){for(i=0;u!==w&&i0),t||(typeof process=="object"&&process.versions&&process.versions.node&&(s.os=process.platform,s.nodejs=n(process.versions.node)),YUI.Env.UA=s),s},e.UA=YUI.Env.UA||YUI.Env.parseUA(),e.UA.compareVersions=function(e,t){var n,r,i,s,o,u;if(e===t)return 0;r=(e+"").split("."),s=(t+"").split(".");for(o=0,u=Math.max(r.length,s.length);oi)return 1}return 0},YUI.Env.aliases={anim:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"],"anim-shape-transform":["anim-shape"],app:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"],attribute:["attribute-base","attribute-complex"],"attribute-events":["attribute-observable"],autocomplete:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"],axes:["axis-numeric","axis-category","axis-time","axis-stacked"],"axes-base":["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"],base:["base-base","base-pluginhost","base-build"],cache:["cache-base","cache-offline","cache-plugin"],charts:["charts-base"],collection:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"],color:["color-base","color-hsl","color-harmony"],controller:["router"],dataschema:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"],datasource:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"],datatable:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"],datatype:["datatype-date","datatype-number","datatype-xml"],"datatype-date":["datatype-date-parse","datatype-date-format","datatype-date-math"],"datatype-number":["datatype-number-parse","datatype-number-format"],"datatype-xml":["datatype-xml-parse","datatype-xml-format"],dd:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"],dom:["dom-base","dom-screen","dom-style","selector-native","selector"],editor:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"],event:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"],"event-custom":["event-custom-base","event-custom-complex"],"event-gestures":["event-flick","event-move"],handlebars:["handlebars-compiler"],highlight:["highlight-base","highlight-accentfold"],history:["history-base","history-hash","history-html5"],io:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"],json:["json-parse","json-stringify"],loader:["loader-base","loader-rollup","loader-yui3"],"loader-pathogen-encoder":["loader-base","loader-rollup","loader-yui3","loader-pathogen-combohandler"],node:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"],pluginhost:["pluginhost-base","pluginhost-config"],querystring:["querystring-parse","querystring-stringify"],recordset:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"],resize:["resize-base","resize-proxy","resize-constrain"],slider:["slider-base","slider-value-range","clickable-rail","range-slider" -],template:["template-base","template-micro"],text:["text-accentfold","text-wordbreak"],widget:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]}},"3.18.1",{use:["yui-base","get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup","loader-yui3"]}),YUI.add("get",function(e,t){var n=e.Lang,r,i,s;e.Get=i={cssOptions:{attributes:{rel:"stylesheet"},doc:e.config.linkDoc||e.config.doc,pollInterval:50},jsOptions:{autopurge:!0,doc:e.config.scriptDoc||e.config.doc},options:{attributes:{charset:"utf-8"},purgethreshold:20},REGEX_CSS:/\.css(?:[?;].*)?$/i,REGEX_JS:/\.js(?:[?;].*)?$/i,_insertCache:{},_pending:null,_purgeNodes:[],_queue:[],abort:function(e){var t,n,r,i,s;if(!e.abort){n=e,s=this._pending,e=null;if(s&&s.transaction.id===n)e=s.transaction,this._pending=null;else for(t=0,i=this._queue.length;t=e&&this._purge(this._purgeNodes)},_getEnv:function(){var t=e.config.doc,n=e.UA;return this._env={async:t&&t.createElement("script").async===!0||n.ie>=10,cssFail:n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0,cssLoad:(!n.gecko&&!n.webkit||n.gecko>=9||n.compareVersions(n.webkit,535.24)>=0)&&!(n.chrome&&n.chrome<=18),preservesScriptOrder:!!(n.gecko||n.opera||n.ie&&n.ie>=10)}},_getTransaction:function(t,r){var i=[],o,u,a,f;n.isArray(t)||(t=[t]),r=e.merge(this.options,r),r.attributes=e.merge(this.options.attributes,r.attributes);for(o=0,u=t.length;o-1&&n.splice(i,1))}}},i.script=i.js,i.Transaction=s=function(t,n){var r=this;r.id=s._lastId+=1,r.data=n.data,r.errors=[],r.nodes=[],r.options=n,r.requests=t,r._callbacks=[],r._queue=[],r._reqsWaiting=0,r.tId=r.id,r.win=n.win||e.config.win},s._lastId=0,s.prototype={_state:"new",abort:function(e){this._pending=null,this._pendingCSS=null,this._pollTimer=clearTimeout(this._pollTimer),this._queue=[],this._reqsWaiting=0,this.errors.push({error:e||"Aborted"}),this._finish()},execute:function(e){var t=this,n=t.requests,r=t._state,i,s,o,u;if(r==="done"){e&&e(t.errors.length?t.errors:null,t);return}e&&t._callbacks.push(e);if(r==="executing")return;t._state="executing",t._queue=o=[],t.options.timeout&&(t._timeout=setTimeout(function(){t.abort("Timeout")},t.options.timeout)),t._reqsWaiting=n.length;for(i=0,s=n.length;i=10?(o.onerror=function(){setTimeout(c,0)},o.onload=function(){setTimeout(h,0)}):(o.onerror=c,o.onload=h),!n.cssFail&&!s&&(f=setTimeout(c,t.timeout||3e3))),this.nodes.push(o),r.parentNode.insertBefore(o,r)},_next:function(){if(this._pending)return;this._queue.length?this._insert(this._queue.shift()):this._reqsWaiting||this._finish()},_poll:function( -t){var n=this,r=n._pendingCSS,i=e.UA.webkit,s,o,u,a,f,l;if(t){r||(r=n._pendingCSS=[]),r.push(t);if(n._pollTimer)return}n._pollTimer=null;for(s=0;s=0)if(l[u].href===a){r.splice(s,1),s-=1,n._progress(null,f);break}}else try{o=!!f.node.sheet.cssRules,r.splice(s,1),s-=1,n._progress(null,f)}catch(c){}}r.length&&(n._pollTimer=setTimeout(function(){n._poll.call(n)},n.options.pollInterval))},_progress:function(e,t){var n=this.options;e&&(t.error=e,this.errors.push({error:e,request:t})),t.node._yuiget_finished=t.finished=!0,n.onProgress&&n.onProgress.call(n.context||this,this._getEventData(t)),t.autopurge&&(i._autoPurge(this.options.purgethreshold),i._purgeNodes.push(t.node)),this._pending===t&&(this._pending=null),this._reqsWaiting-=1,this._next()}}},"3.18.1",{requires:["yui-base"]}),YUI.add("features",function(e,t){var n={};e.mix(e.namespace("Features"),{tests:n,add:function(e,t,r){n[e]=n[e]||{},n[e][t]=r},all:function(t,r){var i=n[t],s=[];return i&&e.Object.each(i,function(n,i){s.push(i+":"+(e.Features.test(t,i,r)?1:0))}),s.length?s.join(";"):""},test:function(t,r,i){i=i||[];var s,o,u,a=n[t],f=a&&a[r];return!f||(s=f.result,e.Lang.isUndefined(s)&&(o=f.ua,o&&(s=e.UA[o]),u=f.test,u&&(!o||s)&&(s=u.apply(e,i)),f.result=s)),s}});var r=e.Features.add;r("load","0",{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"}),r("load","1",{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"}),r("load","2",{name:"dd-gestures",trigger:"dd-drag",ua:"touchEnabled"}),r("load","3",{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"}),r("load","4",{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"}),r("load","5",{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"}),r("load","6",{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","7",{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}),r("load","8",{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","9",{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}),r("load","10",{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","11",{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}),r("load","12",{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"}),r("load","13",{name:"io-nodejs",trigger:"io-base",ua:"nodejs"}),r("load","14",{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"}),r("load","15",{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"}),r("load","16",{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"}),r("load","17",{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"}),r("load","18",{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"}),r("load","19",{name:"widget-base-ie",trigger:"widget-base",ua:"ie"}),r("load","20",{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"}),r("load","21",{name:"yql-nodejs",trigger:"yql",ua:"nodejs"}),r("load","22",{name:"yql-winjs",trigger:"yql",ua:"winjs"})},"3.18.1",{requires:["yui-base"]}),YUI.add("intl-base",function(e,t){var n=/[, ]/;e.mix(e.namespace("Intl"),{lookupBestLang:function(t,r){function a(e){var t;for(t=0;t0){o=a(s);if(o)return o;u=s.lastIndexOf("-");if(!(u>=0))break;s=s.substring(0,u),u>=2&&s.charAt(u-2)==="-"&&(s=s.substring(0,u-2))}}return""}})},"3.18.1",{requires:["yui-base"]}),YUI.add("yui-log",function(e,t){var n=e,r="yui:log",i="undefined",s={debug:1,info:2,warn:4,error:8};n.log=function(e,t,o,u){var a,f,l,c,h,p,d=n,v=d.config,m=d.fire?d:YUI.Env.globalEvents;return v.debug&&(o=o||"",typeof o!="undefined"&&(f=v.logExclude,l=v.logInclude,!l||o in l?l&&o in l?a=!l[o]:f&&o in f&&(a=f[o]):a=1,typeof t=="undefined"&&(t="info"),d.config.logLevel=d.config.logLevel||"debug",p=s[d.config.logLevel.toLowerCase()],t in s&&s[t]-1,n.comboSep="&",n.maxURLLength=i,n.ignoreRegistered=t.ignoreRegistered,n.root=e.Env.meta.root,n.timeout=0,n.forceMap={},n.allowRollup=!1,n.filters={},n.required={},n.patterns={},n.moduleInfo={},n.groups=e.merge(e.Env.meta.groups),n.skin=e.merge(e.Env.meta.skin),n.conditions={},n.config=t,n._internal=!0,n._populateConditionsCache(),n.loaded=o[c],n.async=!0,n._inspectPage(),n._internal=!1,n._config(t),n.forceMap=n.force?e.Array.hash(n.force):{},n.testresults=null,e.config.tests&&(n.testresults=e.config.tests),n.sorted=[],n.dirty=!0,n.inserted={},n.skipped={},n.tested={},n.ignoreRegistered&&n._resetModules()},e.Loader.prototype={getModuleInfo:function(t){var n=this.moduleInfo[t],r,i,o,a;return n?n:(r=g.modules,i=s._renderedMods,o=this._internal,i&&i.hasOwnProperty(t)&&!this.ignoreRegistered?this.moduleInfo[t]=e.merge(i[t]):r.hasOwnProperty(t)&&(this._internal=!0,a=this.addModule(r[t],t),a&&a.type===u&&this.isCSSLoaded(a.name,!0)&&(this.loaded[a.name]=!0),this._internal=o),this.moduleInfo[t])},_expandAliases:function(t){var n=[],r=YUI.Env.aliases,i,s;t=e.Array(t);for(i=0;i-1&&(A=n);if(L&&(L[m]||A&&L[A])){O=m,L[A]&&(O=A);for(n=0;n-1},getModule:function(t){if(!t)return null;var n,r,i,s=this.getModuleInfo(t),o=this.patterns;if(!s||s&&s.ext)for(i in o)if(o.hasOwnProperty(i)){n=o[i],n.test||(n.test=this._patternTest);if(n.test(t,i)){r=n;break}}return s?r&&s&&r.configFn&&!s.configFn&&(s.configFn=r.configFn,s.configFn(s)):r&&(n.action?n.action.call(this,t,i):(s=this.addModule(e.merge(r,{test:void 0,temp:!0}),t),s&&r.configFn&&(s.configFn=r.configFn))),s},_rollup:function(){},_reduce:function(e){e=e||this.required;var t,n,r,i,s=this.loadType,o=this.ignore?v.hash(this.ignore):!1;for(t in e)if(e.hasOwnProperty(t)){i=this.getModule(t),((this.loaded[t]||w[t])&&!this.forceMap[t]&&!this.ignoreRegistered||s&&i&&i.type!==s)&&delete e[t],o&&o[t]&&delete e[t],r=i&&i.supersedes;if(r)for(n=0;n0&&(m.running=!0,m.next()())},insert:function(t,n,r){var i=this,s=e.merge(this);delete s.require,delete s.dirty,m.add(function(){i._insert(s,t,n,r)}),this._continue()},loadNext:function(){return},_filter:function(e,t,n){var r=this.filter,i=t&&t in this.filters,s=i&&this.filters[t],o=n||(this.getModuleInfo(t)||{}).group||null;return o&&this.groups[o]&&this.groups[o].filter&&(s=this.groups[o].filter,i=!0),e&&(i&&(r=b.isString(s)?this.FILTER_DEFS[s.toUpperCase()]||null:s),r&&(e=e.replace(new RegExp(r.searchExp,"g"),r.replaceStr))),e},_url:function(e,t,n){return this._filter((n||this.base||"")+e,t)},resolve:function(t,r){var i=this,s={js:[],jsMods:[],css:[],cssMods:[]},o,f=e.config.comboLoader&&e.config.customComboBase;(i.skin.overrides||i.skin.defaultSkin!==l||i.ignoreRegistered)&&i._resetModules(),t&&i.calculate(),r=r||i.sorted,o=function(e){if(e){var t=e.group&&i.groups[e.group]||n,r;t.async===!1&&(e.async=t.async),r=e.fullpath?i._filter(e.fullpath,e.name):i._url(e.path,e.name,t.base||e.base);if(e.attributes||e.async===!1)r={url:r,async:e.async},e.attributes&&(r.attributes=e.attributes);s[e.type].push(r),s[e.type+"Mods"].push(e)}};var c=i.ignoreRegistered?{}:i.inserted,h,p,d,v,m,g,y,b,w,E=!1;for(w=0,b=r.length;wp){n=[];for(g=0,y=f.length;gp&&(l=n.pop(),s=d+n.join(m),e[c].push(b._filter(s,null,v.group)),n=[],l&&n.push(l));n.length&&(s=d+n.join(m),e[c].push(b._filter(s,null,v.group)))}else e[c].push(b._filter(s,null,v.group))}}return e},load:function(e){if(!e)return;var t=this,n=t.resolve(!0);t.data=n,t.onEnd=function(){e.apply(t.context||t,arguments)},t.insert()}}},"3.18.1",{requires:["get","features"]}),YUI.add("loader-rollup",function(e,t){e.Loader.prototype._rollup=function(){var e,t,n,r,i=this.required,s,o=this.moduleInfo,u,a,f;if(this.dirty||!this.rollups){this.rollups={};for(e in o)o.hasOwnProperty(e)&&(n=this.getModule(e),n&&n.rollup&&(this.rollups[e]=n))}for(;;){u=!1;for(e in this.rollups)if(this.rollups.hasOwnProperty(e)&&!i[e]&&(!this.loaded[e]||this.forceMap[e])){n=this.getModule(e),r=n.supersedes||[],s=!1;if(!n.rollup)continue;a=0;for(t=0;t=n.rollup;if(s)break}}s&&(i[e]=!0,u=!0,this.getRequires(n))}if(!u)break}}},"3.18.1",{requires:["loader-base"]}),YUI.add("loader-yui3",function(e,t){YUI.Env[e.version].modules=YUI.Env[e.version].modules||{},e.mix(YUI.Env[e.version].modules,{"align-plugin":{requires:["node-screen","node-pluginhost"]},anim:{use:["anim-base","anim-color","anim-curve","anim-easing","anim-node-plugin","anim-scroll","anim-xy"]},"anim-base":{requires:["base-base","node-style","color-base"]},"anim-color":{requires:["anim-base"]},"anim-curve":{requires:["anim-xy"]},"anim-easing":{requires:["anim-base"]},"anim-node-plugin":{requires:["node-pluginhost","anim-base"]},"anim-scroll":{requires:["anim-base"]},"anim-shape":{requires:["anim-base","anim-easing","anim-color","matrix"]},"anim-shape-transform":{use:["anim-shape"]},"anim-xy":{requires:["anim-base","node-screen"]},app:{use:["app-base","app-content","app-transitions","lazy-model-list","model","model-list","model-sync-rest","model-sync-local","router","view","view-node-map"]},"app-base":{requires:["classnamemanager","pjax-base","router","view"]},"app-content":{requires:["app-base","pjax-content"]},"app-transitions":{requires:["app-base"]},"app-transitions-css":{type:"css"},"app-transitions-native":{condition:{name:"app-transitions-native",test:function(e){var t=e.config.doc,n=t?t.documentElement:null;return n&&n.style?"MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style:!1},trigger:"app-transitions"},requires:["app-transitions","app-transitions-css","parallel","transition"]},"array-extras":{requires:["yui-base"]},"array-invoke":{requires:["yui-base"]},arraylist:{requires:["yui-base"]},"arraylist-add":{requires:["arraylist"]},"arraylist-filter":{requires:["arraylist"]},arraysort:{requires:["yui-base"]},"async-queue":{requires:["event-custom"]},attribute:{use:["attribute-base","attribute-complex"]},"attribute-base":{requires:["attribute-core","attribute-observable","attribute-extras"]},"attribute-complex":{requires:["attribute-base"]},"attribute-core":{requires:["oop"]},"attribute-events":{use:["attribute-observable"]},"attribute-extras":{requires:["oop"]},"attribute-observable":{requires:["event-custom"]},autocomplete:{use:["autocomplete-base","autocomplete-sources","autocomplete-list","autocomplete-plugin"]},"autocomplete-base":{optional:["autocomplete-sources"],requires:["array-extras","base-build","escape","event-valuechange","node-base"]},"autocomplete-filters":{requires:["array-extras","text-wordbreak"]},"autocomplete-filters-accentfold":{requires:["array-extras","text-accentfold","text-wordbreak"]},"autocomplete-highlighters":{requires:["array-extras","highlight-base"]},"autocomplete-highlighters-accentfold":{requires:["array-extras","highlight-accentfold"]},"autocomplete-list":{after:["autocomplete-sources"],lang:["en","es","hu","it"],requires:["autocomplete-base","event-resize","node-screen","selector-css3","shim-plugin","widget","widget-position","widget-position-align"],skinnable:!0},"autocomplete-list-keys":{condition:{name:"autocomplete-list-keys",test:function(e){return!e.UA.ios&&!e.UA.android},trigger:"autocomplete-list"},requires:["autocomplete-list","base-build"]},"autocomplete-plugin":{requires:["autocomplete-list","node-pluginhost"]},"autocomplete-sources":{optional:["io-base","json-parse","jsonp","yql"],requires:["autocomplete-base"]},axes:{use:["axis-numeric","axis-category","axis-time","axis-stacked"]},"axes-base":{use:["axis-numeric-base","axis-category-base","axis-time-base","axis-stacked-base"]},axis:{requires:["dom","widget","widget-position","widget-stack","graphics","axis-base"]},"axis-base":{requires:["classnamemanager","datatype-number","datatype-date","base","event-custom"]},"axis-category":{requires:["axis","axis-category-base"]},"axis-category-base":{requires:["axis-base"]},"axis-numeric":{requires:["axis","axis-numeric-base"]},"axis-numeric-base":{requires:["axis-base"]},"axis-stacked":{requires:["axis-numeric","axis-stacked-base"]},"axis-stacked-base":{requires:["axis-numeric-base"]},"axis-time":{requires:["axis","axis-time-base"]},"axis-time-base":{requires:["axis-base"]},base:{use:["base-base","base-pluginhost","base-build"]},"base-base":{requires:["attribute-base","base-core","base-observable"]},"base-build":{requires:["base-base"]},"base-core":{requires:["attribute-core"]},"base-observable":{requires:["attribute-observable","base-core"]},"base-pluginhost":{requires:["base-base","pluginhost"]},button:{requires:["button-core","cssbutton","widget"]},"button-core":{requires:["attribute-core","classnamemanager","node-base","escape"]},"button-group":{requires:["button-plugin","cssbutton","widget"]},"button-plugin":{requires:["button-core","cssbutton","node-pluginhost"]},cache:{use:["cache-base","cache-offline","cache-plugin"]},"cache-base":{requires:["base"]},"cache-offline":{requires:["cache-base","json"]},"cache-plugin":{requires:["plugin","cache-base"]},calendar:{requires:["calendar-base","calendarnavigator"],skinnable:!0},"calendar-base":{lang:["de","en","es","es-AR","fr","hu","it","ja","nb-NO","nl","pt-BR","ru","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-HANT-TW"],requires:["widget","datatype-date","datatype-date-math","cssgrids"],skinnable:!0},calendarnavigator:{requires:["plugin","classnamemanager","datatype-date","node"],skinnable:!0},charts:{use:["charts-base"]},"charts-base":{requires:["dom","event-mouseenter","event-touch","graphics-group","axes","series-pie","series-line","series-marker","series-area","series-spline","series-column","series-bar","series-areaspline","series-combo","series-combospline","series-line-stacked","series-marker-stacked","series-area-stacked","series-spline-stacked","series-column-stacked","series-bar-stacked","series-areaspline-stacked","series-combo-stacked","series-combospline-stacked"]},"charts-legend":{requires:["charts-base"]},classnamemanager:{requires:["yui-base" -]},"clickable-rail":{requires:["slider-base"]},collection:{use:["array-extras","arraylist","arraylist-add","arraylist-filter","array-invoke"]},color:{use:["color-base","color-hsl","color-harmony"]},"color-base":{requires:["yui-base"]},"color-harmony":{requires:["color-hsl"]},"color-hsl":{requires:["color-base"]},"color-hsv":{requires:["color-base"]},console:{lang:["en","es","hu","it","ja"],requires:["yui-log","widget"],skinnable:!0},"console-filters":{requires:["plugin","console"],skinnable:!0},"content-editable":{requires:["node-base","editor-selection","stylesheet","plugin"]},controller:{use:["router"]},cookie:{requires:["yui-base"]},"createlink-base":{requires:["editor-base"]},cssbase:{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},"cssbase-context":{after:["cssreset","cssfonts","cssgrids","cssreset-context","cssfonts-context","cssgrids-context"],type:"css"},cssbutton:{type:"css"},cssfonts:{type:"css"},"cssfonts-context":{type:"css"},cssgrids:{optional:["cssnormalize"],type:"css"},"cssgrids-base":{optional:["cssnormalize"],type:"css"},"cssgrids-responsive":{optional:["cssnormalize"],requires:["cssgrids","cssgrids-responsive-base"],type:"css"},"cssgrids-units":{optional:["cssnormalize"],requires:["cssgrids-base"],type:"css"},cssnormalize:{type:"css"},"cssnormalize-context":{type:"css"},cssreset:{type:"css"},"cssreset-context":{type:"css"},dataschema:{use:["dataschema-base","dataschema-json","dataschema-xml","dataschema-array","dataschema-text"]},"dataschema-array":{requires:["dataschema-base"]},"dataschema-base":{requires:["base"]},"dataschema-json":{requires:["dataschema-base","json"]},"dataschema-text":{requires:["dataschema-base"]},"dataschema-xml":{requires:["dataschema-base"]},datasource:{use:["datasource-local","datasource-io","datasource-get","datasource-function","datasource-cache","datasource-jsonschema","datasource-xmlschema","datasource-arrayschema","datasource-textschema","datasource-polling"]},"datasource-arrayschema":{requires:["datasource-local","plugin","dataschema-array"]},"datasource-cache":{requires:["datasource-local","plugin","cache-base"]},"datasource-function":{requires:["datasource-local"]},"datasource-get":{requires:["datasource-local","get"]},"datasource-io":{requires:["datasource-local","io-base"]},"datasource-jsonschema":{requires:["datasource-local","plugin","dataschema-json"]},"datasource-local":{requires:["base"]},"datasource-polling":{requires:["datasource-local"]},"datasource-textschema":{requires:["datasource-local","plugin","dataschema-text"]},"datasource-xmlschema":{requires:["datasource-local","plugin","datatype-xml","dataschema-xml"]},datatable:{use:["datatable-core","datatable-table","datatable-head","datatable-body","datatable-base","datatable-column-widths","datatable-message","datatable-mutable","datatable-sort","datatable-datasource"]},"datatable-base":{requires:["datatable-core","datatable-table","datatable-head","datatable-body","base-build","widget"],skinnable:!0},"datatable-body":{requires:["datatable-core","view","classnamemanager"]},"datatable-column-widths":{requires:["datatable-base"]},"datatable-core":{requires:["escape","model-list","node-event-delegate"]},"datatable-datasource":{requires:["datatable-base","plugin","datasource-local"]},"datatable-foot":{requires:["datatable-core","view"]},"datatable-formatters":{requires:["datatable-body","datatype-number-format","datatype-date-format","escape"]},"datatable-head":{requires:["datatable-core","view","classnamemanager"]},"datatable-highlight":{requires:["datatable-base","event-hover"],skinnable:!0},"datatable-keynav":{requires:["datatable-base"]},"datatable-message":{lang:["en","fr","es","hu","it"],requires:["datatable-base"],skinnable:!0},"datatable-mutable":{requires:["datatable-base"]},"datatable-paginator":{lang:["en","fr"],requires:["model","view","paginator-core","datatable-foot","datatable-paginator-templates"],skinnable:!0},"datatable-paginator-templates":{requires:["template"]},"datatable-scroll":{requires:["datatable-base","datatable-column-widths","dom-screen"],skinnable:!0},"datatable-sort":{lang:["en","fr","es","hu"],requires:["datatable-base"],skinnable:!0},"datatable-table":{requires:["datatable-core","datatable-head","datatable-body","view","classnamemanager"]},datatype:{use:["datatype-date","datatype-number","datatype-xml"]},"datatype-date":{use:["datatype-date-parse","datatype-date-format","datatype-date-math"]},"datatype-date-format":{lang:["ar","ar-JO","ca","ca-ES","da","da-DK","de","de-AT","de-DE","el","el-GR","en","en-AU","en-CA","en-GB","en-IE","en-IN","en-JO","en-MY","en-NZ","en-PH","en-SG","en-US","es","es-AR","es-BO","es-CL","es-CO","es-EC","es-ES","es-MX","es-PE","es-PY","es-US","es-UY","es-VE","fi","fi-FI","fr","fr-BE","fr-CA","fr-FR","hi","hi-IN","hu","id","id-ID","it","it-IT","ja","ja-JP","ko","ko-KR","ms","ms-MY","nb","nb-NO","nl","nl-BE","nl-NL","pl","pl-PL","pt","pt-BR","ro","ro-RO","ru","ru-RU","sv","sv-SE","th","th-TH","tr","tr-TR","vi","vi-VN","zh-Hans","zh-Hans-CN","zh-Hant","zh-Hant-HK","zh-Hant-TW"]},"datatype-date-math":{requires:["yui-base"]},"datatype-date-parse":{},"datatype-number":{use:["datatype-number-parse","datatype-number-format"]},"datatype-number-format":{},"datatype-number-parse":{requires:["escape"]},"datatype-xml":{use:["datatype-xml-parse","datatype-xml-format"]},"datatype-xml-format":{},"datatype-xml-parse":{},dd:{use:["dd-ddm-base","dd-ddm","dd-ddm-drop","dd-drag","dd-proxy","dd-constrain","dd-drop","dd-scroll","dd-delegate"]},"dd-constrain":{requires:["dd-drag"]},"dd-ddm":{requires:["dd-ddm-base","event-resize"]},"dd-ddm-base":{requires:["node","base","yui-throttle","classnamemanager"]},"dd-ddm-drop":{requires:["dd-ddm"]},"dd-delegate":{requires:["dd-drag","dd-drop-plugin","event-mouseenter"]},"dd-drag":{requires:["dd-ddm-base","selector-css2"]},"dd-drop":{requires:["dd-drag","dd-ddm-drop"]},"dd-drop-plugin":{requires:["dd-drop"]},"dd-gestures":{condition:{name:"dd-gestures" -,trigger:"dd-drag",ua:"touchEnabled"},requires:["dd-drag","event-synthetic","event-gestures"]},"dd-plugin":{optional:["dd-constrain","dd-proxy"],requires:["dd-drag"]},"dd-proxy":{requires:["dd-drag"]},"dd-scroll":{requires:["dd-drag"]},dial:{lang:["en","es","hu"],requires:["widget","dd-drag","event-mouseenter","event-move","event-key","transition","intl"],skinnable:!0},dom:{use:["dom-base","dom-screen","dom-style","selector-native","selector"]},"dom-base":{requires:["dom-core"]},"dom-core":{requires:["oop","features"]},"dom-screen":{requires:["dom-base","dom-style"]},"dom-style":{requires:["dom-base"]},"dom-style-ie":{condition:{name:"dom-style-ie",test:function(e){var t=e.Features.test,n=e.Features.add,r=e.config.win,i=e.config.doc,s="documentElement",o=!1;return n("style","computedStyle",{test:function(){return r&&"getComputedStyle"in r}}),n("style","opacity",{test:function(){return i&&"opacity"in i[s].style}}),o=!t("style","opacity")&&!t("style","computedStyle"),o},trigger:"dom-style"},requires:["dom-style","color-base"]},dump:{requires:["yui-base"]},editor:{use:["frame","editor-selection","exec-command","editor-base","editor-para","editor-br","editor-bidi","editor-tab","createlink-base"]},"editor-base":{requires:["base","frame","node","exec-command","editor-selection"]},"editor-bidi":{requires:["editor-base"]},"editor-br":{requires:["editor-base"]},"editor-inline":{requires:["editor-base","content-editable"]},"editor-lists":{requires:["editor-base"]},"editor-para":{requires:["editor-para-base"]},"editor-para-base":{requires:["editor-base"]},"editor-para-ie":{condition:{name:"editor-para-ie",trigger:"editor-para",ua:"ie",when:"instead"},requires:["editor-para-base"]},"editor-selection":{requires:["node"]},"editor-tab":{requires:["editor-base"]},escape:{requires:["yui-base"]},event:{after:["node-base"],use:["event-base","event-delegate","event-synthetic","event-mousewheel","event-mouseenter","event-key","event-focus","event-resize","event-hover","event-outside","event-touch","event-move","event-flick","event-valuechange","event-tap"]},"event-base":{after:["node-base"],requires:["event-custom-base"]},"event-base-ie":{after:["event-base"],condition:{name:"event-base-ie",test:function(e){var t=e.config.doc&&e.config.doc.implementation;return t&&!t.hasFeature("Events","2.0")},trigger:"node-base"},requires:["node-base"]},"event-contextmenu":{requires:["event-synthetic","dom-screen"]},"event-custom":{use:["event-custom-base","event-custom-complex"]},"event-custom-base":{requires:["oop"]},"event-custom-complex":{requires:["event-custom-base"]},"event-delegate":{requires:["node-base"]},"event-flick":{requires:["node-base","event-touch","event-synthetic"]},"event-focus":{requires:["event-synthetic"]},"event-gestures":{use:["event-flick","event-move"]},"event-hover":{requires:["event-mouseenter"]},"event-key":{requires:["event-synthetic"]},"event-mouseenter":{requires:["event-synthetic"]},"event-mousewheel":{requires:["node-base"]},"event-move":{requires:["node-base","event-touch","event-synthetic"]},"event-outside":{requires:["event-synthetic"]},"event-resize":{requires:["node-base","event-synthetic"]},"event-simulate":{requires:["event-base"]},"event-synthetic":{requires:["node-base","event-custom-complex"]},"event-tap":{requires:["node-base","event-base","event-touch","event-synthetic"]},"event-touch":{requires:["node-base"]},"event-valuechange":{requires:["event-focus","event-synthetic"]},"exec-command":{requires:["frame"]},features:{requires:["yui-base"]},file:{requires:["file-flash","file-html5"]},"file-flash":{requires:["base"]},"file-html5":{requires:["base"]},frame:{requires:["base","node","plugin","selector-css3","yui-throttle"]},"gesture-simulate":{requires:["async-queue","event-simulate","node-screen"]},get:{requires:["yui-base"]},graphics:{requires:["node","event-custom","pluginhost","matrix","classnamemanager"]},"graphics-canvas":{condition:{name:"graphics-canvas",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-canvas-default":{condition:{name:"graphics-canvas-default",test:function(e){var t=e.config.doc,n=e.config.defaultGraphicEngine&&e.config.defaultGraphicEngine=="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return(!i||n)&&r&&r.getContext&&r.getContext("2d")},trigger:"graphics"}},"graphics-group":{requires:["graphics"]},"graphics-svg":{condition:{name:"graphics-svg",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"},requires:["graphics"]},"graphics-svg-default":{condition:{name:"graphics-svg-default",test:function(e){var t=e.config.doc,n=!e.config.defaultGraphicEngine||e.config.defaultGraphicEngine!="canvas",r=t&&t.createElement("canvas"),i=t&&t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1");return i&&(n||!r)},trigger:"graphics"}},"graphics-vml":{condition:{name:"graphics-vml",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"},requires:["graphics","color-base"]},"graphics-vml-default":{condition:{name:"graphics-vml-default",test:function(e){var t=e.config.doc,n=t&&t.createElement("canvas");return t&&!t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")&&(!n||!n.getContext||!n.getContext("2d"))},trigger:"graphics"}},handlebars:{use:["handlebars-compiler" -]},"handlebars-base":{requires:[]},"handlebars-compiler":{requires:["handlebars-base"]},highlight:{use:["highlight-base","highlight-accentfold"]},"highlight-accentfold":{requires:["highlight-base","text-accentfold"]},"highlight-base":{requires:["array-extras","classnamemanager","escape","text-wordbreak"]},history:{use:["history-base","history-hash","history-html5"]},"history-base":{requires:["event-custom-complex"]},"history-hash":{after:["history-html5"],requires:["event-synthetic","history-base","yui-later"]},"history-hash-ie":{condition:{name:"history-hash-ie",test:function(e){var t=e.config.doc&&e.config.doc.documentMode;return e.UA.ie&&(!("onhashchange"in e.config.win)||!t||t<8)},trigger:"history-hash"},requires:["history-hash","node-base"]},"history-html5":{optional:["json"],requires:["event-base","history-base","node-base"]},imageloader:{requires:["base-base","node-style","node-screen"]},intl:{requires:["intl-base","event-custom"]},"intl-base":{requires:["yui-base"]},io:{use:["io-base","io-xdr","io-form","io-upload-iframe","io-queue"]},"io-base":{requires:["event-custom-base","querystring-stringify-simple"]},"io-form":{requires:["io-base","node-base"]},"io-nodejs":{condition:{name:"io-nodejs",trigger:"io-base",ua:"nodejs"},requires:["io-base"]},"io-queue":{requires:["io-base","queue-promote"]},"io-upload-iframe":{requires:["io-base","node-base"]},"io-xdr":{requires:["io-base","datatype-xml-parse"]},json:{use:["json-parse","json-stringify"]},"json-parse":{requires:["yui-base"]},"json-parse-shim":{condition:{name:"json-parse-shim",test:function(e){function i(e,t){return e==="ok"?!0:t}var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONParse!==!1&&!!n;if(r)try{r=n.parse('{"ok":false}',i).ok}catch(s){r=!1}return!r},trigger:"json-parse"},requires:["json-parse"]},"json-stringify":{requires:["yui-base"]},"json-stringify-shim":{condition:{name:"json-stringify-shim",test:function(e){var t=e.config.global.JSON,n=Object.prototype.toString.call(t)==="[object JSON]"&&t,r=e.config.useNativeJSONStringify!==!1&&!!n;if(r)try{r="0"===n.stringify(0)}catch(i){r=!1}return!r},trigger:"json-stringify"},requires:["json-stringify"]},jsonp:{requires:["get","oop"]},"jsonp-url":{requires:["jsonp"]},"lazy-model-list":{requires:["model-list"]},loader:{use:["loader-base","loader-rollup","loader-yui3"]},"loader-base":{requires:["get","features"]},"loader-pathogen-combohandler":{},"loader-pathogen-encoder":{use:["loader-base","loader-rollup","loader-yui3","loader-pathogen-combohandler"]},"loader-rollup":{requires:["loader-base"]},"loader-yui3":{requires:["loader-base"]},matrix:{requires:["yui-base"]},model:{requires:["base-build","escape","json-parse"]},"model-list":{requires:["array-extras","array-invoke","arraylist","base-build","escape","json-parse","model"]},"model-sync-local":{requires:["model","json-stringify"]},"model-sync-rest":{requires:["model","io-base","json-stringify"]},node:{use:["node-base","node-event-delegate","node-pluginhost","node-screen","node-style"]},"node-base":{requires:["event-base","node-core","dom-base","dom-style"]},"node-core":{requires:["dom-core","selector"]},"node-event-delegate":{requires:["node-base","event-delegate"]},"node-event-html5":{requires:["node-base"]},"node-event-simulate":{requires:["node-base","event-simulate","gesture-simulate"]},"node-flick":{requires:["classnamemanager","transition","event-flick","plugin"],skinnable:!0},"node-focusmanager":{requires:["attribute","node","plugin","node-event-simulate","event-key","event-focus"]},"node-load":{requires:["node-base","io-base"]},"node-menunav":{requires:["node","classnamemanager","plugin","node-focusmanager"],skinnable:!0},"node-pluginhost":{requires:["node-base","pluginhost"]},"node-screen":{requires:["dom-screen","node-base"]},"node-scroll-info":{requires:["array-extras","base-build","event-resize","node-pluginhost","plugin","selector"]},"node-style":{requires:["dom-style","node-base"]},oop:{requires:["yui-base"]},overlay:{requires:["widget","widget-stdmod","widget-position","widget-position-align","widget-stack","widget-position-constrain"],skinnable:!0},paginator:{requires:["paginator-core"]},"paginator-core":{requires:["base"]},"paginator-url":{requires:["paginator"]},panel:{requires:["widget","widget-autohide","widget-buttons","widget-modality","widget-position","widget-position-align","widget-position-constrain","widget-stack","widget-stdmod"],skinnable:!0},parallel:{requires:["yui-base"]},pjax:{requires:["pjax-base","pjax-content"]},"pjax-base":{requires:["classnamemanager","node-event-delegate","router"]},"pjax-content":{requires:["io-base","node-base","router"]},"pjax-plugin":{requires:["node-pluginhost","pjax","plugin"]},plugin:{requires:["base-base"]},pluginhost:{use:["pluginhost-base","pluginhost-config"]},"pluginhost-base":{requires:["yui-base"]},"pluginhost-config":{requires:["pluginhost-base"]},promise:{requires:["timers"]},querystring:{use:["querystring-parse","querystring-stringify"]},"querystring-parse":{requires:["yui-base","array-extras"]},"querystring-parse-simple":{requires:["yui-base"]},"querystring-stringify":{requires:["yui-base"]},"querystring-stringify-simple":{requires:["yui-base"]},"queue-promote":{requires:["yui-base"]},"range-slider":{requires:["slider-base","slider-value-range","clickable-rail"]},recordset:{use:["recordset-base","recordset-sort","recordset-filter","recordset-indexer"]},"recordset-base":{requires:["base","arraylist"]},"recordset-filter":{requires:["recordset-base","array-extras","plugin"]},"recordset-indexer":{requires:["recordset-base","plugin"]},"recordset-sort":{requires:["arraysort","recordset-base","plugin"]},resize:{use:["resize-base","resize-proxy","resize-constrain"]},"resize-base":{requires:["base","widget","event","oop","dd-drag","dd-delegate","dd-drop"],skinnable:!0},"resize-constrain":{requires:["plugin","resize-base"]},"resize-plugin":{optional:["resize-constrain"],requires:["resize-base","plugin"]},"resize-proxy" -:{requires:["plugin","resize-base"]},router:{optional:["querystring-parse"],requires:["array-extras","base-build","history"]},scrollview:{requires:["scrollview-base","scrollview-scrollbars"]},"scrollview-base":{requires:["widget","event-gestures","event-mousewheel","transition"],skinnable:!0},"scrollview-base-ie":{condition:{name:"scrollview-base-ie",trigger:"scrollview-base",ua:"ie"},requires:["scrollview-base"]},"scrollview-list":{requires:["plugin","classnamemanager"],skinnable:!0},"scrollview-paginator":{requires:["plugin","classnamemanager"]},"scrollview-scrollbars":{requires:["classnamemanager","transition","plugin"],skinnable:!0},selector:{requires:["selector-native"]},"selector-css2":{condition:{name:"selector-css2",test:function(e){var t=e.config.doc,n=t&&!("querySelectorAll"in t);return n},trigger:"selector"},requires:["selector-native"]},"selector-css3":{requires:["selector-native","selector-css2"]},"selector-native":{requires:["dom-base"]},"series-area":{requires:["series-cartesian","series-fill-util"]},"series-area-stacked":{requires:["series-stacked","series-area"]},"series-areaspline":{requires:["series-area","series-curve-util"]},"series-areaspline-stacked":{requires:["series-stacked","series-areaspline"]},"series-bar":{requires:["series-marker","series-histogram-base"]},"series-bar-stacked":{requires:["series-stacked","series-bar"]},"series-base":{requires:["graphics","axis-base"]},"series-candlestick":{requires:["series-range"]},"series-cartesian":{requires:["series-base"]},"series-column":{requires:["series-marker","series-histogram-base"]},"series-column-stacked":{requires:["series-stacked","series-column"]},"series-combo":{requires:["series-cartesian","series-line-util","series-plot-util","series-fill-util"]},"series-combo-stacked":{requires:["series-stacked","series-combo"]},"series-combospline":{requires:["series-combo","series-curve-util"]},"series-combospline-stacked":{requires:["series-combo-stacked","series-curve-util"]},"series-curve-util":{},"series-fill-util":{},"series-histogram-base":{requires:["series-cartesian","series-plot-util"]},"series-line":{requires:["series-cartesian","series-line-util"]},"series-line-stacked":{requires:["series-stacked","series-line"]},"series-line-util":{},"series-marker":{requires:["series-cartesian","series-plot-util"]},"series-marker-stacked":{requires:["series-stacked","series-marker"]},"series-ohlc":{requires:["series-range"]},"series-pie":{requires:["series-base","series-plot-util"]},"series-plot-util":{},"series-range":{requires:["series-cartesian"]},"series-spline":{requires:["series-line","series-curve-util"]},"series-spline-stacked":{requires:["series-stacked","series-spline"]},"series-stacked":{requires:["axis-stacked"]},"shim-plugin":{requires:["node-style","node-pluginhost"]},slider:{use:["slider-base","slider-value-range","clickable-rail","range-slider"]},"slider-base":{requires:["widget","dd-constrain","event-key"],skinnable:!0},"slider-value-range":{requires:["slider-base"]},sortable:{requires:["dd-delegate","dd-drop-plugin","dd-proxy"]},"sortable-scroll":{requires:["dd-scroll","sortable"]},stylesheet:{requires:["yui-base"]},substitute:{optional:["dump"],requires:["yui-base"]},swf:{requires:["event-custom","node","swfdetect","escape"]},swfdetect:{requires:["yui-base"]},tabview:{requires:["widget","widget-parent","widget-child","tabview-base","node-pluginhost","node-focusmanager"],skinnable:!0},"tabview-base":{requires:["node-event-delegate","classnamemanager"]},"tabview-plugin":{requires:["tabview-base"]},template:{use:["template-base","template-micro"]},"template-base":{requires:["yui-base"]},"template-micro":{requires:["escape"]},test:{requires:["event-simulate","event-custom","json-stringify"]},"test-console":{requires:["console-filters","test","array-extras"],skinnable:!0},text:{use:["text-accentfold","text-wordbreak"]},"text-accentfold":{requires:["array-extras","text-data-accentfold"]},"text-data-accentfold":{requires:["yui-base"]},"text-data-wordbreak":{requires:["yui-base"]},"text-wordbreak":{requires:["array-extras","text-data-wordbreak"]},timers:{requires:["yui-base"]},transition:{requires:["node-style"]},"transition-timer":{condition:{name:"transition-timer",test:function(e){var t=e.config.doc,n=t?t.documentElement:null,r=!0;return n&&n.style&&(r=!("MozTransition"in n.style||"WebkitTransition"in n.style||"transition"in n.style)),r},trigger:"transition"},requires:["transition"]},tree:{requires:["base-build","tree-node"]},"tree-labelable":{requires:["tree"]},"tree-lazy":{requires:["base-pluginhost","plugin","tree"]},"tree-node":{},"tree-openable":{requires:["tree"]},"tree-selectable":{requires:["tree"]},"tree-sortable":{requires:["tree"]},uploader:{requires:["uploader-html5","uploader-flash"]},"uploader-flash":{requires:["swfdetect","escape","widget","base","cssbutton","node","event-custom","uploader-queue"]},"uploader-html5":{requires:["widget","node-event-simulate","file-html5","uploader-queue"]},"uploader-queue":{requires:["base"]},view:{requires:["base-build","node-event-delegate"]},"view-node-map":{requires:["view"]},widget:{use:["widget-base","widget-htmlparser","widget-skin","widget-uievents"]},"widget-anim":{requires:["anim-base","plugin","widget"]},"widget-autohide":{requires:["base-build","event-key","event-outside","widget"]},"widget-base":{requires:["attribute","base-base","base-pluginhost","classnamemanager","event-focus","node-base","node-style"],skinnable:!0},"widget-base-ie":{condition:{name:"widget-base-ie",trigger:"widget-base",ua:"ie"},requires:["widget-base"]},"widget-buttons":{requires:["button-plugin","cssbutton","widget-stdmod"]},"widget-child":{requires:["base-build","widget"]},"widget-htmlparser":{requires:["widget-base"]},"widget-modality":{requires:["base-build","event-outside","widget"],skinnable:!0},"widget-parent":{requires:["arraylist","base-build","widget"]},"widget-position":{requires:["base-build","node-screen","widget"]},"widget-position-align":{requires:["widget-position" -]},"widget-position-constrain":{requires:["widget-position"]},"widget-skin":{requires:["widget-base"]},"widget-stack":{requires:["base-build","widget"],skinnable:!0},"widget-stdmod":{requires:["base-build","widget"]},"widget-uievents":{requires:["node-event-delegate","widget-base"]},yql:{requires:["oop"]},"yql-jsonp":{condition:{name:"yql-jsonp",test:function(e){return!e.UA.nodejs&&!e.UA.winjs},trigger:"yql"},requires:["yql","jsonp","jsonp-url"]},"yql-nodejs":{condition:{name:"yql-nodejs",trigger:"yql",ua:"nodejs"},requires:["yql"]},"yql-winjs":{condition:{name:"yql-winjs",trigger:"yql",ua:"winjs"},requires:["yql"]},yui:{},"yui-base":{},"yui-later":{requires:["yui-base"]},"yui-log":{requires:["yui-base"]},"yui-throttle":{requires:["yui-base"]}}),YUI.Env[e.version].md5="2fd2be6b12ee9f999b4367499ae61aae"},"3.18.1",{requires:["loader-base"]}),YUI.add("yui",function(e,t){},"3.18.1",{use:["yui-base","get","features","intl-base","yui-log","yui-later","loader-base","loader-rollup","loader-yui3"]}); -YUI.add("attribute-base",function(e,t){function n(){e.AttributeCore.apply(this,arguments),e.AttributeObservable.apply(this,arguments),e.AttributeExtras.apply(this,arguments)}e.mix(n,e.AttributeCore,!1,null,1),e.mix(n,e.AttributeExtras,!1,null,1),e.mix(n,e.AttributeObservable,!0,null,1),n.INVALID_VALUE=e.AttributeCore.INVALID_VALUE,n._ATTR_CFG=e.AttributeCore._ATTR_CFG.concat(e.AttributeObservable._ATTR_CFG),n.protectAttrs=e.AttributeCore.protectAttrs,e.Attribute=n},"3.18.1",{requires:["attribute-core","attribute-observable","attribute-extras"]}); -YUI.add("attribute-core",function(e,t){function b(e,t,n){this._yuievt=null,this._initAttrHost(e,t,n)}e.State=function(){this.data={}},e.State.prototype={add:function(e,t,n){var r=this.data[e];r||(r=this.data[e]={}),r[t]=n},addAll:function(e,t){var n=this.data[e],r;n||(n=this.data[e]={});for(r in t)t.hasOwnProperty(r)&&(n[r]=t[r])},remove:function(e,t){var n=this.data[e];n&&delete n[t]},removeAll:function(t,n){var r;n?e.each(n,function(e,n){this.remove(t,typeof n=="string"?n:e)},this):(r=this.data,t in r&&delete r[t])},get:function(e,t){var n=this.data[e];if(n)return n[t]},getAll:function(e,t){var n=this.data[e],r,i;if(t)i=n;else if(n){i={};for(r in n)n.hasOwnProperty(r)&&(i[r]=n[r])}return i}};var n=e.Object,r=e.Lang,i=".",s="getter",o="setter",u="readOnly",a="writeOnce",f="initOnly",l="validator",c="value",h="valueFn",p="lazyAdd",d="added",v="_bypassProxy",m="initValue",g="lazy",y;b.INVALID_VALUE={},y=b.INVALID_VALUE,b._ATTR_CFG=[o,s,l,c,h,a,u,p,v],b.protectAttrs=function(t){if(t){t=e.merge(t);for(var n in t)t.hasOwnProperty(n)&&(t[n]=e.merge(t[n]))}return t},b.prototype={_initAttrHost:function(t,n,r){this._state=new e.State,this._initAttrs(t,n,r)},addAttr:function(e,t,n){var r=this,i=r._state,s=i.data,o,u,a;t=t||{},p in t&&(n=t[p]),u=i.get(e,d);if(n&&!u)i.data[e]={lazy:t,added:!0};else if(!u||t.isLazyAdd)a=c in t,a&&(o=t.value,t.value=undefined),t.added=!0,t.initializing=!0,s[e]=t,a&&r.set(e,o),t.initializing=!1;return r},attrAdded:function(e){return!!this._state.get(e,d)},get:function(e){return this._getAttr(e)},_isLazyAttr:function(e){return this._state.get(e,g)},_addLazyAttr:function(e,t){var n=this._state;t=t||n.get(e,g),t&&(n.data[e].lazy=undefined,t.isLazyAdd=!0,this.addAttr(e,t))},set:function(e,t,n){return this._setAttr(e,t,n)},_set:function(e,t,n){return this._setAttr(e,t,n,!0)},_setAttr:function(t,r,s,o){var u=!0,a=this._state,l=this._stateProxy,c=this._tCfgs,h,p,d,v,m,g,y;return t.indexOf(i)!==-1&&(d=t,v=t.split(i),t=v.shift()),c&&c[t]&&this._addOutOfOrder(t,c[t]),h=a.data[t]||{},h.lazy&&(h=h.lazy,this._addLazyAttr(t,h)),p=h.value===undefined,l&&t in l&&!h._bypassProxy&&(p=!1),g=h.writeOnce,y=h.initializing,!p&&!o&&(g&&(u=!1),h.readOnly&&(u=!1)),!y&&!o&&g===f&&(u=!1),u&&(p||(m=this.get(t)),v&&(r=n.setValue(e.clone(m),v,r),r===undefined&&(u=!1)),u&&(!this._fireAttrChange||y?this._setAttrVal(t,d,m,r,s,h):this._fireAttrChange(t,d,m,r,s,h))),this},_addOutOfOrder:function(e,t){var n={};n[e]=t,delete this._tCfgs[e],this._addAttrs(n,this._tVals)},_getAttr:function(e){var t=e,r=this._tCfgs,s,o,u,a;return e.indexOf(i)!==-1&&(s=e.split(i),e=s.shift()),r&&r[e]&&this._addOutOfOrder(e,r[e]),a=this._state.data[e]||{},a.lazy&&(a=a.lazy,this._addLazyAttr(e,a)),u=this._getStateVal(e,a),o=a.getter,o&&!o.call&&(o=this[o]),u=o?o.call(this,u,t):u,u=s?n.getValue(u,s):u,u},_getStateVal:function(e,t){var n=this._stateProxy;return t||(t=this._state.getAll(e)||{}),n&&e in n&&!t._bypassProxy?n[e]:t.value},_setStateVal:function(e,t){var n=this._stateProxy;n&&e in n&&!this._state.get(e,v)?n[e]=t:this._state.add(e,c,t)},_setAttrVal:function(e,t,n,i,s,o){var u=this,a=!0,f=o||this._state.data[e]||{},l=f.validator,c=f.setter,h=f.initializing,p=this._getStateVal(e,f),d=t||e,v,g;return l&&(l.call||(l=this[l]),l&&(g=l.call(u,i,d,s),!g&&h&&(i=f.defaultValue,g=!0))),!l||g?(c&&(c.call||(c=this[c]),c&&(v=c.call(u,i,d,s),v===y?h?i=f.defaultValue:a=!1:v!==undefined&&(i=v))),a&&(!t&&i===p&&!r.isObject(i)?a=!1:(m in f||(f.initValue=i),u._setStateVal(e,i)))):a=!1,a},setAttrs:function(e,t){return this._setAttrs(e,t)},_setAttrs:function(e,t){var n;for(n in e)e.hasOwnProperty(n)&&this.set(n,e[n],t);return this},getAttrs:function(e){return this._getAttrs(e)},_getAttrs:function(e){var t={},r,i,s,o=e===!0;if(!e||o)e=n.keys(this._state.data);for(i=0,s=e.length;i=0;--u){n=e[u];for(t in n)n.hasOwnProperty(t)&&(s=d({},n[t],f),o=null,t.indexOf(i)!==-1&&(o=t.split(i),t=o.shift()),l=c[t],o&&l&&l.value?(r=c._subAttrs,r||(r=c._subAttrs={}),r[t]||(r[t]={}),r[t][o.join(i)]={value:s.value,path:o}):o||(l?(l.valueFn&&a in s&&(l.valueFn=null),d(l,s,f)):c[t]=s))}return c},_initHierarchy:function(e){var t=this._lazyAddAttrs,n,r,i,s,o,a,f,l,c,h,p,d=[],v=this._getClasses(),m=this._getAttrCfgs(),g=v.length-1;for(o=g;o>=0;o--){n=v[o],r=n.prototype,h=n._yuibuild&&n._yuibuild.exts,r.hasOwnProperty(u)&&(d[d.length]=r.initializer);if(h)for(a=0,f=h.length;a-1&&(t=r.getValue(n[e.selectedIndex]))),t}});var f,l,c;e.mix(e.DOM,{hasClass:function(t,n){var r=e.DOM._getRegExp("(?:^|\\s+)"+n+"(?:\\s+|$)");return r.test(t.className)},addClass:function(t,n){e.DOM.hasClass(t,n)||(t.className=e.Lang.trim([t.className,n].join(" ")))},removeClass:function(t,n){n&&l(t,n)&&(t.className=e.Lang.trim(t.className.replace(e.DOM._getRegExp("(?:^|\\s+)"+n+"(?:\\s+|$)")," ")),l(t,n)&&c(t,n))},replaceClass:function(e,t,n){c(e,t),f(e,n)},toggleClass:function(e,t,n){var r=n!==undefined?n:!l(e,t);r?f(e,t):c(e,t)}}),l=e.DOM.hasClass,c=e.DOM.removeClass,f=e.DOM.addClass;var h=/<([a-z]+)/i,r=e.DOM,u=e.Features.add,a=e.Features.test,p={},d=function(t,n){var r=e.config.doc.createElement("div"),i=!0;r.innerHTML=t;if(!r.firstChild||r.firstChild.tagName!==n.toUpperCase())i=!1;return i},v=/(?:\/(?:thead|tfoot|tbody|caption|col|colgroup)>)+\s*0&&(t.selectedIndex=y-1),a},wrap:function(t,n){var r=n&&n.nodeType?n:e.DOM.create(n),i=r.getElementsByTagName("*");i.length&&(r=i[i.length-1]),t.parentNode&&t.parentNode.replaceChild(r,t),r.appendChild(t)},unwrap:function(e){var t=e.parentNode,n=t.lastChild,r=e,i;if(t){i=t.parentNode;if(i){e=t.firstChild;while(e!==n)r=e.nextSibling,i.insertBefore(e,t),e=r;i.replaceChild(n,t)}else t.removeChild(e)}}}),u("innerhtml","table",{test:function(){var t=e.config.doc.createElement("table");try{t.innerHTML=""}catch(n){return!1}return t.firstChild&&t.firstChild.nodeName==="TBODY"}}),u("innerhtml-div","tr",{test:function(){return d("","tr")}}),u("innerhtml-div","script",{test:function(){return d("","script")}}),a("innerhtml","table")||(p.tbody=function(t,n){var i=r.create(m+t+g,n),s=e.DOM._children(i,"tbody")[0];return i.children.length>1&&s&&!v.test(t)&&s.parentNode.removeChild(s),i}),a("innerhtml-div","script")||(p.script=function(e,t){var n=t.createElement("div");return n.innerHTML="-"+e,n.removeChild(n.firstChild),n},p.link=p.style=p.script),a("innerhtml-div","tr")||(e.mix(p,{option:function(e,t){return r.create('",t)},tr:function(e,t){return r.create(""+e+"",t)},td:function(e,t){return r.create(""+e+"",t)},col:function(e,t){return r.create(""+e+"",t)},tbody:"table"}),e.mix(p,{legend:"fieldset" -,th:p.td,thead:p.tbody,tfoot:p.tbody,caption:p.tbody,colgroup:p.tbody,optgroup:p.option})),r.creators=p,e.mix(e.DOM,{setWidth:function(t,n){e.DOM._setSize(t,"width",n)},setHeight:function(t,n){e.DOM._setSize(t,"height",n)},_setSize:function(e,t,n){n=n>0?n:0;var r=0;e.style[t]=n+"px",r=t==="height"?e.offsetHeight:e.offsetWidth,r>n&&(n-=r-n,n<0&&(n=0),e.style[t]=n+"px")}})},"3.18.1",{requires:["dom-core"]}); -YUI.add("dom-core",function(e,t){var n="nodeType",r="ownerDocument",i="documentElement",s="defaultView",o="parentWindow",u="tagName",a="parentNode",f="previousSibling",l="nextSibling",c="contains",h="compareDocumentPosition",p=[],d=function(){var t=e.config.doc.createElement("div"),n=t.appendChild(e.config.doc.createTextNode("")),r=!1;try{r=t.contains(n)}catch(i){}return r}(),v={byId:function(e,t){return v.allById(e,t)[0]||null},getId:function(e){var t;return e.id&&!e.id.tagName&&!e.id.item?t=e.id:e.attributes&&e.attributes.id&&(t=e.attributes.id.value),t},setId:function(e,t){e.setAttribute?e.setAttribute("id",t):e.id=t},ancestor:function(e,t,n,r){var i=null;return n&&(i=!t||t(e)?e:null),i||v.elementByAxis(e,a,t,null,r)},ancestors:function(e,t,n,r){var i=e,s=[];while(i=v.ancestor(i,t,n,r)){n=!1;if(i){s.unshift(i);if(r&&r(i))return s}}return s},elementByAxis:function(e,t,n,r,i){while(e&&(e=e[t])){if((r||e[u])&&(!n||n(e)))return e;if(i&&i(e))return null}return null},contains:function(e,t){var r=!1;if(!t||!e||!t[n]||!e[n])r=!1;else if(e[c]&&(t[n]===1||d))r=e[c](t);else if(e[h]){if(e===t||!!(e[h](t)&16))r=!0}else r=v._bruteContains(e,t);return r},inDoc:function(e,t){var n=!1,s;return e&&e.nodeType&&(t||(t=e[r]),s=t[i],s&&s.contains&&e.tagName?n=s.contains(e):n=v.contains(s,e)),n},allById:function(t,n){n=n||e.config.doc;var r=[],i=[],s,o;if(n.querySelectorAll)i=n.querySelectorAll('[id="'+t+'"]');else if(n.all){r=n.all(t);if(r){r.nodeName&&(r.id===t?(i.push(r),r=p):r=[r]);if(r.length)for(s=0;o=r[s++];)(o.id===t||o.attributes&&o.attributes.id&&o.attributes.id.value===t)&&i.push(o)}}else i=[v._getDoc(n).getElementById(t)];return i},isWindow:function(e){return!!(e&&e.scrollTo&&e.document)},_removeChildNodes:function(e){while(e.firstChild)e.removeChild(e.firstChild)},siblings:function(e,t){var n=[],r=e;while(r=r[f])r[u]&&(!t||t(r))&&n.unshift(r);r=e;while(r=r[l])r[u]&&(!t||t(r))&&n.push(r);return n},_bruteContains:function(e,t){while(t){if(e===t)return!0;t=t.parentNode}return!1},_getRegExp:function(e,t){return t=t||"",v._regexCache=v._regexCache||{},v._regexCache[e+t]||(v._regexCache[e+t]=new RegExp(e,t)),v._regexCache[e+t]},_getDoc:function(t){var i=e.config.doc;return t&&(i=t[n]===9?t:t[r]||t.document||e.config.doc),i},_getWin:function(t){var n=v._getDoc(t);return n[s]||n[o]||e.config.win},_batch:function(e,t,n,r,i,s){t=typeof t=="string"?v[t]:t;var o,u=0,a,f;if(t&&e)while(a=e[u++])o=o=t.call(v,a,n,r,i,s),typeof o!="undefined"&&(f||(f=[]),f.push(o));return typeof f!="undefined"?f:e},generateID:function(t){var n=t.id;return n||(n=e.stamp(t),t.id=n),n}};e.DOM=v},"3.18.1",{requires:["oop","features"]}); -YUI.add("dom-screen",function(e,t){(function(e){var t="documentElement",n="compatMode",r="position",i="fixed",s="relative",o="left",u="top",a="BackCompat",f="medium",l="borderLeftWidth",c="borderTopWidth",h="getBoundingClientRect",p="getComputedStyle",d=e.DOM,v=/^t(?:able|d|h)$/i,m;e.UA.ie&&(e.config.doc[n]!=="BackCompat"?m=t:m="body"),e.mix(d,{winHeight:function(e){var t=d._getWinSize(e).height;return t},winWidth:function(e){var t=d._getWinSize(e).width;return t},docHeight:function(e){var t=d._getDocSize(e).height;return Math.max(t,d._getWinSize(e).height)},docWidth:function(e){var t=d._getDocSize(e).width;return Math.max(t,d._getWinSize(e).width)},docScrollX:function(n,r){r=r||n?d._getDoc(n):e.config.doc;var i=r.defaultView,s=i?i.pageXOffset:0;return Math.max(r[t].scrollLeft,r.body.scrollLeft,s)},docScrollY:function(n,r){r=r||n?d._getDoc(n):e.config.doc;var i=r.defaultView,s=i?i.pageYOffset:0;return Math.max(r[t].scrollTop,r.body.scrollTop,s)},getXY:function(){return e.config.doc[t][h]?function(r){var i=null,s,o,u,f,l,c,p,v,g,y;if(r&&r.tagName){p=r.ownerDocument,u=p[n],u!==a?y=p[t]:y=p.body,y.contains?g=y.contains(r):g=e.DOM.contains(y,r);if(g){v=p.defaultView,v&&"pageXOffset"in v?(s=v.pageXOffset,o=v.pageYOffset):(s=m?p[m].scrollLeft:d.docScrollX(r,p),o=m?p[m].scrollTop:d.docScrollY(r,p)),e.UA.ie&&(!p.documentMode||p.documentMode<8||u===a)&&(l=y.clientLeft,c=y.clientTop),f=r[h](),i=[f.left,f.top];if(l||c)i[0]-=l,i[1]-=c;if(o||s)if(!e.UA.ios||e.UA.ios>=4.2)i[0]+=s,i[1]+=o}else i=d._getOffset(r)}return i}:function(t){var n=null,s,o,u,a,f;if(t)if(d.inDoc(t)){n=[t.offsetLeft,t.offsetTop],s=t.ownerDocument,o=t,u=e.UA.gecko||e.UA.webkit>519?!0:!1;while(o=o.offsetParent)n[0]+=o.offsetLeft,n[1]+=o.offsetTop,u&&(n=d._calcBorders(o,n));if(d.getStyle(t,r)!=i){o=t;while(o=o.parentNode){a=o.scrollTop,f=o.scrollLeft,e.UA.gecko&&d.getStyle(o,"overflow")!=="visible"&&(n=d._calcBorders(o,n));if(a||f)n[0]-=f,n[1]-=a}n[0]+=d.docScrollX(t,s),n[1]+=d.docScrollY(t,s)}else n[0]+=d.docScrollX(t,s),n[1]+=d.docScrollY(t,s)}else n=d._getOffset(t);return n}}(),getScrollbarWidth:e.cached(function(){var t=e.config.doc,n=t.createElement("div"),r=t.getElementsByTagName("body")[0],i=.1;return r&&(n.style.cssText="position:absolute;visibility:hidden;overflow:scroll;width:20px;",n.appendChild(t.createElement("p")).style.height="1px",r.insertBefore(n,r.firstChild),i=n.offsetWidth-n.clientWidth,r.removeChild(n)),i},null,.1),getX:function(e){return d.getXY(e)[0]},getY:function(e){return d.getXY(e)[1]},setXY:function(e,t,n){var i=d.setStyle,a,f,l,c;e&&t&&(a=d.getStyle(e,r),f=d._getOffset(e),a=="static"&&(a=s,i(e,r,a)),c=d.getXY(e),t[0]!==null&&i(e,o,t[0]-c[0]+f[0]+"px"),t[1]!==null&&i(e,u,t[1]-c[1]+f[1]+"px"),n||(l=d.getXY(e),(l[0]!==t[0]||l[1]!==t[1])&&d.setXY(e,t,!0)))},setX:function(e,t){return d.setXY(e,[t,null])},setY:function(e,t){return d.setXY(e,[null,t])},swapXY:function(e,t){var n=d.getXY(e);d.setXY(e,d.getXY(t)),d.setXY(t,n)},_calcBorders:function(t,n){var r=parseInt(d[p](t,c),10)||0,i=parseInt(d[p](t,l),10)||0;return e.UA.gecko&&v.test(t.tagName)&&(r=0,i=0),n[0]+=i,n[1]+=r,n},_getWinSize:function(r,i){i=i||r?d._getDoc(r):e.config.doc;var s=i.defaultView||i.parentWindow,o=i[n],u=s.innerHeight,a=s.innerWidth,f=i[t];return o&&!e.UA.opera&&(o!="CSS1Compat"&&(f=i.body),u=f.clientHeight,a=f.clientWidth),{height:u,width:a}},_getDocSize:function(r){var i=r?d._getDoc(r):e.config.doc,s=i[t];return i[n]!="CSS1Compat"&&(s=i.body),{height:s.scrollHeight,width:s.scrollWidth}}})})(e),function(e){var t="top",n="right",r="bottom",i="left",s=function(e,s){var o=Math.max(e[t],s[t]),u=Math.min(e[n],s[n]),a=Math.min(e[r],s[r]),f=Math.max(e[i],s[i]),l={};return l[t]=o,l[n]=u,l[r]=a,l[i]=f,l},o=e.DOM;e.mix(o,{region:function(e){var t=o.getXY(e),n=!1;return e&&t&&(n=o._getRegion(t[1],t[0]+e.offsetWidth,t[1]+e.offsetHeight,t[0])),n},intersect:function(u,a,f){var l=f||o.region(u),c={},h=a,p;if(h.tagName)c=o.region(h);else{if(!e.Lang.isObject(a))return!1;c=a}return p=s(c,l),{top:p[t],right:p[n],bottom:p[r],left:p[i],area:(p[r]-p[t])*(p[n]-p[i]),yoff:p[r]-p[t],xoff:p[n]-p[i],inRegion:o.inRegion(u,a,!1,f)}},inRegion:function(u,a,f,l){var c={},h=l||o.region(u),p=a,d;if(p.tagName)c=o.region(p);else{if(!e.Lang.isObject(a))return!1;c=a}return f?h[i]>=c[i]&&h[n]<=c[n]&&h[t]>=c[t]&&h[r]<=c[r]:(d=s(c,h),d[r]>=d[t]&&d[n]>=d[i]?!0:!1)},inViewportRegion:function(e,t,n){return o.inRegion(e,o.viewportRegion(e),t,n)},_getRegion:function(e,s,o,u){var a={};return a[t]=a[1]=e,a[i]=a[0]=u,a[r]=o,a[n]=s,a.width=a[n]-a[i],a.height=a[r]-a[t],a},viewportRegion:function(t){t=t||e.config.doc.documentElement;var n=!1,r,i;return t&&(r=o.docScrollX(t),i=o.docScrollY(t),n=o._getRegion(i,o.winWidth(t)+r,i+o.winHeight(t),r)),n}})}(e)},"3.18.1",{requires:["dom-base","dom-style"]}); -YUI.add("dom-style",function(e,t){var n="documentElement",r="defaultView",i="ownerDocument",s="style",o="float",u="cssFloat",a="styleFloat",f="transparent",l="getComputedStyle",c="getBoundingClientRect",h=e.config.doc,p=e.DOM,d,v,m=["WebkitTransform","MozTransform","OTransform","msTransform","transform"],g=/width|height|top|left|right|bottom|margin|padding/i;e.Array.each(m,function(e){e in h[n].style&&(d=e,v=e+"Origin")}),e.mix(p,{DEFAULT_UNIT:"px",CUSTOM_STYLES:{},setStyle:function(e,t,n,r){r=r||e.style;var i=p.CUSTOM_STYLES;if(r){n===null||n===""?n="":!isNaN(Number(n))&&g.test(t)&&(n+=p.DEFAULT_UNIT);if(t in i){if(i[t].set){i[t].set(e,n,r);return}typeof i[t]=="string"&&(t=i[t])}else t===""&&(t="cssText",n="");r[t]=n}},getStyle:function(e,t,n){n=n||e.style;var r=p.CUSTOM_STYLES,i="";if(n){if(t in r){if(r[t].get)return r[t].get(e,t,n);typeof r[t]=="string"&&(t=r[t])}i=n[t],i===""&&(i=p[l](e,t))}return i},setStyles:function(t,n){var r=t.style;e.each(n,function(e,n){p.setStyle(t,n,e,r)},p)},getComputedStyle:function(e,t){var n="",o=e[i],u;return e[s]&&o[r]&&o[r][l]&&(u=o[r][l](e,null),u&&(n=u[t])),n}}),h[n][s][u]!==undefined?p.CUSTOM_STYLES[o]=u:h[n][s][a]!==undefined&&(p.CUSTOM_STYLES[o]=a),e.UA.webkit&&(p[l]=function(e,t){var n=e[i][r],s=n[l](e,"")[t];return s==="rgba(0, 0, 0, 0)"&&(s=f),s}),e.DOM._getAttrOffset=function(t,n){var r=e.DOM[l](t,n),i=t.offsetParent,s,o,u;return r==="auto"&&(s=e.DOM.getStyle(t,"position"),s==="static"||s==="relative"?r=0:i&&i[c]&&(o=i[c]()[n],u=t[c]()[n],n==="left"||n==="top"?r=u-o:r=o-t[c]()[n])),r},e.DOM._getOffset=function(e){var t,n=null;return e&&(t=p.getStyle(e,"position"),n=[parseInt(p[l](e,"left"),10),parseInt(p[l](e,"top"),10)],isNaN(n[0])&&(n[0]=parseInt(p.getStyle(e,"left"),10),isNaN(n[0])&&(n[0]=t==="relative"?0:e.offsetLeft||0)),isNaN(n[1])&&(n[1]=parseInt(p.getStyle(e,"top"),10),isNaN(n[1])&&(n[1]=t==="relative"?0:e.offsetTop||0))),n},d&&(p.CUSTOM_STYLES.transform={set:function(e,t,n){n[d]=t},get:function(e){return p[l](e,d)}},p.CUSTOM_STYLES.transformOrigin={set:function(e,t,n){n[v]=t},get:function(e){return p[l](e,v)}})},"3.18.1",{requires:["dom-base"]}); -YUI.add("event-base",function(e,t){e.publish("domready",{fireOnce:!0,async:!0}),YUI.Env.DOMReady?e.fire("domready"):e.Do.before(function(){e.fire("domready")},YUI.Env,"_ready");var n=e.UA,r={},i={63232:38,63233:40,63234:37,63235:39,63276:33,63277:34,25:9,63272:46,63273:36,63275:35},s=function(t){if(!t)return t;try{t&&3==t.nodeType&&(t=t.parentNode)}catch(n){return null}return e.one(t)},o=function(e,t,n){this._event=e,this._currentTarget=t,this._wrapper=n||r,this.init()};e.extend(o,Object,{init:function(){var e=this._event,t=this._wrapper.overrides,r=e.pageX,o=e.pageY,u,a=this._currentTarget;this.altKey=e.altKey,this.ctrlKey=e.ctrlKey,this.metaKey=e.metaKey,this.shiftKey=e.shiftKey,this.type=t&&t.type||e.type,this.clientX=e.clientX,this.clientY=e.clientY,this.pageX=r,this.pageY=o,u=e.keyCode||e.charCode,n.webkit&&u in i&&(u=i[u]),this.keyCode=u,this.charCode=u,this.which=e.which||e.charCode||u,this.button=this.which,this.target=s(e.target),this.currentTarget=s(a),this.relatedTarget=s(e.relatedTarget);if(e.type=="mousewheel"||e.type=="DOMMouseScroll")this.wheelDelta=e.detail?e.detail*-1:Math.round(e.wheelDelta/80)||(e.wheelDelta<0?-1:1);this._touch&&this._touch(e,a,this._wrapper)},stopPropagation:function(){this._event.stopPropagation(),this._wrapper.stopped=1,this.stopped=1},stopImmediatePropagation:function(){var e=this._event;e.stopImmediatePropagation?e.stopImmediatePropagation():this.stopPropagation(),this._wrapper.stopped=2,this.stopped=2},preventDefault:function(e){var t=this._event;t.preventDefault(),e&&(t.returnValue=e),this._wrapper.prevented=1,this.prevented=1},halt:function(e){e?this.stopImmediatePropagation():this.stopPropagation(),this.preventDefault()}}),o.resolve=s,e.DOM2EventFacade=o,e.DOMEventFacade=o,function(){e.Env.evt.dom_wrappers={},e.Env.evt.dom_map={};var t=e.Env.evt,n=e.config,r=n.win,i=YUI.Env.add,s=YUI.Env.remove,o=function(){YUI.Env.windowLoaded=!0,e.Event._load(),s(r,"load",o)},u=function(){e.Event._unload()},a="domready",f="~yui|2|compat~",l=function(t){try{return t&&typeof t!="string"&&e.Lang.isNumber(t.length)&&!t.tagName&&!e.DOM.isWindow(t)}catch(n){return!1}},c=e.CustomEvent.prototype._delete,h=function(t){var n=c.apply(this,arguments);return this.hasSubs()||e.Event._clean(this),n},p=function(){var n=!1,o=0,c=[],d=t.dom_wrappers,v=null,m=t.dom_map;return{POLL_RETRYS:1e3,POLL_INTERVAL:40,lastError:null,_interval:null,_dri:null,DOMReady:!1,startInterval:function(){p._interval||(p._interval=setInterval(p._poll,p.POLL_INTERVAL))},onAvailable:function(t,n,r,i,s,u){var a=e.Array(t),f,l;for(f=0;f4?t.slice(4):null),c&&u.fire(),h):!1},detach:function(t,n,r,i){var s=e.Array(arguments,0,!0),o,u,a,c,h,v;s[s.length-1]===f&&(o=!0);if(t&&t.detach)return t.detach();typeof r=="string"&&(o?r=e.DOM.byId(r):(r=e.Selector.query(r),u=r.length,u<1?r=null:u==1&&(r=r[0])));if(!r)return!1;if(r.detach)return s.splice(2,1),r.detach.apply(r,s);if(l(r)){a=!0;for(c=0,u=r.length;c0),u=[],a=function(t,n){var r,i=n.override;try{n.compat?(n.override?i===!0?r=n.obj:r=i:r=t,n.fn.call(r,n.obj)):(r=n.obj||e.one(t),n.fn.apply(r,e.Lang.isArray(i)?i:[]))}catch(s){}};for(t=0,r=c.length;t4?e.Array(arguments,4,!0):null;return e.Event.onAvailable.call(e.Event,r,n,i,s)}},e.Env.evt.plugins.contentready={on:function(t,n,r,i){var s=arguments.length>4?e.Array(arguments,4,!0):null;return e.Event.onContentReady.call(e.Event,r,n,i,s)}}},"3.18.1",{requires:["event-custom-base"]}); -YUI.add("event-custom-base",function(e,t){e.Env.evt={handles:{},plugins:{}};var n=0,r=1,i={objs:null,before:function(t,r,i,s){var o=t,u;return s&&(u=[t,s].concat(e.Array(arguments,4,!0)),o=e.rbind.apply(e,u)),this._inject(n,o,r,i)},after:function(t,n,i,s){var o=t,u;return s&&(u=[t,s].concat(e.Array(arguments,4,!0)),o=e.rbind.apply(e,u)),this._inject(r,o,n,i)},_inject:function(t,n,r,i){var s=e.stamp(r),o,u;return r._yuiaop||(r._yuiaop={}),o=r._yuiaop,o[i]||(o[i]=new e.Do.Method(r,i),r[i]=function(){return o[i].exec.apply(o[i],arguments)}),u=s+e.stamp(n)+i,o[i].register(u,n,t),new e.EventHandle(o[i],u)},detach:function(e){e.detach&&e.detach()}};e.Do=i,i.Method=function(e,t){this.obj=e,this.methodName=t,this.method=e[t],this.before={},this.after={}},i.Method.prototype.register=function(e,t,n){n?this.after[e]=t:this.before[e]=t},i.Method.prototype._delete=function(e){delete this.before[e],delete this.after[e]},i.Method.prototype.exec=function(){var t=e.Array(arguments,0,!0),n,r,s,o=this.before,u=this.after,a=!1;for(n in o)if(o.hasOwnProperty(n)){r=o[n].apply(this.obj,t);if(r)switch(r.constructor){case i.Halt:return r.retVal;case i.AlterArgs:t=r.newArgs;break;case i.Prevent:a=!0;break;default:}}a||(r=this.method.apply(this.obj,t)),i.originalRetVal=r,i.currentRetVal=r;for(n in u)if(u.hasOwnProperty(n)){s=u[n].apply(this.obj,t);if(s&&s.constructor===i.Halt)return s.retVal;s&&s.constructor===i.AlterReturn&&(r=s.newRetVal,i.currentRetVal=r)}return r},i.AlterArgs=function(e,t){this.msg=e,this.newArgs=t},i.AlterReturn=function(e,t){this.msg=e,this.newRetVal=t},i.Halt=function(e,t){this.msg=e,this.retVal=t},i.Prevent=function(e){this.msg=e},i.Error=i.Halt;var s=e.Array,o="after",u=["broadcast","monitored","bubbles","context","contextFn","currentTarget","defaultFn","defaultTargetOnly","details","emitFacade","fireOnce","async","host","preventable","preventedFn","queuable","silent","stoppedFn","target","type"],a=s.hash(u),f=Array.prototype.slice,l=9,c="yui:log",h=function(e,t,n){var r;for(r in t)a[r]&&(n||!(r in e))&&(e[r]=t[r]);return e};e.CustomEvent=function(t,n){this._kds=e.CustomEvent.keepDeprecatedSubs,this.id=e.guid(),this.type=t,this.silent=this.logSystem=t===c,this._kds&&(this.subscribers={},this.afters={}),n&&h(this,n,!0)},e.CustomEvent.keepDeprecatedSubs=!1,e.CustomEvent.mixConfigs=h,e.CustomEvent.prototype={constructor:e.CustomEvent,signature:l,context:e,preventable:!0,bubbles:!0,hasSubs:function(e){var t=0,n=0,r=this._subscribers,i=this._afters,s=this.sibling;return r&&(t=r.length),i&&(n=i.length),s&&(r=s._subscribers,i=s._afters,r&&(t+=r.length),i&&(n+=i.length)),e?e==="after"?n:t:t+n},monitor:function(e){this.monitored=!0;var t=this.id+"|"+this.type+"_"+e,n=f.call(arguments,0);return n[0]=t,this.host.on.apply(this.host,n)},getSubs:function(){var e=this.sibling,t=this._subscribers,n=this._afters,r,i;return e&&(r=e._subscribers,i=e._afters),r?t?t=t.concat(r):t=r.concat():t?t=t.concat():t=[],i?n?n=n.concat(i):n=i.concat():n?n=n.concat():n=[],[t,n]},applyConfig:function(e,t){h(this,e,t)},_on:function(t,n,r,i){var s=new e.Subscriber(t,n,r,i),u;return this.fireOnce&&this.fired&&(u=this.firedWith,this.emitFacade&&this._addFacadeToArgs&&this._addFacadeToArgs(u),this.async?setTimeout(e.bind(this._notify,this,s,u),0):this._notify(s,u)),i===o?(this._afters||(this._afters=[]),this._afters.push(s)):(this._subscribers||(this._subscribers=[]),this._subscribers.push(s)),this._kds&&(i===o?this.afters[s.id]=s:this.subscribers[s.id]=s),new e.EventHandle(this,s)},subscribe:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this._on(e,t,n,!0)},on:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this.monitored&&this.host&&this.host._monitor("attach",this,{args:arguments}),this._on(e,t,n,!0)},after:function(e,t){var n=arguments.length>2?f.call(arguments,2):null;return this._on(e,t,n,o)},detach:function(e,t){if(e&&e.detach)return e.detach();var n,r,i=0,s=this._subscribers,o=this._afters;if(s)for(n=s.length;n>=0;n--)r=s[n],r&&(!e||e===r.fn)&&(this._delete(r,s,n),i++);if(o)for(n=o.length;n>=0;n--)r=o[n],r&&(!e||e===r.fn)&&(this._delete(r,o,n),i++);return i},unsubscribe:function(){return this.detach.apply(this,arguments)},_notify:function(e,t,n){var r;return r=e.notify(t,this),!1===r||this.stopped>1?!1:!0},log:function(e,t){},fire:function(){var e=[];return e.push.apply(e,arguments),this._fire(e)},_fire:function(e){return this.fireOnce&&this.fired?!0:(this.fired=!0,this.fireOnce&&(this.firedWith=e),this.emitFacade?this.fireComplex(e):this.fireSimple(e))},fireSimple:function(e){this.stopped=0,this.prevented=0;if(this.hasSubs()){var t=this.getSubs();this._procSubs(t[0],e),this._procSubs(t[1],e)}return this.broadcast&&this._broadcast(e),this.stopped?!1:!0},fireComplex:function(e){return e[0]=e[0]||{},this.fireSimple(e)},_procSubs:function(e,t,n){var r,i,s;for(i=0,s=e.length;i-1?e:t+d+e},w=e.cached(function(e,t){var n=e,r,i,s;return p.isString(n)?(s=n.indexOf(m),s>-1&&(i=!0,n=n.substr(m.length)),s=n.indexOf(v),s>-1&&(r=n.substr(0,s),n=n.substr(s+1),n==="*"&&(n=null)),[r,t?b(n,t):n,i,n]):n}),E=function(t){var n=this._yuievt,r;n||(n=this._yuievt={events:{},targets:null,config:{host:this,context:this},chain:e.config.chain}),r=n.config,t&&(h(r,t,!0),t.chain!==undefined&&(n.chain=t.chain),t.prefix&&(r.prefix=t.prefix))};E.prototype={constructor:E,once:function(){var e=this.on.apply(this,arguments);return e.batch(function(e){e.sub&&(e.sub.once=!0)}),e},onceAfter:function(){var e=this.after.apply(this,arguments);return e.batch(function(e){e.sub&&(e.sub.once=!0)}),e},parseType:function(e,t){return w(e,t||this._yuievt.config.prefix)},on:function(t,n,r){var i=this._yuievt,s=w(t,i.config.prefix),o,u,a,l,c,h,d,v=e.Env.evt.handles,g,y,b,E=e.Node,S,x,T;this._monitor("attach",s[1],{args:arguments,category:s[0],after:s[2]});if(p.isObject(t))return p.isFunction(t)?e.Do.before.apply(e.Do,arguments):(o=n,u=r,a=f.call(arguments,0),l=[],p.isArray(t)&&(T=!0),g=t._after,delete t._after,e.each(t,function(e,t){p.isObject(e)&&(o=e.fn||(p.isFunction(e)?e:o),u=e.context||u);var n=g?m:"";a[0]=n+(T?e:t),a[1]=o,a[2]=u,l.push(this.on.apply(this,a))},this),i.chain?this:new e.EventHandle(l));h=s[0],g=s[2],b=s[3];if(E&&e.instanceOf(this,E)&&b in E.DOM_EVENTS)return a=f.call(arguments,0),a.splice(2,0,E.getDOMNode(this)),e.on.apply(e,a);t=s[1];if(e.instanceOf(this,YUI)){y=e.Env.evt.plugins[t],a=f.call(arguments,0),a[0]=b,E&&(S=a[2],e.instanceOf(S,e.NodeList)?S=e.NodeList.getDOMNodes(S):e.instanceOf(S,E)&&(S=E.getDOMNode(S)),x=b in E.DOM_EVENTS,x&&(a[2]=S));if(y)d=y.on.apply(e,a);else if(!t||x)d=e.Event._attach(a)}return d||(c=i.events[t]||this.publish(t),d=c._on(n,r,arguments.length>3?f.call(arguments,3):null,g?"after":!0),t.indexOf("*:")!==-1&&(this._hasSiblings=!0)),h&&(v[h]=v[h]||{},v[h][t]=v[h][t]||[],v[h][t].push(d)),i.chain?this:d},subscribe:function(){return this.on.apply(this,arguments)},detach:function(t,n,r){var i=this._yuievt.events,s,o=e.Node,u=o&&e.instanceOf(this,o);if(!t&&this!==e){for(s in i)i.hasOwnProperty(s)&&i[s].detach(n,r);return u&&e.Event.purgeElement(o.getDOMNode(this)),this}var a=w(t,this._yuievt.config.prefix),l=p.isArray(a)?a[0]:null,c=a?a[3]:null,h,d=e.Env.evt.handles,v,m,g,y,b=function(e,t,n){var r=e[t],i,s;if(r)for(s=r.length-1;s>=0;--s)i=r[s].evt,(i.host===n||i.el===n)&&r[s].detach()};if(l){m=d[l],t=a[1],v=u?e.Node.getDOMNode(this):this;if(m){if(t)b(m,t,v);else for(s in m)m.hasOwnProperty(s)&&b(m,s,v);return this}}else{if(p.isObject(t)&&t.detach)return t.detach(),this;if(u&&(!c||c in o.DOM_EVENTS))return g=f.call(arguments,0),g[2]=o.getDOMNode(this),e.detach.apply(e,g),this}h=e.Env.evt.plugins[c];if(e.instanceOf(this,YUI)){g=f.call(arguments,0);if(h&&h.detach)return h.detach.apply(e,g),this;if(!t||!h&&o&&t in o.DOM_EVENTS)return g[0]=t,e.Event.detach.apply(e.Event,g),this}return y=i[a[1]],y&&y.detach(n,r),this},unsubscribe:function(){return this.detach.apply(this,arguments)},detachAll:function(e){return this.detach(e)},unsubscribeAll:function(){return this.detachAll.apply(this,arguments)},publish:function(t,n){var r,i=this._yuievt,s=i.config,o=s.prefix;return typeof t=="string"?(o&&(t=b(t,o)),r=this._publish(t,s,n)):(r={},e.each(t,function(e,t){o&&(t=b(t,o)),r[t]=this._publish(t,s,e||n)},this)),r},_getFullType:function(e){var t=this._yuievt.config.prefix;return t?t+d+e:e},_publish:function(t,n,r){var i,s=this._yuievt,o=s.config,u=o.host,a=o.context,f=s.events;return i=f[t],(o.monitored&&!i||i&&i.monitored)&&this._monitor("publish",t,{args:arguments}),i||(i=f[t]=new e.CustomEvent(t,n),n||(i.host=u,i.context=a)),r&&h(i,r,!0),i},_monitor:function(e,t,n){var r,i,s;if(t){typeof t=="string"?(s=t,i=this.getEvent(t,!0)):(i=t,s=t.type);if(this._yuievt.config.monitored&&(!i||i.monitored)||i&&i.monitored)r=s+"_"+e,n.monitored=e,this.fire.call(this,r,n)}},fire:function(e){var t=typeof e=="string",n=arguments.length,r=e,i=this._yuievt,s=i.config,o=s.prefix,u,a,l,c;t&&n<=3?n===2?c=[arguments[1]]:n===3?c=[arguments[1],arguments[2]]:c=[]:c=f.call(arguments,t?1:0),t||(r=e&&e.type),o&&(r=b(r,o)),a=i.events[r],this._hasSiblings&&(l=this.getSibling(r,a),l&&!a&&(a=this.publish(r))),(s.monitored&&(!a||a.monitored)||a&&a.monitored)&&this._monitor("fire",a||r,{args:c});if(!a){if(i.hasTargets)return this.bubble({type:r},c,this);u=!0}else l&&(a.sibling=l),u=a._fire(c);return i.chain?this:u},getSibling:function(e,t){var n;return e.indexOf(d)>-1&&(e=y(e),n=this.getEvent(e,!0),n&&(n.applyConfig(t),n.bubbles=!1,n.broadcast=0)),n},getEvent:function(e,t){var n,r;return t||(n=this._yuievt.config.prefix,e=n?b(e,n):e),r=this._yuievt.events,r[e]||null},after:function(t,n){var r=f.call(arguments,0);switch(p.type(t)){case"function":return e.Do.after.apply(e.Do,arguments);case"array":case"object":r[0]._after=!0;break;default:r[0]=m+t}return this.on.apply(this,r)},before:function(){return this.on.apply -(this,arguments)}},e.EventTarget=E,e.mix(e,E.prototype),E.call(e,{bubbles:!1}),YUI.Env.globalEvents=YUI.Env.globalEvents||new E,e.Global=YUI.Env.globalEvents},"3.18.1",{requires:["oop"]}); -YUI.add("event-custom-complex",function(e,t){var n,r,i=e.Object,s,o={},u=e.CustomEvent.prototype,a=e.EventTarget.prototype,f=function(e,t){var n;for(n in t)r.hasOwnProperty(n)||(e[n]=t[n])};e.EventFacade=function(e,t){e||(e=o),this._event=e,this.details=e.details,this.type=e.type,this._type=e.type,this.target=e.target,this.currentTarget=t,this.relatedTarget=e.relatedTarget},e.mix(e.EventFacade.prototype,{stopPropagation:function(){this._event.stopPropagation(),this.stopped=1},stopImmediatePropagation:function(){this._event.stopImmediatePropagation(),this.stopped=2},preventDefault:function(){this._event.preventDefault(),this.prevented=1},halt:function(e){this._event.halt(e),this.prevented=1,this.stopped=e?2:1}}),u.fireComplex=function(t){var n,r,i,s,o,u=!0,a,f,l,c,h,p,d,v,m,g=this,y=g.host||g,b,w,E=g.stack,S=y._yuievt,x;if(E&&g.queuable&&g.type!==E.next.type)return E.queue||(E.queue=[]),E.queue.push([g,t]),!0;x=g.hasSubs()||S.hasTargets||g.broadcast,g.target=g.target||y,g.currentTarget=y,g.details=t.concat();if(x){n=E||{id:g.id,next:g,silent:g.silent,stopped:0,prevented:0,bubbling:null,type:g.type,defaultTargetOnly:g.defaultTargetOnly},f=g.getSubs(),l=f[0],c=f[1],g.stopped=g.type!==n.type?0:n.stopped,g.prevented=g.type!==n.type?0:n.prevented,g.stoppedFn&&(a=new e.EventTarget({fireOnce:!0,context:y}),g.events=a,a.on("stopped",g.stoppedFn)),g._facade=null,r=g._createFacade(t),l&&g._procSubs(l,t,r),g.bubbles&&y.bubble&&!g.stopped&&(w=n.bubbling,n.bubbling=g.type,n.type!==g.type&&(n.stopped=0,n.prevented=0),u=y.bubble(g,t,null,n),g.stopped=Math.max(g.stopped,n.stopped),g.prevented=Math.max(g.prevented,n.prevented),n.bubbling=w),d=g.prevented,d?(v=g.preventedFn,v&&v.apply(y,t)):(m=g.defaultFn,m&&(!g.defaultTargetOnly&&!n.defaultTargetOnly||y===r.target)&&m.apply(y,t)),g.broadcast&&g._broadcast(t);if(c&&!g.prevented&&g.stopped<2){h=n.afterQueue;if(n.id===g.id||g.type!==S.bubbling){g._procSubs(c,t,r);if(h)while(b=h.last())b()}else p=c,n.execDefaultCnt&&(p=e.merge(p),e.each(p,function(e){e.postponed=!0})),h||(n.afterQueue=new e.Queue),n.afterQueue.add(function(){g._procSubs(p,t,r)})}g.target=null;if(n.id===g.id){s=n.queue;if(s)while(s.length)i=s.pop(),o=i[0],n.next=o,o._fire(i[1]);g.stack=null}u=!g.stopped,g.type!==S.bubbling&&(n.stopped=0,n.prevented=0,g.stopped=0,g.prevented=0)}else m=g.defaultFn,m&&(r=g._createFacade(t),(!g.defaultTargetOnly||y===r.target)&&m.apply(y,t));return g._facade=null,u},u._hasPotentialSubscribers=function(){return this.hasSubs()||this.host._yuievt.hasTargets||this.broadcast},u._createFacade=u._getFacade=function(t){var n=this.details,r=n&&n[0],i=r&&typeof r=="object",s=this._facade;return s||(s=new e.EventFacade(this,this.currentTarget)),i?(f(s,r),r.type&&(s.type=r.type),t&&(t[0]=s)):t&&t.unshift(s),s.details=this.details,s.target=this.originalTarget||this.target,s.currentTarget=this.currentTarget,s.stopped=0,s.prevented=0,this._facade=s,this._facade},u._addFacadeToArgs=function(e){var t=e[0];t&&t.halt&&t.stopImmediatePropagation&&t.stopPropagation&&t._event||this._createFacade(e)},u.stopPropagation=function(){this.stopped=1,this.stack&&(this.stack.stopped=1),this.events&&this.events.fire("stopped",this)},u.stopImmediatePropagation=function(){this.stopped=2,this.stack&&(this.stack.stopped=2),this.events&&this.events.fire("stopped",this)},u.preventDefault=function(){this.preventable&&(this.prevented=1,this.stack&&(this.stack.prevented=1))},u.halt=function(e){e?this.stopImmediatePropagation():this.stopPropagation(),this.preventDefault()},a.addTarget=function(t){var n=this._yuievt;return n.targets||(n.targets={}),n.targets[e.stamp(t)]=t,n.hasTargets=!0,this},a.getTargets=function(){var e=this._yuievt.targets;return e?i.values(e):[]},a.removeTarget=function(t){var n=this._yuievt.targets;return n&&(delete n[e.stamp(t,!0)],i.size(n)===0&&(this._yuievt.hasTargets=!1)),this},a.bubble=function(e,t,n,r){var i=this._yuievt.targets,s=!0,o,u,a,f,l,c=e&&e.type,h=n||e&&e.target||this,p;if(!e||!e.stopped&&i)for(a in i)if(i.hasOwnProperty(a)){o=i[a],u=o._yuievt.events[c],o._hasSiblings&&(l=o.getSibling(c,u)),l&&!u&&(u=o.publish(c)),p=o._yuievt.bubbling,o._yuievt.bubbling=c;if(!u)o._yuievt.hasTargets&&o.bubble(e,t,h,r);else{l&&(u.sibling=l),u.target=h,u.originalTarget=h,u.currentTarget=o,f=u.broadcast,u.broadcast=!1,u.emitFacade=!0,u.stack=r,s=s&&u.fire.apply(u,t||e.details||[]),u.broadcast=f,u.originalTarget=null;if(u.stopped)break}o._yuievt.bubbling=p}return s},a._hasPotentialSubscribers=function(e){var t=this._yuievt,n=t.events[e];return n?n.hasSubs()||t.hasTargets||n.broadcast:!1},n=new e.EventFacade,r={};for(s in n)r[s]=!0},"3.18.1",{requires:["event-custom-base"]}); -YUI.add("event-delegate",function(e,t){function f(t,r,u,l){var c=n(arguments,0,!0),h=i(u)?u:null,p,d,v,m,g,y,b,w,E;if(s(t)){w=[];if(o(t))for(y=0,b=t.length;y1&&(g=p.shift(),c[0]=t=p.shift()),d=e.Node.DOM_EVENTS[t],s(d)&&d.delegate&&(E=d.delegate.apply(d,arguments));if(!E){if(!t||!r||!u||!l)return;v=h?e.Selector.query(h,null,!0):u,!v&&i(u)&&(E=e.on("available",function(){e.mix(E,e.delegate.apply(e,c),!0)},u)),!E&&v&&(c.splice(2,2,v),E=e.Event._attach(c,{facade:!1}),E.sub.filter=l,E.sub._notify=f.notifySub)}return E&&g&&(m=a[g]||(a[g]={}),m=m[t]||(m[t]=[]),m.push(E)),E}var n=e.Array,r=e.Lang,i=r.isString,s=r.isObject,o=r.isArray,u=e.Selector.test,a=e.Env.evt.handles;f.notifySub=function(t,r,i){r=r.slice(),this.args&&r.push.apply(r,this.args);var s=f._applyFilter(this.filter,r,i),o,u,a,l;if(s){s=n(s),o=r[0]=new e.DOMEventFacade(r[0],i.el,i),o.container=e.one(i.el);for(u=0,a=s.length;u3?e.merge(t.splice(3,1)[0]):{};return a in n||(n[a]=this.MIN_VELOCITY),f in n||(n[f]=this.MIN_DISTANCE),l in n||(n[l]=this.PREVENT_DEFAULT),n},_onStart:function(t,n,i,a){var f=!0,l,h,m,g=i._extra.preventDefault,y=t;t.touches&&(f=t.touches.length===1,t=t.touches[0]),f&&(g&&(!g.call||g(t))&&y.preventDefault(),t.flick={time:(new Date).getTime()},i[c]=t,l=i[p],m=n.get(v)===9?n:n.get(u),l||(l=m.on(r[s],e.bind(this._onEnd,this),null,n,i,a),i[p]=l),i[d]=m.once(r[o],e.bind(this._onMove,this),null,n,i,a))},_onMove:function(e,t,n,r){var i=n[c];i&&i.flick&&(i.flick.time=(new Date).getTime())},_onEnd:function(e,t,n,r){var i=(new Date).getTime(),s=n[c],o=!!s,u=e,h,p,v,m,g,y,b,w,E=n[d];E&&(E.detach(),delete n[d]),o&&(e.changedTouches&&(e.changedTouches.length===1&&e.touches.length===0?u=e.changedTouches[0]:o=!1),o&&(m=n._extra,v=m[l],v&&(!v.call||v(e))&&e.preventDefault(),h=s.flick.time,i=(new Date).getTime(),p=i-h,g=[u.pageX-s.pageX,u.pageY-s.pageY],m.axis?w=m.axis:w=Math.abs(g[0])>=Math.abs(g[1])?"x":"y",y=g[w==="x"?0:1],b=p!==0?y/p:0,isFinite(b)&&Math.abs(y)>=m[f]&&Math.abs(b)>=m[a]&&(e.type="flick",e.flick={time:p,distance:y,velocity:b,axis:w,start:s},r.fire(e)),n[c]=null))},MIN_VELOCITY:0,MIN_DISTANCE:0,PREVENT_DEFAULT:!1})},"3.18.1",{requires:["node-base","event-touch","event-synthetic"]}); -YUI.add("event-focus",function(e,t){function u(t,r,u){var a="_"+t+"Notifiers";e.Event.define(t,{_useActivate:o,_attach:function(i,s,o){return e.DOM.isWindow(i)?n._attach([t,function(e){s.fire(e)},i]):n._attach([r,this._proxy,i,this,s,o],{capture:!0})},_proxy:function(t,r,i){var s=t.target,f=t.currentTarget,l=s.getData(a),c=e.stamp(f._node),h=o||s!==f,p;r.currentTarget=i?s:f,r.container=i?f:null,l?h=!0:(l={},s.setData(a,l),h&&(p=n._attach([u,this._notify,s._node]).sub,p.once=!0)),l[c]||(l[c]=[]),l[c].push(r),h||this._notify(t)},_notify:function(t,n){var r=t.currentTarget,i=r.getData(a),o=r.ancestors(),u=r.get("ownerDocument"),f=[],l=i?e.Object.keys(i).length:0,c,h,p,d,v,m,g,y,b,w;r.clearData(a),o.push(r),u&&o.unshift(u),o._nodes.reverse(),l&&(m=l,o.some(function(t){var n=e.stamp(t),r=i[n],s,o;if(r){l--;for(s=0,o=r.length;s=0;--l){u=o(s[l]);if(!u)continue;+u==u?i.keys[u]=r:(f=u.toLowerCase(),this.KEY_MAP[f]?(i.keys[this.KEY_MAP[f]]=r,i.type||(i.type="down")):(u=u.charAt(0),a=u.toUpperCase(),r["+shift"]&&(u=a),i.keys[u.charCodeAt(0)]=u===a?e.merge(r,{"+shift":!0}):r))}}return i.type||(i.type="press"),i},on:function(e,t,o,u){var a=t._extra,f="key"+a.type,l=a.keys,c=u?"delegate":"on";t._detach=e[c](f,function(e){var t=l?l[e.which]:a.mods;t&&(!t[n]||t[n]&&e.altKey)&&(!t[r]||t[r]&&e.ctrlKey)&&(!t[i]||t[i]&&e.metaKey)&&(!t[s]||t[s]&&e.shiftKey)&&o.fire(e)},u)},detach:function(e,t,n){t._detach.detach()}};u.delegate=u.on,u.detachDelegate=u.detach,e.Event.define("key",u,!0)},"3.18.1",{requires:["event-synthetic"]}); -YUI.add("event-mousewheel",function(e,t){var n="DOMMouseScroll",r=function(t){var r=e.Array(t,0,!0),i;return e.UA.gecko?(r[0]=n,i=e.config.win):i=e.config.doc,r.length<3?r[2]=i:r.splice(2,0,i),r};e.Env.evt.plugins.mousewheel={on:function(){return e.Event._attach(r(arguments))},detach:function(){return e.Event.detach.apply(e.Event,r(arguments))}}},"3.18.1",{requires:["node-base"]}); -YUI.add("event-mouseenter",function(e,t){var n=e.Env.evt.dom_wrappers,r=e.DOM.contains,i=e.Array,s=function(){},o={proxyType:"mouseover",relProperty:"fromElement",_notify:function(t,i,s){var o=this._node,u=t.relatedTarget||t[i];o!==u&&!r(o,u)&&s.fire(new e.DOMEventFacade(t,o,n["event:"+e.stamp(o)+t.type]))},on:function(t,n,r){var i=e.Node.getDOMNode(t),s=[this.proxyType,this._notify,i,null,this.relProperty,r];n.handle=e.Event._attach(s,{facade:!1})},detach:function(e,t){t.handle.detach()},delegate:function(t,n,r,i){var o=e.Node.getDOMNode(t),u=[this.proxyType,s,o,null,r];n.handle=e.Event._attach(u,{facade:!1}),n.handle.sub.filter=i,n.handle.sub.relProperty=this.relProperty,n.handle.sub._notify=this._filterNotify},_filterNotify:function(t,n,s){n=n.slice(),this.args&&n.push.apply(n,this.args);var o=e.delegate._applyFilter(this.filter,n,s),u=n[0].relatedTarget||n[0][this.relProperty],a,f,l,c,h;if(o){o=i(o);for(f=0,l=o.length&&(!a||!a.stopped);fi?e.merge(n.splice(i,1)[0]):{};return w in s||(s[w]=t.PREVENT_DEFAULT),s},_=function(e,t){return t._extra.root||e.get(N)===9?e:e.get(S)},D=function(t){var n=t.getDOMNode();return t.compareTo(e.config.doc)&&n.documentElement?n.documentElement:!1},P=function(e,t,n){e.pageX=t.pageX,e.pageY=t.pageY,e.screenX=t.screenX,e.screenY=t.screenY,e.clientX=t.clientX,e.clientY=t.clientY,e[T]=e[T]||t[T],e[x]=e[x]||t[x],e[E]=n&&n[E]||1},H=function(t){var n=t.getDOMNode(),r=t.getData(A);L&&(r||(r=0,t.setData(O,n.style[k])),n.style[k]=e.Event._DEFAULT_TOUCH_ACTION,r++,t.setData(A,r))},B=function(e){var t=e.getDOMNode(),n=e.getData(A),r=e.getData(O);L&&(n--,e.setData(A,n),n===0&&t.style[k]!==r&&(t.style[k]=r))},j=function(e,t){t&&(!t.call||t(e))&&e.preventDefault()},F=e.Event.define;e.Event._DEFAULT_TOUCH_ACTION="none",F(f,{on:function(e,t,n){D(e)||H(e),t[l]=e.on(r[i],this._onStart,this,e,t,n)},delegate:function(e,t,n,s){var o=this;t[p]=e.delegate(r[i],function(r){o._onStart(r,e,t,n,!0)},s)},detachDelegate:function(e,t,n,r){var i=t[p];i&&(i.detach(),t[p]=null),D(e)||B(e)},detach:function(e,t,n){var r=t[l];r&&(r.detach(),t[l]=null),D(e)||B(e)},processArgs:function(e,t){var n=M(this,e,t);return y in n||(n[y]=this.MIN_TIME),b in n||(n[b]=this.MIN_DISTANCE),n},_onStart:function(t,n,i,u,a){a&&(n=t[x]);var f=i._extra,l=!0,c=f[y],h=f[b],p=f.button,d=f[w],v=_(n,i),m;t.touches?t.touches.length===1?P(t,t.touches[0],f):l=!1:l=p===undefined||p===t.button,l&&(j(t,d),c===0||h===0?this._start(t,n,u,f):(m=[t.pageX,t.pageY],c>0&&(f._ht=e.later(c,this,this._start,[t,n,u,f]),f._hme=v.on(r[o],e.bind(function(){this._cancel(f)},this))),h>0&&(f._hm=v.on(r[s],e.bind(function(e){(Math.abs(e.pageX-m[0])>h||Math.abs(e.pageY-m[1])>h)&&this._start(t,n,u,f)},this)))))},_cancel:function(e){e._ht&&(e._ht.cancel(),e._ht=null),e._hme&&(e._hme.detach(),e._hme=null),e._hm&&(e._hm.detach(),e._hm=null)},_start:function(e,t,n,r){r&&this._cancel(r),e.type=f,t.setData(m,e),n.fire(e)},MIN_TIME:0,MIN_DISTANCE:0,PREVENT_DEFAULT:!1}),F(u,{on:function(e,t,n){var i,o;D(e)||H(e),o=_(e,t,r[s]),i=o.on(r[s],this._onMove,this,e,t,n),t[c]=i},delegate:function(e,t,n,i){var o=this;t[d]=e.delegate(r[s],function(r){o._onMove(r,e,t,n,!0)},i)},detach:function(e,t,n){var r=t[c];r&&(r.detach(),t[c]=null),D(e)||B(e)},detachDelegate:function(e,t,n,r){var i=t[d];i&&(i.detach(),t[d]=null),D(e)||B(e)},processArgs:function(e,t){return M(this,e,t)},_onMove:function(e,t,n,r,i){i&&(t=e[x]);var s=n._extra.standAlone||t.getData(m),o=n._extra.preventDefault;s&&(e.touches&&(e.touches.length===1?P(e,e.touches[0]):s=!1),s&&(j(e,o),e.type=u,r.fire(e)))},PREVENT_DEFAULT:!1}),F(a,{on:function(e,t,n){var i,s;D(e)||H(e),s=_(e,t),i=s.on(r[o],this._onEnd,this,e,t,n),t[h]=i},delegate:function(e,t,n,i){var s=this;t[v]=e.delegate(r[o],function(r){s._onEnd(r,e,t,n,!0)},i)},detachDelegate:function(e,t,n,r){var i=t[v];i&&(i.detach(),t[v]=null),D(e)||B(e)},detach:function(e,t,n){var r=t[h];r&&(r.detach(),t[h]=null),D(e)||B(e)},processArgs:function(e,t){return M(this,e,t)},_onEnd:function(e,t,n,r,i){i&&(t=e[x]);var s=n._extra.standAlone||t.getData(g)||t.getData(m),o=n._extra.preventDefault;s&&(e.changedTouches&&(e.changedTouches.length===1?P(e,e.changedTouches[0]):s=!1),s&&(j(e,o),e.type=a,r.fire(e),t.clearData(m),t.clearData(g)))},PREVENT_DEFAULT:!1})},"3.18.1",{requires:["node-base","event-touch","event-synthetic"]}); -YUI.add("event-outside",function(e,t){var n=["blur","change","click","dblclick","focus","keydown","keypress","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","select","submit"];e.Event.defineOutside=function(t,n){n=n||t+"outside";var r={on:function(n,r,i){r.handle=e.one("doc").on(t,function(e){this.isOutside(n,e.target)&&(e.currentTarget=n,i.fire(e))},this)},detach:function(e,t,n){t.handle.detach()},delegate:function(n,r,i,s){r.handle=e.one("doc").delegate(t,function(e){this.isOutside(n,e.target)&&i.fire(e)},s,this)},isOutside:function(e,t){return t!==e&&!t.ancestor(function(t){return t===e})}};r.detachDelegate=r.detach,e.Event.define(n,r)},e.Array.each(n,function(t){e.Event.defineOutside(t)})},"3.18.1",{requires:["event-synthetic"]}); -YUI.add("event-resize",function(e,t){e.Event.define("windowresize",{on:e.UA.gecko&&e.UA.gecko<1.91?function(t,n,r){n._handle=e.Event.attach("resize",function(e){r.fire(e)})}:function(t,n,r){var i=e.config.windowResizeDelay||100;n._handle=e.Event.attach("resize",function(t){n._timer&&n._timer.cancel(),n._timer=e.later(i,e,function(){r.fire(t)})})},detach:function(e,t){t._timer&&t._timer.cancel(),t._handle.detach()}})},"3.18.1",{requires:["node-base","event-synthetic"]}); -YUI.add("event-synthetic",function(e,t){function c(e,t){this.handle=e,this.emitFacade=t}function h(e,t,n){this.handles=[],this.el=e,this.key=n,this.domkey=t}function p(){this._init.apply(this,arguments)}var n=e.CustomEvent,r=e.Env.evt.dom_map,i=e.Array,s=e.Lang,o=s.isObject,u=s.isString,a=s.isArray,f=e.Selector.query,l=function(){};c.prototype.fire=function(t){var n=i(arguments,0,!0),r=this.handle,s=r.evt,u=r.sub,a=u.context,f=u.filter,l=t||{},c;if(this.emitFacade){if(!t||!t.preventDefault)l=s._getFacade(),o(t)&&!t.preventDefault?(e.mix(l,t,!0),n[0]=l):n.unshift(l);l.type=s.type,l.details=n.slice(),f&&(l.container=s.host)}else f&&o(t)&&t.currentTarget&&n.shift();return u.context=a||l.currentTarget||s.host,c=s.fire.apply(s,n),t.prevented&&s.preventedFn&&s.preventedFn.apply(s,n),t.stopped&&s.stoppedFn&&s.stoppedFn.apply(s,n),u.context=a,c},h.prototype={constructor:h,type:"_synth",fn:l,capture:!1,register:function(e){e.evt.registry=this,this.handles.push(e)},unregister:function(t){var n=this.handles,i=r[this.domkey],s;for(s=n.length-1;s>=0;--s)if(n[s].sub===t){n.splice(s,1);break}n.length||(delete i[this.key],e.Object.size(i)||delete r[this.domkey])},detachAll:function(){var e=this.handles,t=e.length;while(--t>=0)e[t].detach()}},e.mix(p,{Notifier:c,SynthRegistry:h,getRegistry:function(t,n,i){var s=t._node,o=e.stamp(s),u="event:"+o+n+"_synth",a=r[o];return i&&(a||(a=r[o]={}),a[u]||(a[u]=new h(s,o,u))),a&&a[u]||null},_deleteSub:function(e){if(e&&e.fn){var t=this.eventDef,r=e.filter?"detachDelegate":"detach";this._subscribers=[],n.keepDeprecatedSubs&&(this.subscribers={}),t[r](e.node,e,this.notifier,e.filter),this.registry.unregister(e),delete e.fn,delete e.node,delete e.context}},prototype:{constructor:p,_init:function(){var e=this.publishConfig||(this.publishConfig={});this.emitFacade="emitFacade"in e?e.emitFacade:!0,e.emitFacade=!1},processArgs:l,on:l,detach:l,delegate:l,detachDelegate:l,_on:function(t,n){var r=[],s=t.slice(),o=this.processArgs(t,n),a=t[2],l=n?"delegate":"on",c,h;return c=u(a)?f(a):i(a||e.one(e.config.win)),!c.length&&u(a)?(h=e.on("available",function(){e.mix(h,e[l].apply(e,s),!0)},a),h):(e.Array.each(c,function(i){var s=t.slice(),u;i=e.one(i),i&&(n&&(u=s.splice(3,1)[0]),s.splice(0,4,s[1],s[3]),(!this.preventDups||!this.getSubs(i,t,null,!0))&&r.push(this._subscribe(i,l,s,o,u)))},this),r.length===1?r[0]:new e.EventHandle(r))},_subscribe:function(t,n,r,i,s){var o=new e.CustomEvent(this.type,this.publishConfig),u=o.on.apply(o,r),a=new c(u,this.emitFacade),f=p.getRegistry(t,this.type,!0),l=u.sub;return l.node=t,l.filter=s,i&&this.applyArgExtras(i,l),e.mix(o,{eventDef:this,notifier:a,host:t,currentTarget:t,target:t,el:t._node,_delete:p._deleteSub},!0),u.notifier=a,f.register(u),this[n](t,l,a,s),u},applyArgExtras:function(e,t){t._extra=e},_detach:function(t){var n=t[2],r=u(n)?f(n):i(n),s,o,a,l,c;t.splice(2,1);for(o=0,a=r.length;o=0;--c)l[c].detach()}}},getSubs:function(e,t,n,r){var i=p.getRegistry(e,this.type),s=[],o,u,a,f;if(i){o=i.handles,n||(n=this.subMatch);for(u=0,a=o.length;u=0&&(h=n._extra.sensitivity),e.changedTouches?(l=[e.changedTouches[0].pageX,e.changedTouches[0].pageY],c=[e.changedTouches[0].clientX,e.changedTouches[0].clientY]):(l=[e.pageX,e.pageY],c=[e.clientX,e.clientY]),Math.abs(l[0]-f[0])<=h&&Math.abs(l[1]-f[1])<=h&&(e.type=s,e.pageX=l[0],e.pageY=l[1],e.clientX=c[0],e.clientY=c[1],e.currentTarget=o.node,r.fire(e)),a(n,[u.END,u.CANCEL])}})},"3.18.1",{requires:["node-base","event-base","event-touch","event-synthetic"]}); -YUI.add("event-touch",function(e,t){var n="scale",r="rotation",i="identifier",s=e.config.win,o={};e.DOMEventFacade.prototype._touch=function(t,s,o){var u,a,f,l,c;if(t.touches){this.touches=[],c={};for(u=0,a=t.touches.length;ua&&(a=n,a===1&&(d=t.el));if(a===2)return!0}),o._refreshTimeout(t))},_refreshTimeout:function(e,t){if(!e._node)return;var r=e.getData(n);o._stopTimeout(e),r.timeout=setTimeout(function(){o._stopPolling(e,t)},o.TIMEOUT)},_startPolling:function(t,s,u){var a,f;if(!t.test("input,textarea,select")&&!(f=o._isEditable(t)))return;a=t.getData(n),a||(a={nodeName:t.get(i).toLowerCase(),isEditable:f,prevVal:f?t.getDOMNode().innerHTML:t.get(r)},t.setData(n,a)),a.notifiers||(a.notifiers={});if(a.interval){if(!u.force){a.notifiers[e.stamp(s)]=s;return}o._stopPolling(t,s)}a.notifiers[e.stamp(s)]=s,a.interval=setInterval(function(){o._poll(t,u)},o.POLL_INTERVAL),o._refreshTimeout(t,s)},_stopPolling:function(t,r){if(!t._node)return;var i=t.getData(n)||{};clearInterval(i.interval),delete i.interval,o._stopTimeout(t),r?i.notifiers&&delete i.notifiers[e.stamp(r)]:i.notifiers={}},_stopTimeout:function(e){var t=e.getData(n)||{};clearTimeout(t.timeout),delete t.timeout},_isEditable:function(e){var t=e._node;return t.contentEditable==="true"||t.contentEditable===""},_onBlur:function(e,t){o._stopPolling(e.currentTarget,t)},_onFocus:function(e,t){var s=e.currentTarget,u=s.getData(n);u||(u={isEditable:o._isEditable(s),nodeName:s.get(i).toLowerCase()},s.setData(n,u)),u.prevVal=u.isEditable?s.getDOMNode().innerHTML:s.get(r),o._startPolling(s,t,{e:e})},_onKeyDown:function(e,t){o._startPolling(e.currentTarget,t,{e:e})},_onKeyUp:function(e,t){(e.charCode===229||e.charCode===197)&&o._startPolling(e.currentTarget,t,{e:e,force:!0})},_onMouseDown:function(e,t){o._startPolling(e.currentTarget,t,{e:e})},_onSubscribe:function(t,s,u,a){var f,l,c,h,p;l={blur:o._onBlur,focus:o._onFocus,keydown:o._onKeyDown,keyup:o._onKeyUp,mousedown:o._onMouseDown},f=u._valuechange={};if(a)f.delegated=!0,f.getNodes=function(){return h=t.all("input,textarea,select").filter(a),p=t.all('[contenteditable="true"],[contenteditable=""]').filter(a),h.concat(p)},f.getNodes().each(function(e){e.getData(n)||e.setData(n,{nodeName:e.get(i).toLowerCase(),isEditable:o._isEditable(e),prevVal:c?e.getDOMNode().innerHTML:e.get(r)})}),u._handles=e.delegate(l,t,a,null,u);else{c=o._isEditable(t);if(!t.test("input,textarea,select")&&!c)return;t.getData(n)||t.setData(n,{nodeName:t.get(i).toLowerCase(),isEditable:c,prevVal:c?t.getDOMNode().innerHTML:t.get(r)}),u._handles=t.on(l,null,null,u)}},_onUnsubscribe:function(e,t,n){var r=n._valuechange;n._handles&&n._handles.detach(),r.delegated?r.getNodes().each(function(e){o._stopPolling(e,n)}):o._stopPolling(e,n)}};s={detach:o._onUnsubscribe,on:o._onSubscribe,delegate:o._onSubscribe,detachDelegate:o._onUnsubscribe,publishConfig:{emitFacade:!0}},e.Event.define("valuechange",s),e.Event.define("valueChange",s),e.ValueChange=o},"3.18.1",{requires:["event-focus","event-synthetic"]}); -YUI.add("node-base",function(e,t){var n=["hasClass","addClass","removeClass","replaceClass","toggleClass"];e.Node.importMethod(e.DOM,n),e.NodeList.importMethod(e.Node.prototype,n);var r=e.Node,i=e.DOM;r.create=function(t,n){return n&&n._node&&(n=n._node),e.one(i.create(t,n))},e.mix(r.prototype,{create:r.create,insert:function(e,t){return this._insert(e,t),this},_insert:function(e,t){var n=this._node,r=null;return typeof t=="number"?t=this._node.childNodes[t]:t&&t._node&&(t=t._node),e&&typeof e!="string"&&(e=e._node||e._nodes||e),r=i.addHTML(n,e,t),r},prepend:function(e){return this.insert(e,0)},append:function(e){return this.insert(e,null)},appendChild:function(e){return r.scrubVal(this._insert(e))},insertBefore:function(t,n){return e.Node.scrubVal(this._insert(t,n))},appendTo:function(t){return e.one(t).append(this),this},setContent:function(e){return this._insert(e,"replace"),this},getContent:function(){var e=this;return e._node.nodeType===11&&(e=e.create("
").append(e.cloneNode(!0))),e.get("innerHTML")}}),e.Node.prototype.setHTML=e.Node.prototype.setContent,e.Node.prototype.getHTML=e.Node.prototype.getContent,e.NodeList.importMethod(e.Node.prototype,["append","insert","appendChild","insertBefore","prepend","setContent","getContent","setHTML","getHTML"]);var r=e.Node,i=e.DOM;r.ATTRS={text:{getter:function(){return i.getText(this._node)},setter:function(e){return i.setText(this._node,e),e}},"for":{getter:function(){return i.getAttribute(this._node,"for")},setter:function(e){return i.setAttribute(this._node,"for",e),e}},options:{getter:function(){return this._node.getElementsByTagName("option")}},children:{getter:function(){var t=this._node,n=t.children,r,i,s;if(!n){r=t.childNodes,n=[];for(i=0,s=r.length;i1?this._data[e]=t:this._data=e,this},clearData:function(e){return"_data"in this&&(typeof e!="undefined"?delete this._data[e]:delete this._data),this}}),e.mix(e.NodeList.prototype,{getData:function(e){var t=arguments.length?[e]:[];return this._invoke("getData",t,!0)},setData:function(e,t){var n=arguments.length>1?[e,t]:[e];return this._invoke("setData",n)},clearData:function(e){var t=arguments.length?[e]:[];return this._invoke("clearData",[e])}})},"3.18.1",{requires:["event-base","node-core","dom-base","dom-style"]}); -YUI.add("node-core",function(e,t){var n=".",r="nodeName",i="nodeType",s="ownerDocument",o="tagName",u="_yuid",a={},f=Array.prototype.slice,l=e.DOM,c=function(t){if(!this.getDOMNode)return new c(t);if(typeof t=="string"){t=c._fromString(t);if(!t)return null}var n=t.nodeType!==9?t.uniqueID:t[u];n&&c._instances[n]&&c._instances[n]._node!==t&&(t[u]=null),n=n||e.stamp(t),n||(n=e.guid()),this[u]=n,this._node=t,this._stateProxy=t,this._initPlugins&&this._initPlugins()},h=function(t){var n=null;return t&&(n=typeof t=="string"?function(n){return e.Selector.test(n,t)}:function(n){return t(e.one(n))}),n};c.ATTRS={},c.DOM_EVENTS={},c._fromString=function(t){return t&&(t.indexOf("doc")===0?t=e.config.doc:t.indexOf("win")===0?t=e.config.win:t=e.Selector.query(t,null,!0)),t||null},c.NAME="node",c.re_aria=/^(?:role$|aria-)/,c.SHOW_TRANSITION="fadeIn",c.HIDE_TRANSITION="fadeOut",c._instances={},c.getDOMNode=function(e){return e?e.nodeType?e:e._node||null:null},c.scrubVal=function(t,n){if(t){if(typeof t=="object"||typeof t=="function")if(i in t||l.isWindow(t))t=e.one(t);else if(t.item&&!t._nodes||t[0]&&t[0][i])t=e.all(t)}else typeof t=="undefined"?t=n:t===null&&(t=null);return t},c.addMethod=function(e,t,n){e&&t&&typeof t=="function"&&(c.prototype[e]=function(){var e=f.call(arguments),r=this,i;return e[0]&&e[0]._node&&(e[0]=e[0]._node),e[1]&&e[1]._node&&(e[1]=e[1]._node),e.unshift(r._node),i=t.apply(n||r,e),i&&(i=c.scrubVal(i,r)),typeof i!="undefined"||(i=r),i})},c.importMethod=function(t,n,r){typeof n=="string"?(r=r||n,c.addMethod(r,t[n],t)):e.Array.each(n,function(e){c.importMethod(t,e)})},c.one=function(t){var n=null,r,i;if(t){if(typeof t=="string"){t=c._fromString(t);if(!t)return null}else if(t.getDOMNode)return t;if(t.nodeType||e.DOM.isWindow(t)){i=t.uniqueID&&t.nodeType!==9?t.uniqueID:t._yuid,n=c._instances[i],r=n?n._node:null;if(!n||r&&t!==r)n=new c(t),t.nodeType!=11&&(c._instances[n[u]]=n)}}return n},c.DEFAULT_SETTER=function(t,r){var i=this._stateProxy,s;return t.indexOf(n)>-1?(s=t,t=t.split(n),e.Object.setValue(i,t,r)):typeof i[t]!="undefined"&&(i[t]=r),r},c.DEFAULT_GETTER=function(t){var r=this._stateProxy,i;return t.indexOf&&t.indexOf(n)>-1?i=e.Object.getValue(r,t.split(n)):typeof r[t]!="undefined"&&(i=r[t]),i},e.mix(c.prototype,{DATA_PREFIX:"data-",toString:function(){var e=this[u]+": not bound to a node",t=this._node,n,i,s;return t&&(n=t.attributes,i=n&&n.id?t.getAttribute("id"):null,s=n&&n.className?t.getAttribute("className"):null,e=t[r],i&&(e+="#"+i),s&&(e+="."+s.replace(" ",".")),e+=" "+this[u]),e},get:function(e){var t;return this._getAttr?t=this._getAttr(e):t=this._get(e),t?t=c.scrubVal(t,this):t===null&&(t=null),t},_get:function(e){var t=c.ATTRS[e],n;return t&&t.getter?n=t.getter.call(this):c.re_aria.test(e)?n=this._node.getAttribute(e,2):n=c.DEFAULT_GETTER.apply(this,arguments),n},set:function(e,t){var n=c.ATTRS[e];return this._setAttr?this._setAttr.apply(this,arguments):n&&n.setter?n.setter.call(this,t,e):c.re_aria.test(e)?this._node.setAttribute(e,t):c.DEFAULT_SETTER.apply(this,arguments),this},setAttrs:function(t){return this._setAttrs?this._setAttrs(t):e.Object.each(t,function(e,t){this.set(t,e)},this),this},getAttrs:function(t){var n={};return this._getAttrs?this._getAttrs(t):e.Array.each(t,function(e,t){n[e]=this.get(e)},this),n},compareTo:function(e){var t=this._node;return e&&e._node&&(e=e._node),t===e},inDoc:function(e){var t=this._node;if(t){e=e?e._node||e:t[s];if(e.documentElement)return l.contains(e.documentElement,t)}return!1},getById:function(t){var n=this._node,r=l.byId(t,n[s]);return r&&l.contains(n,r)?r=e.one(r):r=null,r},ancestor:function(t,n,r){return arguments.length===2&&(typeof n=="string"||typeof n=="function")&&(r=n),e.one(l.ancestor(this._node,h(t),n,h(r)))},ancestors:function(t,n,r){return arguments.length===2&&(typeof n=="string"||typeof n=="function")&&(r=n),e.all(l.ancestors(this._node,h(t),n,h(r)))},previous:function(t,n){return e.one(l.elementByAxis(this._node,"previousSibling",h(t),n))},next:function(t,n){return e.one(l.elementByAxis(this._node,"nextSibling",h(t),n))},siblings:function(t){return e.all(l.siblings(this._node,h(t)))},one:function(t){return e.one(e.Selector.query(t,this._node,!0))},all:function(t){var n;return this._node&&(n=e.all(e.Selector.query(t,this._node)),n._query=t,n._queryRoot=this._node),n||e.all([])},test:function(t){return e.Selector.test(this._node,t)},remove:function(e){var t=this._node;return t&&t.parentNode&&t.parentNode.removeChild(t),e&&this.destroy(),this},replace:function(e){var t=this._node;return typeof e=="string"&&(e=c.create(e)),t.parentNode.replaceChild(c.getDOMNode(e),t),this},replaceChild:function(t,n){return typeof t=="string"&&(t=l.create(t)),e.one(this._node.replaceChild(c.getDOMNode(t),c.getDOMNode(n)))},destroy:function(t){var n=e.config.doc.uniqueID?"uniqueID":"_yuid",r;this.purge(),this.unplug&&this.unplug(),this.clearData(),t&&e.NodeList.each(this.all("*"),function(t){r=c._instances[t[n]],r?r.destroy():e.Event.purgeElement(t)}),this._node=null,this._stateProxy=null,delete c._instances[this._yuid]},invoke:function(e,t,n,r,i,s){var o=this._node,u;return t&&t._node&&(t=t._node),n&&n._node&&(n=n._node),u=o[e](t,n,r,i,s),c.scrubVal(u,this)},swap:e.config.doc.documentElement.swapNode?function(e){this._node.swapNode(c.getDOMNode(e))}:function(e){e=c.getDOMNode(e);var t=this._node,n=e.parentNode,r=e.nextSibling;return r===t?n.insertBefore(t,e):e===t.nextSibling?n.insertBefore(e,t):(t.parentNode.replaceChild(e,t),l.addHTML(n,t,r)),this},hasMethod:function(e){var t=this._node;return!(!(t&&e in t&&typeof t[e]!="unknown")||typeof t[e]!="function"&&String(t[e]).indexOf("function")!==1)},isFragment:function(){return this.get("nodeType")===11},empty:function(){return this.get("childNodes").remove().destroy(!0),this},getDOMNode:function(){return this._node}},!0),e.Node=c,e.one=c.one;var p=function(t){var n=[];t&&(typeof t=="string"?(this._query=t,t=e.Selector.query(t)):t.nodeType||l.isWindow(t)?t=[t]:t._node?t=[t._node]: -t[0]&&t[0]._node?(e.Array.each(t,function(e){e._node&&n.push(e._node)}),t=n):t=e.Array(t,0,!0)),this._nodes=t||[]};p.NAME="NodeList",p.getDOMNodes=function(e){return e&&e._nodes?e._nodes:e},p.each=function(t,n,r){var i=t._nodes;i&&i.length&&e.Array.each(i,n,r||t)},p.addMethod=function(t,n,r){t&&n&&(p.prototype[t]=function(){var t=[],i=arguments;return e.Array.each(this._nodes,function(s){var o=s.uniqueID&&s.nodeType!==9?"uniqueID":"_yuid",u=e.Node._instances[s[o]],a,f;u||(u=p._getTempNode(s)),a=r||u,f=n.apply(a,i),f!==undefined&&f!==u&&(t[t.length]=f)}),t.length?t:this})},p.importMethod=function(t,n,r){typeof n=="string"?(r=r||n,p.addMethod(r,t[n])):e.Array.each(n,function(e){p.importMethod(t,e)})},p._getTempNode=function(t){var n=p._tempNode;return n||(n=e.Node.create("
"),p._tempNode=n),n._node=t,n._stateProxy=t,n},e.mix(p.prototype,{_invoke:function(e,t,n){var r=n?[]:this;return this.each(function(i){var s=i[e].apply(i,t);n&&r.push(s)}),r},item:function(t){return e.one((this._nodes||[])[t])},each:function(t,n){var r=this;return e.Array.each(this._nodes,function(i,s){return i=e.one(i),t.call(n||i,i,s,r)}),r},batch:function(t,n){var r=this;return e.Array.each(this._nodes,function(i,s){var o=e.Node._instances[i[u]];return o||(o=p._getTempNode(i)),t.call(n||o,o,s,r)}),r},some:function(t,n){var r=this;return e.Array.some(this._nodes,function(i,s){return i=e.one(i),n=n||i,t.call(n,i,s,r)})},toFrag:function(){return e.one(e.DOM._nl2frag(this._nodes))},indexOf:function(t){return e.Array.indexOf(this._nodes,e.Node.getDOMNode(t))},filter:function(t){return e.all(e.Selector.filter(this._nodes,t))},modulus:function(t,n){n=n||0;var r=[];return p.each(this,function(e,i){i%t===n&&r.push(e)}),e.all(r)},odd:function(){return this.modulus(2,1)},even:function(){return this.modulus(2)},destructor:function(){},refresh:function(){var t,n=this._nodes,r=this._query,i=this._queryRoot;return r&&(i||n&&n[0]&&n[0].ownerDocument&&(i=n[0].ownerDocument),this._nodes=e.Selector.query(r,i)),this},size:function(){return this._nodes.length},isEmpty:function(){return this._nodes.length<1},toString:function(){var e="",t=this[u]+": not bound to any nodes",n=this._nodes,i;return n&&n[0]&&(i=n[0],e+=i[r],i.id&&(e+="#"+i.id),i.className&&(e+="."+i.className.replace(" ",".")),n.length>1&&(e+="...["+n.length+" items]")),e||t},getDOMNodes:function(){return this._nodes}},!0),p.importMethod(e.Node.prototype,["destroy","empty","remove","set"]),p.prototype.get=function(t){var n=[],r=this._nodes,i=!1,s=p._getTempNode,o,u;return r[0]&&(o=e.Node._instances[r[0]._yuid]||s(r[0]),u=o._get(t),u&&u.nodeType&&(i=!0)),e.Array.each(r,function(r){o=e.Node._instances[r._yuid],o||(o=s(r)),u=o._get(t),i||(u=e.Node.scrubVal(u,o)),n.push(u)}),i?e.all(n):n},e.NodeList=p,e.all=function(e){return new p(e)},e.Node.all=e.all;var d=e.NodeList,v=Array.prototype,m={concat:1,pop:0,push:0,shift:0,slice:1,splice:1,unshift:0};e.Object.each(m,function(t,n){d.prototype[n]=function(){var r=[],i=0,s,o;while(typeof (s=arguments[i++])!="undefined")r.push(s._node||s._nodes||s);return o=v[n].apply(this._nodes,r),t?o=e.all(o):o=e.Node.scrubVal(o),o}}),e.Array.each(["removeChild","hasChildNodes","cloneNode","hasAttribute","scrollIntoView","getElementsByTagName","focus","blur","submit","reset","select","createCaption"],function(t){e.Node.prototype[t]=function(e,n,r){var i=this.invoke(t,e,n,r);return i}}),e.Node.prototype.removeAttribute=function(e){var t=this._node;return t&&t.removeAttribute(e,0),this},e.Node.importMethod(e.DOM,["contains","setAttribute","getAttribute","wrap","unwrap","generateID"]),e.NodeList.importMethod(e.Node.prototype,["getAttribute","setAttribute","removeAttribute","unwrap","wrap","generateID"])},"3.18.1",{requires:["dom-core","selector"]}); -YUI.add("node-event-delegate",function(e,t){e.Node.prototype.delegate=function(t){var n=e.Array(arguments,0,!0),r=e.Lang.isObject(t)&&!e.Lang.isArray(t)?1:2;return n.splice(r,0,this._node),e.delegate.apply(e,n)}},"3.18.1",{requires:["node-base","event-delegate"]}); -YUI.add("node-pluginhost",function(e,t){e.Node.plug=function(){var t=e.Array(arguments);return t.unshift(e.Node),e.Plugin.Host.plug.apply(e.Base,t),e.Node},e.Node.unplug=function(){var t=e.Array(arguments);return t.unshift(e.Node),e.Plugin.Host.unplug.apply(e.Base,t),e.Node},e.mix(e.Node,e.Plugin.Host,!1,null,1),e.Object.each(e.Node._instances,function(t){e.Plugin.Host.apply(t)}),e.NodeList.prototype.plug=function(){var t=arguments;return e.NodeList.each(this,function(n){e.Node.prototype.plug.apply(e.one(n),t)}),this},e.NodeList.prototype.unplug=function(){var t=arguments;return e.NodeList.each(this,function(n){e.Node.prototype.unplug.apply(e.one(n),t)}),this}},"3.18.1",{requires:["node-base","pluginhost"]}); -YUI.add("node-screen",function(e,t){e.each(["winWidth","winHeight","docWidth","docHeight","docScrollX","docScrollY"],function(t){e.Node.ATTRS[t]={getter:function(){var n=Array.prototype.slice.call(arguments);return n.unshift(e.Node.getDOMNode(this)),e.DOM[t].apply(this,n)}}}),e.Node.ATTRS.scrollLeft={getter:function(){var t=e.Node.getDOMNode(this);return"scrollLeft"in t?t.scrollLeft:e.DOM.docScrollX(t)},setter:function(t){var n=e.Node.getDOMNode(this);n&&("scrollLeft"in n?n.scrollLeft=t:(n.document||n.nodeType===9)&&e.DOM._getWin(n).scrollTo(t,e.DOM.docScrollY(n)))}},e.Node.ATTRS.scrollTop={getter:function(){var t=e.Node.getDOMNode(this);return"scrollTop"in t?t.scrollTop:e.DOM.docScrollY(t)},setter:function(t){var n=e.Node.getDOMNode(this);n&&("scrollTop"in n?n.scrollTop=t:(n.document||n.nodeType===9)&&e.DOM._getWin(n).scrollTo(e.DOM.docScrollX(n),t))}},e.Node.importMethod(e.DOM,["getXY","setXY","getX","setX","getY","setY","swapXY"]),e.Node.ATTRS.region={getter:function(){var t=this.getDOMNode(),n;return t&&!t.tagName&&t.nodeType===9&&(t=t.documentElement),e.DOM.isWindow(t)?n=e.DOM.viewportRegion(t):n=e.DOM.region(t),n}},e.Node.ATTRS.viewportRegion={getter:function(){return e.DOM.viewportRegion(e.Node.getDOMNode(this))}},e.Node.importMethod(e.DOM,"inViewportRegion"),e.Node.prototype.intersect=function(t,n){var r=e.Node.getDOMNode(this);return e.instanceOf(t,e.Node)&&(t=e.Node.getDOMNode(t)),e.DOM.intersect(r,t,n)},e.Node.prototype.inRegion=function(t,n,r){var i=e.Node.getDOMNode(this);return e.instanceOf(t,e.Node)&&(t=e.Node.getDOMNode(t)),e.DOM.inRegion(i,t,n,r)}},"3.18.1",{requires:["dom-screen","node-base"]}); -YUI.add("node-style",function(e,t){(function(e){e.mix(e.Node.prototype,{setStyle:function(t,n){return e.DOM.setStyle(this._node,t,n),this},setStyles:function(t){return e.DOM.setStyles(this._node,t),this},getStyle:function(t){return e.DOM.getStyle(this._node,t)},getComputedStyle:function(t){return e.DOM.getComputedStyle(this._node,t)}}),e.NodeList.importMethod(e.Node.prototype,["getStyle","getComputedStyle","setStyle","setStyles"])})(e);var n=e.Node;e.mix(n.prototype,{show:function(e){return e=arguments[arguments.length-1],this.toggleView(!0,e),this},_show:function(){this.removeAttribute("hidden"),this.setStyle("display","")},_isHidden:function(){return this.hasAttribute("hidden")||e.DOM.getComputedStyle(this._node,"display")==="none"},toggleView:function(e,t){return this._toggleView.apply(this,arguments),this},_toggleView:function(e,t){return t=arguments[arguments.length-1],typeof e!="boolean"&&(e=this._isHidden()?1:0),e?this._show():this._hide(),typeof t=="function"&&t.call(this),this},hide:function(e){return e=arguments[arguments.length-1],this.toggleView(!1,e),this},_hide:function(){this.setAttribute("hidden","hidden"),this.setStyle("display","none")}}),e.NodeList.importMethod(e.Node.prototype,["show","hide","toggleView"])},"3.18.1",{requires:["dom-style","node-base"]}); -YUI.add("oop",function(e,t){function a(t,n,i,s,o){if(t&&t[o]&&t!==e)return t[o].call(t,n,i);switch(r.test(t)){case 1:return r[o](t,n,i);case 2:return r[o](e.Array(t,0,!0),n,i);default:return e.Object[o](t,n,i,s)}}var n=e.Lang,r=e.Array,i=Object.prototype,s="_~yuim~_",o=i.hasOwnProperty,u=i.toString;e.augment=function(t,n,r,i,s){var a=t.prototype,f=a&&n,l=n.prototype,c=a||t,h,p,d,v,m;return s=s?e.Array(s):[],f&&(p={},d={},v={},h=function(e,t){if(r||!(t in a))u.call(e)==="[object Function]"?(v[t]=e,p[t]=d[t]=function(){return m(this,e,arguments)}):p[t]=e},m=function(e,t,r){for(var i in v)o.call(v,i)&&e[i]===d[i]&&(e[i]=v[i]);return n.apply(e,s),t.apply(e,r)},i?e.Array.each(i,function(e){e in l&&h(l[e],e)}):e.Object.each(l,h,null,!0)),e.mix(c,p||l,r,i),f||n.apply(c,s),t},e.aggregate=function(t,n,r,i){return e.mix(t,n,r,i,0,!0)},e.extend=function(t,n,r,s){(!n||!t)&&e.error("extend failed, verify dependencies");var o=n.prototype,u=e.Object(o);return t.prototype=u,u.constructor=t,t.superclass=o,n!=Object&&o.constructor==i.constructor&&(o.constructor=n),r&&e.mix(u,r,!0),s&&e.mix(t,s,!0),t},e.each=function(e,t,n,r){return a(e,t,n,r,"each")},e.some=function(e,t,n,r){return a(e,t,n,r,"some")},e.clone=function(t,r,i,o,u,a){var f,l,c;if(!n.isObject(t)||e.instanceOf(t,YUI)||t.addEventListener||t.attachEvent)return t;l=a||{};switch(n.type(t)){case"date":return new Date(t);case"regexp":return t;case"function":return t;case"array":f=[];break;default:if(t[s])return l[t[s]];c=e.guid(),f=r?{}:e.Object(t),t[s]=c,l[c]=t}return e.each(t,function(n,a){(a||a===0)&&(!i||i.call(o||this,n,a,this,t)!==!1)&&a!==s&&a!="prototype"&&(this[a]=e.clone(n,r,i,o,u||t,l))},f),a||(e.Object.each(l,function(e,t){if(e[s])try{delete e[s]}catch(n){e[s]=null}},this),l=null),f},e.bind=function(t,r){var i=arguments.length>2?e.Array(arguments,2,!0):null;return function(){var s=n.isString(t)?r[t]:t,o=i?i.concat(e.Array(arguments,0,!0)):arguments;return s.apply(r||s,o)}},e.rbind=function(t,r){var i=arguments.length>2?e.Array(arguments,2,!0):null;return function(){var s=n.isString(t)?r[t]:t,o=i?e.Array(arguments,0,!0).concat(i):arguments;return s.apply(r||s,o)}}},"3.18.1",{requires:["yui-base"]}); -YUI.add("pluginhost-base",function(e,t){function r(){this._plugins={}}var n=e.Lang;r.prototype={plug:function(e,t){var r,i,s;if(n.isArray(e))for(r=0,i=e.length;r=0;o--)s=n[o],a=s._UNPLUG,a&&e.mix(i,a,!0),u=s._PLUG,u&&e.mix(r,u,!0);for(f in r)r.hasOwnProperty(f)&&(i[f]||this.plug(r[f]));t&&t.plugins&&this.plug(t.plugins)},n.plug=function(t,n,i){var s,o,u,a;if(t!==e.Base){t._PLUG=t._PLUG||{},r.isArray(n)||(i&&(n={fn:n,cfg:i}),n=[n]);for(o=0,u=n.length;o+~"]/gi},attr:{token:"\ue001",re:/(\[[^\]]*\])/g},pseudo:{token:"\ue002",re:/(\([^\)]*\))/g}},useNative:!0,_escapeId:function(e){return e&&(e=e.replace(/([:\[\]\(\)#\.'<>+~"])/g,"\\$1")),e},_compare:"sourceIndex"in e.config.doc.documentElement?function(e,t){var n=e.sourceIndex,r=t.sourceIndex;return n===r?0:n>r?1:-1}:e.config.doc.documentElement[t]?function(e,n){return e[t](n)&4?-1:1}:function(e,t){var r,i,s;return e&&t&&(r=e[n].createRange(),r.setStart(e,0),i=t[n].createRange(),i.setStart(t,0),s=r.compareBoundaryPoints(1,i)),s},_sort:function(t){return t&&(t=e.Array(t,0,!0),t.sort&&t.sort(r._compare)),t},_deDupe:function(e){var t=[],n,r;for(n=0;r=e[n++];)r._found||(t[t.length]=r,r._found=!0);for(n=0;r=t[n++];)r._found=null,r.removeAttribute("_found");return t},query:function(t,n,i,s){n=n||e.config.doc;var o=[],u=e.Selector.useNative&&e.config.doc.querySelector&&!s,a=[[t,n]],f,l,c,h=u?e.Selector._nativeQuery:e.Selector._bruteQuery;if(t&&h){!s&&(!u||n.tagName)&&(a=r._splitQueries(t,n));for(c=0;f=a[c++];)l=h(f[0],f[1],i),i||(l=e.Array(l,0,!0)),l&&(o=o.concat(l));a.length>1&&(o=r._sort(r._deDupe(o)))}return i?o[0]||null:o},_replaceSelector:function(t){var n=e.Selector._parse("esc",t),i,s;return t=e.Selector._replace("esc",t),s=e.Selector._parse("pseudo",t),t=r._replace("pseudo",t),i=e.Selector._parse("attr",t),t=e.Selector._replace("attr",t),{esc:n,attrs:i,pseudos:s,selector:t}},_restoreSelector:function(t){var n=t.selector;return n=e.Selector._restore("attr",n,t.attrs),n=e.Selector._restore("pseudo",n,t.pseudos),n=e.Selector._restore("esc",n,t.esc),n},_replaceCommas:function(t){var n=e.Selector._replaceSelector(t),t=n.selector;return t&&(t=t.replace(/,/g,"\ue007"),n.selector=t,t=e.Selector._restoreSelector(n)),t},_splitQueries:function(t,n){t.indexOf(",")>-1&&(t=e.Selector._replaceCommas(t));var r=t.split("\ue007"),i=[],s="",o,u,a;if(n){n.nodeType===1&&(o=e.Selector._escapeId(e.DOM.getId(n)),o||(o=e.guid(),e.DOM.setId(n,o)),s='[id="'+o+'"] ');for(u=0,a=r.length;u-1&&e.Selector.pseudos&&e.Selector.pseudos.checked)return e.Selector.query(t,n,r,!0);try{return n["querySelector"+(r?"":"All")](t)}catch(i){return e.Selector.query(t,n,r,!0)}},filter:function(t,n){var r=[],i,s;if(t&&n)for(i=0;s=t[i++];)e.Selector.test(s,n)&&(r[r.length]=s);return r},test:function(t,r,i){var s=!1,o=!1,u,a,f,l,c,h,p,d,v;if(t&&t.tagName)if(typeof r=="function")s=r.call(t,t);else{u=r.split(","),!i&&!e.DOM.inDoc(t)&&(a=t.parentNode,a?i=a:(c=t[n].createDocumentFragment(),c.appendChild(t),i=c,o=!0)),i=i||t[n],h=e.Selector._escapeId(e.DOM.getId(t)),h||(h=e.guid(),e.DOM.setId(t,h));for(p=0;v=u[p++];){v+='[id="'+h+'"]',l=e.Selector.query(v,i);for(d=0;f=l[d++];)if(f===t){s=!0;break}if(s)break}o&&c.removeChild(t)}return s},ancestor:function(t,n,r){return e.DOM.ancestor(t,function(t){return e.Selector.test(t,n)},r)},_parse:function(t,n){return n.match(e.Selector._types[t].re)},_replace:function(t,n){var r=e.Selector._types[t];return n.replace(r.re,r.token)},_restore:function(t,n,r){if(r){var i=e.Selector._types[t].token,s,o;for(s=0,o=r.length;s[index:"+l(e)+",length:"+e.childNodes.length+"]["+(e.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"):e.nodeName:"[No node]";var t}function y(e){this.root=e,this._next=e}function S(e,t){this.node=e,this.offset=t}function T(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}i.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||c.fail("document missing a Node creation method"),i.isHostMethod(document,"getElementsByTagName")||c.fail("document missing getElementsByTagName method"),t=document.createElement("div"),i.areHostMethods(t,["insertBefore","appendChild","cloneNode"])||c.fail("Incomplete Element implementation"),i.isHostProperty(t,"innerHTML")||c.fail("Element is missing innerHTML property"),t=document.createTextNode("test"),i.areHostMethods(t,["splitText","deleteData","insertData","appendData","cloneNode"])||c.fail("Incomplete Text Node implementation"),o=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1},(t=document.createElement("b")).innerHTML="1",u=t.firstChild,t.innerHTML="
",n=N(u),e.features.crashyTextNodes=n,typeof window.getComputedStyle!=r?s=function(e,t){return R(e).getComputedStyle(e,null)[t]}:typeof document.documentElement.currentStyle!=r?s=function(e,t){return e.currentStyle?e.currentStyle[t]:""}:c.fail("No means of obtaining computed style properties found"),y.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},S.prototype={equals:function(e){return!!e&&this.node===e.node&&this.offset==e.offset},inspect:function(){return"[DomPosition("+E(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},(T.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11,INVALID_NODE_TYPE_ERR:24}).toString=function(){return this.message},e.dom={arrayContains:o,isHtmlNamespace:function(e){var t;return typeof e.namespaceURI==r||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t},parentElement:function(e){return 1==(e=e.parentNode).nodeType?e:null},getNodeIndex:l,getNodeLength:function(e){switch(e.nodeType){case 7:case 10:return 0;case 3:case 8:return e.length;default:return e.childNodes.length}},getCommonAncestor:f,isAncestorOf:g,isOrIsAncestorOf:function(e,t){return g(e,t,!0)},getClosestAncestorIn:d,isCharacterDataNode:p,isTextOrCommentNode:function(e){return!!e&&(3==(e=e.nodeType)||8==e)},insertAfter:m,splitDataNode:function(e,t,n){var r,o,i=e.cloneNode(!1);if(i.deleteData(0,t),e.deleteData(t,e.length-t),m(i,e),n)for(r=0;o=n[r++];)o.node==e&&o.offset>t?(o.node=i,o.offset-=t):o.node==e.parentNode&&o.offset>l(e)&&++o.offset;return i},getDocument:a,getWindow:R,getIframeWindow:function(e){if(typeof e.contentWindow!=r)return e.contentWindow;if(typeof e.contentDocument!=r)return e.contentDocument.defaultView;throw c.createError("getIframeWindow: No Window object found for iframe element")},getIframeDocument:C,getBody:h,isWindow:v,getContentDocument:function(e,t,n){var r;if(e?i.isHostProperty(e,"nodeType")?r=(1==e.nodeType&&"iframe"==e.tagName.toLowerCase()?C:a)(e):v(e)&&(r=e.document):r=document,!r)throw t.createError(n+"(): Parameter must be a Window object or DOM node");return r},getRootContainer:function(e){for(var t;t=e.parentNode;)e=t;return e},comparePoints:function(e,t,n,r){var o,i,a,s;if(e==n)return t===r?0:t=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function $(e,t,n){if(g(e),g(t),d(t)!=d(e))throw new a("WRONG_DOCUMENT_ERR");var r=c(e.startContainer,e.startOffset,t.endContainer,t.endOffset),e=c(e.endContainer,e.endOffset,t.startContainer,t.startOffset);return n?r<=0&&0<=e:r<0&&0(u(e)?e:e.childNodes).length)throw new a("INDEX_SIZE_ERR")}function J(e,t){if(r(e,!0)!==r(t,!0))throw new a("WRONG_DOCUMENT_ERR")}function ee(e){if(o(e,!0))throw new a("NO_MODIFICATION_ALLOWED_ERR")}function te(e,t){if(!e)throw new a(t)}function ne(e,t){return t<=(u(e)?e:e.childNodes).length}function re(e){return!!e.startContainer&&!!e.endContainer&&!(W&&(s.isBrokenNode(e.startContainer)||s.isBrokenNode(e.endContainer)))&&l(e.startContainer)==l(e.endContainer)&&ne(e.startContainer,e.startOffset)&&ne(e.endContainer,e.endOffset)}function g(e){if(!re(e))throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: ("+e.inspect()+")")}h.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current=null,this._next=this._first},hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next;return e&&(this._next=e!==this._last?e.nextSibling:null,u(e)&&this.clonePartiallySelectedTextNodes&&(e===this.ec&&(e=e.cloneNode(!0)).deleteData(this.eo,e.length-this.eo),this._current===this.sc&&(e=e.cloneNode(!0)).deleteData(0,this.so))),e},remove:function(){var e,t,n=this._current;!u(n)||n!==this.sc&&n!==this.ec?n.parentNode&&z(n):(e=n===this.sc?this.so:0)!=(t=n===this.ec?this.eo:n.length)&&n.deleteData(e,t-e)},isPartiallySelectedSubtree:function(){return F(this._current,this.range)},getSubtreeIterator:function(){var e,t,n,r,o,i;return this.isSingleCharacterDataNode?(e=this.range.cloneRange()).collapse(!1):(e=new ue(d(this.range)),t=this._current,r=0,i=M(o=n=t),D(t,this.sc)&&(n=this.sc,r=this.so),D(t,this.ec)&&(o=this.ec,i=this.eo),de(e,n,r,o,i)),new h(e,this.clonePartiallySelectedTextNodes)},detach:function(){this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}},p=[1,3,4,5,7,8,10],m=[2,9,11],t=[1,3,4,5,7,8,10,11],n=[1,3,4,5,7,8],r=K([9,11]),o=K([5,6,10,12]),R=K([6,10,12]),C=K([1]),v=document.createElement("style"),N=!1;try{v.innerHTML="x",N=3==v.firstChild.nodeType}catch(he){}function oe(e,t){var n,r,o,i,a;g(e),n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset,a=n===o,u(o)&&0=_(n)&&i++,r=0),e.setStartAndEnd(n,r,o,i)}function ie(e){g(e);var t=e.commonAncestorContainer.parentNode.cloneNode(!1);return t.appendChild(e.cloneContents()),t.innerHTML}function ae(e){e.START_TO_START=y,e.START_TO_END=S,e.END_TO_END=T,e.END_TO_START=w,e.NODE_BEFORE=A,e.NODE_AFTER=x,e.NODE_BEFORE_AND_AFTER=O,e.NODE_INSIDE=b}function se(e){ae(e),ae(e.prototype)}function ce(o,i){return function(){var e,t,n,r;return g(this),e=this.startContainer,t=this.startOffset,r=this.commonAncestorContainer,n=new h(this,!0),e!==r&&(e=(r=j(L(e,r,!0))).node,t=r.offset),q(n,ee),n.reset(),r=o(n),n.detach(),i(this,e,t,e,t),r}}function le(e,d){function t(t,n){return function(e){X(e,p),X(l(e),m);e=(t?V:j)(e);(n?r:o)(this,e.node,e.offset)}}function r(e,t,n){var r=e.endContainer,o=e.endOffset;t===e.startContainer&&n===e.startOffset||(l(t)==l(r)&&1!=c(t,n,r,o)||(r=t,o=n),d(e,t,n,r,o))}function o(e,t,n){var r=e.startContainer,o=e.startOffset;t===e.endContainer&&n===e.endOffset||(l(t)==l(r)&&-1!=c(t,n,r,o)||(r=t,o=n),d(e,r,o,t,n))}var n=function(){};n.prototype=i.rangePrototype,e.prototype=new n,P.extend(e.prototype,{setStart:function(e,t){f(e,!0),Z(e,t),r(this,e,t)},setEnd:function(e,t){f(e,!0),Z(e,t),o(this,e,t)},setStartAndEnd:function(){var e=arguments,t=e[0],n=e[1],r=t,o=n;switch(e.length){case 3:o=e[2];break;case 4:r=e[2],o=e[3]}f(t,!0),Z(t,n),f(r,!0),Z(r,o),d(this,t,n,r,o)},setBoundary:function(e,t,n){this["set"+(n?"Start":"End")](e,t)},setStartBefore:t(!0,!0),setStartAfter:t(!1,!0),setEndBefore:t(!0,!1),setEndAfter:t(!1,!1),collapse:function(e){g(this),e?d(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):d(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){f(e,!0),d(this,e,0,e,M(e))}, -selectNode:function(e){f(e,!1),X(e,p);var t=V(e),e=j(e);d(this,t.node,t.offset,e.node,e.offset)},extractContents:ce(G,d),deleteContents:ce(Y,d),canSurroundContents:function(){var e,t;return g(this),ee(this.startContainer),ee(this.endContainer),t=(e=new h(this,!0))._first&&F(e._first,this)||e._last&&F(e._last,this),e.detach(),!t},splitBoundaries:function(){oe(this)},splitBoundariesPreservingPositions:function(e){oe(this,e)},normalizeBoundaries:function(){var r,o,i,a,e,t,n,s,c,l;g(this),r=this.startContainer,o=this.startOffset,i=this.endContainer,a=this.endOffset,e=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(a=(i=e).length,e.appendData(t.data),z(t))},t=function(e){var t,n=e.previousSibling;n&&n.nodeType==e.nodeType&&(t=(r=e).length,o=n.length,e.insertData(0,n.data),z(n),r==i?(a+=o,i=r):i==e.parentNode&&(n=_(e),a==n?(i=e,a=t):n=l&&e.start<=i&&(this.setStart(n,e.start-l),r=!0),r&&e.end>=l&&e.end<=i&&(this.setEnd(n,e.end-l),o=!0),l=i;else for(a=(s=n.childNodes).length;a--;)t.push(s[a])},getName:function(){return"DomRange"},equals:function(e){return ue.rangesEqual(this,e)},isValid:function(){return re(this)},inspect:function(){return Q(this)},detach:function(){}}),le(ue,de),P.extend(ue,{rangeProperties:E,RangeIterator:h,copyComparisonConstants:se,createPrototypeRange:le,inspect:Q,toHtml:ie,getRangeDocument:d,rangesEqual:function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset}}),i.DomRange=ue}),f.createCoreModule("WrappedRange",["DomRange"],function(n,c){var l,d,u,h,f,g,R=n.dom,p=n.util,C=R.DomPosition,m=n.DomRange,v=R.getBody,N=R.getContentDocument,E=R.isCharacterDataNode;if(n.features.implementsDomRange){var e,t,r,o,i,y,S=m.rangeProperties;function a(e){for(var t,n=S.length;n--;)e[t=S[n]]=e.nativeRange[t];e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset}m.createPrototypeRange(l=function(e){if(!e)throw c.createError("WrappedRange: Range must be specified");this.nativeRange=e,a(this)},function(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,a=e.endContainer!==r||e.endOffset!=o,s=!e.equals(e.nativeRange);(i||a||s)&&(e.setEnd(r,o),e.setStart(t,n))}),(e=l.prototype).selectNode=function(e){this.nativeRange.selectNode(e),a(this)},e.cloneContents=function(){return this.nativeRange.cloneContents()},e.surroundContents=function(e){this.nativeRange.surroundContents(e),a(this)},e.collapse=function(e){this.nativeRange.collapse(e),a(this)},e.cloneRange=function(){return new l(this.nativeRange.cloneRange())},e.refresh=function(){a(this)},e.toString=function(){return this.nativeRange.toString()},t=document.createTextNode("test"),v(document).appendChild(t),(r=document.createRange()).setStart(t,0),r.setEnd(t,0);try{r.setStart(t,1),e.setStart=function(e,t){this.nativeRange.setStart(e,t),a(this)},e.setEnd=function(e,t){this.nativeRange.setEnd(e,t),a(this)},o=function(t){return function(e){this.nativeRange[t](e),a(this)}}}catch(s){e.setStart=function(e,t){try{this.nativeRange.setStart(e,t)}catch(s){this.nativeRange.setEnd(e,t),this.nativeRange.setStart(e,t)}a(this)},e.setEnd=function(e,t){try{this.nativeRange.setEnd(e,t)}catch(s){this.nativeRange.setStart(e,t),this.nativeRange.setEnd(e,t)}a(this)},o=function(t,n){return function(e){try{this.nativeRange[t](e)}catch(s){this.nativeRange[n](e),this.nativeRange[t](e)}a(this)}}}e.setStartBefore=o("setStartBefore","setEndBefore"),e.setStartAfter=o("setStartAfter","setEndAfter"),e.setEndBefore=o("setEndBefore","setStartBefore"),e.setEndAfter=o("setEndAfter","setStartAfter"),e.selectNodeContents=function(e){this.setStartAndEnd(e,0,R.getNodeLength(e))},r.selectNodeContents(t),r.setEnd(t,3),(o=document.createRange()).selectNodeContents(t),o.setEnd(t,4),o.setStart(t,2),-1==r.compareBoundaryPoints(r.START_TO_END,o)&&1==r.compareBoundaryPoints(r.END_TO_START,o)?e.compareBoundaryPoints=function(e,t){return e==(t=t.nativeRange||t).START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:e.compareBoundaryPoints=function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)},(o=document.createElement("div")).innerHTML="123",i=o.firstChild,(y=v(document)).appendChild(o),r.setStart(i,1),r.setEnd(i,2),r.deleteContents(),"13"==i.data&&(e.deleteContents=function(){this.nativeRange.deleteContents(),a(this)},e.extractContents=function(){var e=this.nativeRange.extractContents();return a(this),e}),y.removeChild(o),y=null,p.isHostMethod(r,"createContextualFragment")&&(e.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),v(document).removeChild(t),e.getName=function(){return"WrappedRange"},n.WrappedRange=l,n.createNativeRange=function(e){return(e=N(e,c,"createNativeRange")).createRange()}}n.features.implementsTextRange&&(d=function(e){var t,n=e.parentElement(),r=e.duplicate();return r.collapse(!0),t=r.parentElement(),(r=e.duplicate()).collapse(!1),(r=t==(e=r.parentElement())?t:R.getCommonAncestor(t,e))==n?r:R.getCommonAncestor(n,r)},u=function(e){return 0==e.compareEndPoints("StartToEnd",e)},h=function(d,e,t,u,n){var r,o,h,f,g,i,p,a,s,m,c,l=d.duplicate();if(l.collapse(t),r=l.parentElement(),!(r=R.isOrIsAncestorOf(e,r)?r:e).canHaveHTML)return{boundaryPosition:e=new C(r.parentNode,R.getNodeIndex(r)),nodeInfo:{nodeIndex:e.offset,containerElement:e.node}};for((o=R.getDocument(r).createElement("span")).parentNode&&R.removeNode(o), -f=t?"StartToStart":"StartToEnd",i=n&&n.containerElement==r?n.nodeIndex:0,s=a=p=r.childNodes.length;;){if(s==p?r.appendChild(o):r.insertBefore(o,r.childNodes[s]),l.moveToElementText(o),0==(h=l.compareEndPoints(f,d))||i==a)break;if(-1==h){if(a==i+1)break;i=s}else a=a==i+1?i:s;s=Math.floor((i+a)/2),r.removeChild(o)}if(e=o.nextSibling,-1==h&&e&&E(e)){if(l.setEndPoint(t?"EndToStart":"EndToEnd",d),/[\r\n]/.test(e.data))for(n=(c=l.duplicate()).text.replace(/\r\n/g,"\r").length,m=c.moveStart("character",n);-1==(h=c.compareEndPoints("StartToEnd",c));)m++,c.moveStart("character",1);else m=l.text.length;g=new C(e,m)}else n=(u||!t)&&o.previousSibling,g=(e=(u||t)&&o.nextSibling)&&E(e)?new C(e,0):n&&E(n)?new C(n,n.data.length):new C(r,R.getNodeIndex(o));return R.removeNode(o),{boundaryPosition:g,nodeInfo:{nodeIndex:s,containerElement:r}}},f=function(e,t){var n,r=e.offset,o=R.getDocument(e.node),i=v(o).createTextRange(),a=E(e.node),s=a?(n=e.node).parentNode:(n=r<(s=e.node.childNodes).length?s[r]:null,e.node),e=o.createElement("span");return e.innerHTML="&#feff;",n?s.insertBefore(e,n):s.appendChild(e),i.moveToElementText(e),i.collapse(!t),s.removeChild(e),a&&i[t?"moveStart":"moveEnd"]("character",r),i},((i=function(e){this.textRange=e,this.refresh()}).prototype=new m(document)).refresh=function(){var e,t,n=d(this.textRange),n=u(this.textRange)?e=h(this.textRange,n,!0,!0).boundaryPosition:(e=(t=h(this.textRange,n,!0,!1)).boundaryPosition,h(this.textRange,n,!1,!1,t.nodeInfo).boundaryPosition);this.setStart(e.node,e.offset),this.setEnd(n.node,n.offset)},i.prototype.getName=function(){return"WrappedTextRange"},m.copyComparisonConstants(i),i.rangeToTextRange=g=function(e){var t,n;return e.collapsed?f(new C(e.startContainer,e.startOffset),!0):(t=f(new C(e.startContainer,e.startOffset),!0),n=f(new C(e.endContainer,e.endOffset),!1),(e=v(m.getRangeDocument(e)).createTextRange()).setEndPoint("StartToStart",t),e.setEndPoint("EndToEnd",n),e)},i.prototype.toTextRange=function(){return g(this)},n.WrappedTextRange=i,n.features.implementsDomRange&&!n.config.preferTextRange||("undefined"==typeof(o=Function("return this;")()).Range&&(o.Range=i),n.createNativeRange=function(e){return e=N(e,c,"createNativeRange"),v(e).createTextRange()},n.WrappedRange=i)),n.createRange=function(e){return e=N(e,c,"createRange"),new n.WrappedRange(n.createNativeRange(e))},n.createRangyRange=function(e){return e=N(e,c,"createRangyRange"),new m(e)},p.createAliasForDeprecatedMethod(n,"createIframeRange","createRange"),p.createAliasForDeprecatedMethod(n,"createIframeRangyRange","createRangyRange"),n.addShimListener(function(e){var t=e.document;"undefined"==typeof t.createRange&&(t.createRange=function(){return n.createRange(t)}),t=null})}),f.createCoreModule("WrappedSelection",["DomRange","WrappedRange"],function(a,c){var d,u,s,h,e,f,g,p,m,R,C,n,v,l,N,E,y,S,t,T,w,A,x,O,b,P,I,_,D,B,k,L,M,H,W,z;function F(e){return"string"==typeof e?/^backward(s)?$/i.test(e):!!e}function V(e,t){return e?s.isWindow(e)?e:e instanceof ae?e.win:(e=s.getContentDocument(e,c,t),s.getWindow(e)):window}function j(e){return V(e,"getDocSelection").document.selection}function U(e){var t=!1;return t=e.anchorNode?1==s.comparePoints(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset):t}if(a.config.checkSelectionRanges=!0,d="boolean",u="number",s=a.dom,e=(h=a.util).isHostMethod,f=a.DomRange,g=a.WrappedRange,p=a.DOMException,m=s.DomPosition,n=a.features,v="Control",l=s.getDocument,N=s.getBody,E=f.rangesEqual,t=e(window,"getSelection"),y=h.isHostObject(document,"selection"),n.implementsWinGetSelection=t,S=(n.implementsDocSelection=y)&&(!t||a.config.preferTextRange))R=j,a.isSelectionValid=function(e){var e=V(e,"isSelectionValid").document,t=e.selection;return"None"!=t.type||l(t.createRange().parentElement())==e};else{if(!t)return c.fail("Neither document.selection or window.getSelection() detected."),!1;R=function(e){return V(e,"getWinSelection").getSelection()},a.isSelectionValid=function(){return!0}}if(!(t=(a.getNativeSelection=R)()))return c.fail("Native selection was null (possibly issue 138?)"),!1;if(T=a.createNativeRange(document),w=N(document),A=h.areHostProperties(t,["anchorNode","focusNode","anchorOffset","focusOffset"]),n.selectionHasAnchorAndFocus=A,x=e(t,"extend"),n.selectionHasExtend=x,O=e(t,"setBaseAndExtent"),n.selectionHasSetBaseAndExtent=O,b=typeof t.rangeCount==u,n.selectionHasRangeCount=b,I=!(P=!1),_=x?function(e,t){var n=f.getRangeDocument(t),n=a.createRange(n);n.collapseToPoint(t.endContainer,t.endOffset),e.addRange(ee(n)),e.extend(t.startContainer,t.startOffset)}:null,h.areHostMethods(t,["addRange","getRangeAt","removeAllRanges"])&&typeof t.rangeCount==u&&n.implementsDomRange){var $,q,Y,r,G,Q,o,K,i=window.getSelection();if(i){for(K=1<($=i.rangeCount),q=[],Y=U(i),r=0;r<$;++r)q[r]=i.getRangeAt(r);for(Q=(G=s.createTestElement(document,"",!1)).appendChild(document.createTextNode("   ")),(o=document.createRange()).setStart(Q,1),o.collapse(!0),i.removeAllRanges(),i.addRange(o),I=1==i.rangeCount,i.removeAllRanges(),K||(K=window.navigator.appVersion.match(/Chrome\/(.*?) /),P=!(K&&36<=parseInt(K[1]))&&(K=o.cloneRange(),o.setStart(Q,0),K.setEnd(Q,3),K.setStart(Q,2),i.addRange(o),i.addRange(K),2==i.rangeCount)),s.removeNode(G),i.removeAllRanges(),r=0;r<$;++r)0==r&&Y?_?_(i,q[r]):(a.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),i.addRange(q[r])):i.addRange(q[r])}}function X(e,t,n){var r=n?"end":"start",n=n?"start":"end";e.anchorNode=t[r+"Container"],e.anchorOffset=t[r+"Offset"],e.focusNode=t[n+"Container"],e.focusOffset=t[n+"Offset"]}function Z(e){e.anchorNode=e.focusNode=null,e.anchorOffset=e.focusOffset=0,e.rangeCount=0,e.isCollapsed=!0,e._ranges.length=0,J(e)}function J(e){e.type=0==e.rangeCount?"None":C(e)?"Caret":"Range"}function ee(e){var t;return e instanceof f?((t=a.createNativeRange(e.getDocument())).setEnd(e.endContainer,e.endOffset),t.setStart( -e.startContainer,e.startOffset)):e instanceof g?t=e.nativeRange:n.implementsDomRange&&e instanceof s.getWindow(e.startContainer).Range&&(t=e),t}function te(e){var t=e.getNodes();if(!function(e){if(e.length&&1==e[0].nodeType){for(var t=1,n=e.length;t=this.rangeCount)throw new p("INDEX_SIZE_ERR");return this._ranges[e].cloneRange()},S)H=function(e){var t;a.isSelectionValid(e.win)?t=e.docSelection.createRange():(t=N(e.win.document).createTextRange()).collapse(!0),e.docSelection.type==v?oe(e):ne(t)?re(e,t):Z(e)};else if(e(t,"getRangeAt")&&typeof t.rangeCount==u)H=function(e){if(D&&y&&e.docSelection.type==v)oe(e);else if(e._ranges.length=e.rangeCount=e.nativeSelection.rangeCount,e.rangeCount){for(var t=0,n=e.rangeCount;t(s.isCharacterDataNode(e)?e:e.childNodes).length)throw new p("INDEX_SIZE_ERR")} -function he(r){return function(e,t){var n;this.rangeCount?(n=this.getRangeAt(0))["set"+(r?"Start":"End")](e,t):(n=a.createRange(this.win.document)).setStartAndEnd(e,t),this.setSingleRange(n,this.isBackward())}}function fe(e){var t,n,r=[],o=new m(e.anchorNode,e.anchorOffset),i=new m(e.focusNode,e.focusOffset),a="function"==typeof e.getName?e.getName():"Selection";if("undefined"!=typeof e.rangeCount)for(t=0,n=e.rangeCount;t>>1^3988292384:t>>>=1;n[r]=t>>>0}return n}()}function f(e){return e.replace(//g,">")}function d(e,t){var n,r,o,i,a,s,c;switch(t=t||[],n=e.nodeType,o=(r=e.childNodes).length,i=[n,e.nodeName,o].join(":"),s=a="",n){case 3:a=f(e.nodeValue);break;case 8:a="\x3c!--"+f(e.nodeValue)+"--\x3e";break;default:a="<"+i+">",s=""}for(a&&t.push(a),c=0;c>6|192,63&t|128):n.push(t>>12|224,t>>6&63|128,63&t|128);return n}(e),n=-1,r=h(),o=0,i=t.length;o>>8^r[255&(n^t[o])];return(-1^n)>>>0},l=s.dom,i=/^([^,]+),([^,\{]+)(\{([^}]+)\})?$/,a="rangySerializedSelection",o.extend(s,{serializePosition:r,deserializePosition:g,serializeRange:p,deserializeRange:m,canDeserializeRange:R,serializeSelection:C,deserializeSelection:v,canDeserializeSelection:function(e,t,n){var r,o,i,a;for(t?r=n?n.document:l.getDocument(t):t=(n=n||window).document.documentElement,i=0,a=(o=e.split("|")).length;it&&--e.offset}),h.removeNode(e)}function _(e,t){for(var n,r=e,o=e.parentNode,i=h.getNodeIndex(e),e=!0,a=t,s=[];n=r.firstChild;)P(n,o,i++,a),s.push(n);return e&&I(r,a),s}function D(e,t){var n=e.cloneRange();return n.selectNodeContents(t),""!=((t=n.intersection(e))?t.toString():"")}function B(e){for(var t,n,r=e.getNodes([3]),o=0;(t=r[o])&&!D(e,t);)++o;for(n=r.length-1;(t=r[n])&&!D(e,t);)--n;return r.slice(o,n+1)}function k(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,r,o=0,i=e.attributes.length;or&&(--e.offset,e.offset==r+1&&ne.start},isContiguousWith:function(e){return this.start==e.end||this.end==e.start},union:function(e){return new w(Math.min(this.start,e.start),Math.max(this.end,e.end))},intersection:function(e){return new w(Math.max(this.start,e.start),Math.min(this.end,e.end))},getComplements:function(e){var t=[];if(this.start>=e.start){if(this.end<=e.end)return[];t.push(new w(e.end,this.end))}else t.push(new w(this.start,Math.min(this.end,e.start))),this.end>e.end&&t.push(new w(e.end,this.end));return t},toString:function(){return"[CharacterRange("+this.start+", "+this.end+")]"}},w.fromCharacterRange=function(e){return new w(e.start,e.end)},o={rangeToCharacterRange:function(e,t){t=e.getBookmark(t);return new w(t.start,t.end)},characterRangeToRange:function(e,t,n){e=y.createRange(e);return e.moveToBookmark({start:t.start,end:t.end,containerNode:n}),e},serializeSelection:function(e,t){for(var n=e.getAllRanges(),r=n.length,o=[],i=1==r&&e.isBackward(),a=0,s=n.length;a1

",!0),P=e.firstChild,(t=m.getSelection()).collapse(P.lastChild,2),t.setStart(P.firstChild,0),e.innerHTML="1
",t.collapse(e,2),t.setStart(e.firstChild,0),P=1==(""+t).length,e.innerHTML="1

1

",t.collapse(e,2),t.setStart(e.firstChild,0),b=1==(""+t).length,a.removeNode(e),t.removeAllRanges(),f={caseSensitive:!(h={en:{wordRegex:/[a-z0-9]+('[a-z0-9]+)*/gi,includeTrailingSpace:!(u={includeBlockContentTrailingSpace:!(l={includeBlockContentTrailingSpace:!0,includeSpaceBeforeBr:!0,includeSpaceBeforeBlock:!0,includePreLineTrailingSpace:!0,ignoreCharacters:""}),includeSpaceBeforeBr:!P,includeSpaceBeforeBlock:!b,includePreLineTrailingSpace:!0}),tokenizer:function(e,t){var n,r,o,i,a=e.join(""),s=[];function c(e,t,n){s.push({start:e,end:t,isWord:n})}for(r=0;n=t.wordRegex.exec(a);){if(i=(o=n.index)+n[0].length,r
'),this.editor=a.Node.create('
'),this.textareaLabel=a.one('[for="'+this.get("elementid")+'"]'),this.textareaLabel&&(this.textareaLabel.generateID(),this.editor.setAttribute("aria-labelledby",this.textareaLabel.get("id"))),this.setupToolbar(),this.setupTemplateEditor(),this.disableCssStyling(),document.queryCommandSupported("DefaultParagraphSeparator")&&document.execCommand("DefaultParagraphSeparator",!1,"p"),this.textarea.get("parentNode").insert(this._wrapper,this.textarea),this.textarea.hide(),this.updateFromTextArea(),this.setupTextareaNavigation(),this._preventEnter(),this.publishEvents(),this.setupSelectionWatchers(),this.setupAutomaticPolling(),this.setupPlugins())},destructor:function(){a.Array.each(this.plugins,function(e,t){e.destroy(),this.plugins[t]=undefined},this),new a.EventHandle(this._eventHandles).detach(),this.textarea.show(),this._wrapper.remove(!0),a.M.editor_ousupsub.removeEditorReference(this.get("elementid"),this)},focus:function(){return this.editor.focus(),this},publishEvents:function(){return this.publish("change",{broadcast:!0,preventable:!0}),this.publish("pluginsloaded",{fireOnce:!0}),this.publish("ousupsub:selectionchanged",{prefix:"ousupsub"}),this},setupAutomaticPolling:function(){return this._registerEventHandle(this.editor.on(["keyup","cut"],this.updateOriginal,this)),this._registerEventHandle(this.editor.on(["keypress","delete"],this.cleanEditorHTMLSimple,this)),this._registerEventHandle(this.editor.on("paste",this.pasteCleanup,this)),this._registerEventHandle(this.editor.on("drop",this.updateOriginalDelayed,this)),this},updateOriginalDelayed:function(){return setTimeout(a.bind(this.updateOriginal,this),0),this},setupPlugins:function(){var e,t,i,s,n;for(t in this.plugins={},e=this.get("plugins"))if((i=e[t]).plugins)for(s in i.plugins)"superscript"===(n=i.plugins[s]).name?this.plugins.superscript=new a.M.editor_ousupsub.EditorPlugin({name:"superscript",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,exec:"superscript",tags:"sup",keys:["94"],icon:"e/superscript",keyDescription:"Shift + ^ or Up arrow"}):"subscript"===n.name&&(this.plugins.subscript=new a.M.editor_ousupsub.EditorPlugin({name:"subscript",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,exec:"subscript",tags:"sub",keys:["95"],icon:"e/subscript",keyDescription:"Shift + _ or Down arrow"}));return this._undoStack=[],this._redoStack=[],this.plugins.undo=new a.M.editor_ousupsub.EditorPlugin({name:"undo",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,keys:["90"],callback:this._undoHandler}),this.plugins.redo=new a.M.editor_ousupsub.EditorPlugin({name:"redo",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,keys:["89"],callback:this._redoHandler}),this.on("pluginsloaded",function(){this._addToUndo(this._getHTML()),this.on("ousupsub:selectionchanged",this._changeListener,this)},this),this._updateButtonsStates(),this.setupUndoHandlers(),this.fire("pluginsloaded"),this},setupUndoHandlers:function(){return this._registerEventHandle(this._wrapper.delegate("key",this._undoHandler,"down:90+ctrl","."+e,this)),this._registerEventHandle(this._wrapper.delegate("key",this._redoHandler,"down:89+ctrl","."+e,this)),this},pluginEnabled:function(e){return!!this.plugins[e]},enablePlugins:function(e){this._setPluginState(!0,e)},disablePlugins:function(e){this._setPluginState(!1,e)},_setPluginState:function(e,t){var i=e?"enableButtons":"disableButtons";t?this.plugins[t][i]():a.Object.each(this.plugins,function(e){e[i]()},this)},_registerEventHandle:function(e){this._eventHandles.push(e)},setupToolbar:function(){return this.toolbar=a.Node.create(''),this._wrapper.appendChild(this.toolbar),this.textareaLabel&&this.toolbar.setAttribute("aria-labelledby",this.textareaLabel.get("id")),this.setupToolbarNavigation(),this},setupToolbarNavigation:function(){return this._wrapper.delegate("key",this.toolbarKeyboardNavigation,"down:37,39","."+t,this),this._wrapper.delegate("focus",function(e){this._setTabFocus(e.currentTarget)},"."+t+" button",this),this},toolbarKeyboardNavigation:function(e){e.preventDefault();var t=this.toolbar.all("button"),i=1,s=e.target.ancestor("button",!0);37===e.keyCode&&(i=-1),(e=this._findFirstFocusable(t,s,i))&&(e.focus(),this._setTabFocus(e))},_findFirstFocusable:function(e,t,i){var s,n,o=0,r=e.indexOf(t);for(r<-1&&(r=0);o=e.size()&&(r=0),o++,!(s=e.item(r)).hasAttribute("hidden")&&!s.hasAttribute("disabled")&&!s.ancestor(".ousupsub_group").hasAttribute("hidden")){n=s;break}return n},checkTabFocus:function(){var e;return this._tabFocus&&(!( -this._tabFocus.hasAttribute("disabled")||this._tabFocus.hasAttribute("hidden")||this._tabFocus.ancestor(".ousupsub_group").hasAttribute("hidden"))||(e=this._findFirstFocusable(this.toolbar.all("button"),this._tabFocus,-1))&&(this._tabFocus.compareTo(document.activeElement)&&e.focus(),this._setTabFocus(e))),this},_setTabFocus:function(e){return this._tabFocus&&this._tabFocus.setAttribute("tabindex","-1"),this._tabFocus=e,this._tabFocus.setAttribute("tabindex",0),this.toolbar.setAttribute("aria-activedescendant",this._tabFocus.generateID()),this},disableCssStyling:function(){try{document.execCommand("styleWithCSS",0,!1)}catch(e){try{document.execCommand("useCSS",0,!0)}catch(t){try{document.execCommand("styleWithCSS",!1,!1)}catch(i){}}}},setupTemplateEditor:function(){var t,e,i,s,n=a.Node.create('
');n.appendChild(this.editor),this._wrapper.appendChild(n),i=6*this.textarea.getAttribute("cols")+41+"px",this.editor.setStyle("width",i),this.editor.setStyle("minWidth",i),this.editor.setStyle("maxWidth",i),i=this.textarea.getAttribute("rows"),i=(t=6*i+13)-6+"px",this.editor.setStyle("height",e=t-10+"px"),this.editor.setStyle("minHeight",e),this.editor.setStyle("maxHeight",e),this.editor.setStyle("line-height",i),n.setStyle("minHeight",i=1+t+"px"),this.textareaLabel.setStyle("display","inline-block"),this.textareaLabel.setStyle("margin",0),this.textareaLabel.setStyle("height",i),this.textareaLabel.setStyle("minHeight",i),this.textareaLabel.setStyle("maxheight",i),this.textareaLabel.hasClass("accesshide")?(this.textareaLabel.removeClass("accesshide"),this.textareaLabel.setStyle("visibility","hidden"),this._wrapper.setStyle("margin-left",-parseInt(this.textareaLabel.get("offsetWidth")))):(this.textareaLabel.getDOMNode().parentNode.style.paddingBottom=e,this.textareaLabel.setStyle("vertical-align","bottom")),n="#"+(s=this).get("elementid").replace(/:/g,"\\:")+"editable",a.on("contentready",function(){s.textareaLabel.setStyle("line-height",s.editor.getComputedStyle("line-height"));var e=1+t+parseInt(s.toolbar.get("offsetHeight"));s._wrapper.setStyle("height",e),s._wrapper.setStyle("minHeight",e),s._wrapper.setStyle("maxHeight",e),a.UA.ie&&"hidden"===s.textareaLabel.getComputedStyle("visibility")&&s._wrapper.setStyle("vertical-align",parseInt(s.toolbar.get("offsetHeight"))-1+"px")},n)},_getEmptyContent:function(){return""},updateFromTextArea:function(){this.editor.setHTML(""),this.editor.append(this.textarea.get("value")),this.cleanEditorHTML(),""===this.editor.getHTML()&&this.editor.setHTML(this._getEmptyContent())},updateOriginal:function(){var e=this.textarea.get("value"),t=this.getCleanHTML();return""===t&&this.isActive()&&(t=this._getEmptyContent()),e!==(t=(t=this._removeUnicodeCharacters(t)).trim())&&(this.textarea.set("value",t),this.fire("change")),this},setupTextareaNavigation:function(){return this._registerEventHandle(this._wrapper.delegate("key",this.textareaKeyboardNavigation,"down:38,40","."+e,this)),this._registerEventHandle(this._wrapper.delegate("key",this.textareaKeyboardNavigation,"press:94, 95","."+e,this)),this},textareaKeyboardNavigation:function(e){var t;e.preventDefault(),YUI.Env.UA.android||this.isActive()||this.focus(),t="",38===(e=(e=window.event||e).keyCode||e.charCode)||94===e?t="superscript":40!==e&&95!==e||(t="subscript"),t&&this._applyTextCommand(t,1)},_preventEnter:function(){var e="keypress";(a.UA.webkit||a.UA.ie)&&(e="keydown"),this.editor.on(e,function(e){e=window.event||e;13===e.keyCode&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},this)},_addToRedo:function(e){this._redoStack.push(e)},_addToUndo:function(e,t){for(void 0===t&&(t=!1),this._undoStack[this._undoStack.length-1]!==e&&(this._undoStack.push(e),t&&(this._redoStack=[]));this._undoStack.length>this._maxUndos;)this._undoStack.shift()},_getHTML:function(){return this.getCleanHTML()},_getRedo:function(){return this._redoStack.pop()},_getUndo:function(e){if(1===this._undoStack.length)return this._undoStack[0];var t=this._undoStack.pop();return t===e&&(t=this._undoStack.pop()),0===this._undoStack.length&&this._addToUndo(t),t},_restoreValue:function(e){this.editor.setHTML(e),this._addToUndo(e)},_updateButtonsStates:function(){1"===t?"":(0===t.indexOf("")&&(e=t.length-("".length+"".length),t=t.substr("".length,e)),this._cleanHTML(t))},cleanEditorHTML:function(){return this.editor.set("innerHTML",this._cleanHTML(this.editor.get("innerHTML"))),this},cleanEditorHTMLSimple:function(){var e=window.rangy.saveSelection();return this.editor.set("innerHTML",this._cleanHTMLSimple(this.editor.get("innerHTML"))),window.rangy.restoreSelection(e,!0),this},_cleanHTMLSimple:function(e){return this._filterContentWithRules(e,[{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>(.+)<\/span>/gi,replace:"$1"}])},_cleanHTML:function(e){return this._filterContentWithRules(e,[{ -regex:/]*>( |\s)*<\/p>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/ /gi,replace:" "},{regex:/<\/sup>(\s*)+/gi,replace:"$1"},{regex:/<\/sub>(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+<\/sup>/gi,replace:"$1"},{regex:/(\s*)+<\/sub>/gi,replace:"$1"},{regex:/
/gi,replace:""},{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/]*>[\s\S]*?<\/script>/gi,replace:""},{regex:/<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi,replace:""},{regex:/<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi,replace:""},{regex:/<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi,replace:""},{regex:/<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi,replace:""},{regex:/<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi,replace:""},{regex:/<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi,replace:""},{regex:/<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi,replace:""},{regex:/<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi,replace:""},{regex:/<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi,replace:""},{regex:/<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi,replace:""},{regex:/<\/?(?:var|wbr|video)[^>]*?>/gi,replace:""},{regex:/<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi,replace:""},{regex:/<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:jsl|nobr)[^>]*?>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*>( |\s)*<\/sup>/gi,replace:""},{regex:/]*>( |\s)*<\/sub>/gi,replace:""},{regex:/(.*?)<\/xmlns.*?>/gi,replace:"$1"}])},cleanEditorHTMLEmptySupAndSubTags:function(){var e=window.rangy.saveSelection(),t=this.editor.get("innerHTML"),t=this._cleanEditorHTMLEmptySupAndSubTags(t),t=this._removeUnicodeCharacters(t);return this.editor.set("innerHTML",t),window.rangy.restoreSelection(e,!0),this},_cleanEditorHTMLEmptySupAndSubTags:function(e){return this._filterContentWithRules(e,[{regex:/]*>(|\s)*<\/su[bp]>/gi,replace:""}])},_filterContentWithRules:function(e,t){for(var i=0,i=0;i([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}]),0!==(e=this._cleanHTML(e)).length&&e.match(/\S/)?((t=document.createElement("div")).innerHTML=e,e=t.innerHTML,t.innerHTML="",e=this._filterContentWithRules(e,[{regex:/(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9\-]*)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9\-]*)+/gi,replace:"$1"},{regex:/
]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}]),this._cleanHTML(e)):e):""},_applyTextCommand:function(e,t){var i;if(t){if("superscript"===(i=this.getCursorTag())&&e===i||"subscript"===i&&e===i)return;if("superscript"===i&&"subscript"===e?e="superscript":"subscript"===i&&"superscript"===e&&(e="subscript"),!this.pluginEnabled(e))return}document.execCommand(e,!1,null),(t=window.rangy.getSelection()).isCollapsed&&(this.cleanEditorHTMLEmptySupAndSubTags(),e=this.insertContentAtFocusPoint("<"+(i="superscript"===e?"sup":"sub")+">\ufeff"),(i=window.rangy.createRange()).selectNode(e._node.childNodes[0]),this.setSelection([i]),t.rangeCount&&t.collapseToEnd()),this._normaliseTextarea(),this.cleanEditorHTMLSimple(),this.saveSelection(),this.updateOriginal()},getCursorTag:function(){var e="text",t=window.rangy.getSelection(),i=t.focusNode.nodeName.toLowerCase(),s=t.focusNode.parentNode.nodeName.toLowerCase(),n=""; -return t.focusNode.childNodes&&t.focusNode.childNodes[t.focusOffset-1]&&(n=t.focusNode.childNodes[t.focusOffset-1].nodeName.toLowerCase()),"sup"===i||"sup"===s||"sup"===n?e="superscript":"sub"!==i&&"sub"!==s&&"sub"!==n||(e="subscript"),e},_normaliseTextarea:function(){var e,t,i=window.rangy.saveSelection(),s=this._getEditorNode();for(this._removeSingleNodesByName(s,"br"),e=["p","b","i","u","ul","ol","li"],t=0;t
", expected: "1", event: "paste"}, - - // Spans with rangy and without. - {input: "12345", expected: "12345"}, // Keep sub. - - /* Check for disallowed characters */ - {input: "

12

", expected: "12"}, - {input: "12", expected: "12"}, - {input: "12", expected: "12"}, - {input: "12", expected: "12"}, - {input: "1
", expected: "1"}, - {input: "1
", expected: "1"}, - {input: "1.2x103 g", expected: "1.2x103 g"}, // Empty trailing sup tag removed - {input: "1.2x103 g", expected: "1.2x103 g"}, // Empty trailing sub tag removed - {input: "mm s", expected: "mm s"} // Empty special xmlns tag from browser plugin. - ]; - - // Elements to remove completely including contents. - var disallowed_characters_and_text = ['style','script']; - for (var x = 0; x < disallowed_characters_and_text.length; x++) { - testcases[testcases.length] = {input: "<" + disallowed_characters_and_text[x] + ">1", expected: ""}; - } - - // Elements to remove while contents are left. - var disallowed_characters = ['br','title','std','font','html','body','link', - 'a','ul','li','ol','b','i','u','ul','ol','li','img', - 'abbr','address','area','article','address','article', - 'aside','audio','base','bdi','bdo','blockquote','button', - 'canvas','caption','cite','code','col','colgroup','content', - 'data','datalist','dd','decorator','del','details','dialog', - 'dfn','div','dl','dt','element','em','embed','fieldset', - 'figcaption','figure','footer','form','h1','h2','h3','h4', - 'h5','h6','head','header','hgroup','hr','iframe','input', - 'ins','kbd','keygen','label','legend','main','map','mark', - 'menu','menuitem','meter','meta','nav','noscript', - 'object','optgroup','option','output','optgroup','options', - 'p','param','pre','progress','q','rp','rt','rtc','ruby', - 'samp','section','select','shadow','small','source','std', - 'strong','summary','span','table','tbody','td','template', - 'textarea','time','tfoot','th','thead','tr','track','var', - 'wbr','video', - // Deprecated elements - 'acronym','applet','basefont','big','blink','center','dir', - 'frame','frameset','isindex','listing','noembed', - 'spacer','strike','tt','xmp', - // Elements from common sites including google.com. - 'jsl','nobr' - ]; - for (var x = 0; x < disallowed_characters.length; x++) { - testcases[testcases.length] = {input: "<" + disallowed_characters[x] + ">1", expected: "1"}; - } - - - function init_ousupsub(id, params) { - M.str = { - "moodle": { - "error": "Error", - "morehelp": "More help" - }, - "editor_ousupsub": { - "editor_command_keycode":"Cmd + {$a}", - "editor_control_keycode":"Ctrl + {$a}", - "editor_shift_keycode":"Shift + {$a}", - "plugin_title_shortcut":"{$a->title} [{$a->shortcut}]", - "subscript":"Subscript", - "superscript":"Superscript" - }, - } - - plugins = []; - if (params.superscript) { - plugins.push({"name": "superscript", "params": []}); - } - if (params.subscript) { - plugins.push({"name": "subscript", "params": []}); - } - - Y.M.editor_ousupsub.createEditor( - {"elementid":id,"content_css":"","contextid":0,"language":"en", - "directionality":"ltr","plugins":[{"group":"style1","plugins":plugins}],"pageHash":""}); - }; - - // Initialise an editor to test with. - init_ousupsub("id_description_editor", {"subscript":true, "superscript":true}); - - function get_editor(id) { - return Y.M.editor_ousupsub.getEditor(id); - } - - function escape_html(str) { - return str.replace(/&/g,'&').replace(//g,'>').replace(/ /g,'.'); - } - - function run_tests(Y) { - var editor = get_editor("id_description_editor"); - for(var i = 0; i < testcases.length; i++) { - run_test(editor, testcases[i]); - } - } - - - function run_test(editor, test) { - var input = test.input - if(test.event && test.event == 'paste') { - input = editor._cleanPasteHTML(input); - } - editor.editor.set('innerHTML', input); - // Fake the subscript button. - editor.plugins.subscript._applyTextCommand(); - // Fake submit - editor.updateFromTextArea(); - test.actual = editor.editor.get('innerHTML'); - test.matched = test.expected == test.actual; - } - - function update_display() { - // Update table. - var table = Y.one('#results'); - var showPasses = false; - var numberPassed = 0, numberFailed = 0; - var summary = ''; - var summaryNode = Y.one('#summary'); - for(var i = 0; i < testcases.length; i++) { - test = testcases[i]; - test.matched ? ++numberPassed : ++numberFailed; - - if (!showPasses && test.matched) { - continue; - } - - var rowText = ''; - rowText += '' + escape_html(test.input) + ''; - rowText += '' + escape_html(test.expected) + ''; - rowText += '' + escape_html(test.actual) + ''; - rowText += '' + test.matched + ''; - rowText += ''; - var row = Y.Node.create(rowText); - table.appendChild(row); - } - - // Explain if there are no failures to show. - if (!numberFailed) { - var rowText = ''; - rowText += 'There were no failures to show.'; - rowText += ''; - var row = Y.Node.create(rowText); - table.appendChild(row); - } - - summary = 'Of ' + testcases.length + ' tests run there were ' + numberPassed + ' test passes and '; - summary += numberFailed + ' failures.'; - summaryNode.set('innerHTML', summary); - - var statusNode = Y.one('#status'); - var status = numberFailed ? 'failure' : 'success'; - statusNode.set('innerHTML', 'Overall status = ' + status + ''); - } - - run_tests(); - update_display(); -}); diff --git a/thirdpartylibs.xml b/thirdpartylibs.xml deleted file mode 100644 index 8b19f67..0000000 --- a/thirdpartylibs.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - yui/src/rangy/js/*.* - Rangy - MIT - 1.3.1 - - - diff --git a/version.php b/version.php index 678456c..a998061 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022041100; +$plugin->version = 2024112600; $plugin->requires = 2020061500; $plugin->component = 'editor_ousupsub'; $plugin->maturity = MATURITY_STABLE; diff --git a/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-debug.js b/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-debug.js deleted file mode 100644 index 17c515e..0000000 --- a/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-debug.js +++ /dev/null @@ -1,3456 +0,0 @@ -YUI.add('moodle-editor_ousupsub-editor', function (Y, NAME) { - -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * The ousupsub WYSIWG pluggable editor, written for Moodle. - * - * @module moodle-editor_ousupsub-editor - * @package editor_ousupsub - * @copyright 2013 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @main moodle-editor_ousupsub-editor - */ - -/** - * @module moodle-editor_ousupsub-editor - * @submodule editor-base - */ - -var LOGNAME = 'moodle-editor_ousupsub-editor'; -var CSS = { - CONTENT: 'editor_ousupsub_content', - CONTENTWRAPPER: 'editor_ousupsub_content_wrap', - EDITORWRAPPER: '.editor_ousupsub_content', - TOOLBAR: 'editor_ousupsub_toolbar', - WRAPPER: 'editor_ousupsub', - HIGHLIGHT: 'highlight' - }; - -/** - * The ousupsub editor for Moodle. - * - * @namespace M.editor_ousupsub - * @class Editor - * @constructor - * @uses M.editor_ousupsub.EditorClean - * @uses M.editor_ousupsub.EditorSelection - */ - -function Editor() { - Editor.superclass.constructor.apply(this, arguments); -} - -Y.extend(Editor, Y.Base, { - - /** - * List of known block level tags. - * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements". - * - * @property BLOCK_TAGS - * @type {Array} - */ - BLOCK_TAGS : [ - 'address', - 'article', - 'aside', - 'audio', - 'blockquote', - 'canvas', - 'dd', - 'div', - 'dl', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hgroup', - 'hr', - 'noscript', - 'ol', - 'output', - 'p', - 'pre', - 'section', - 'table', - 'tfoot', - 'ul', - 'video' - ], - - PLACEHOLDER_CLASS: 'ousupsub-tmp-class', - ALL_NODES_SELECTOR: '[style],font[face]', - FONT_FAMILY: 'fontFamily', - - /** - * The wrapper containing the editor. - * - * @property _wrapper - * @type Node - * @private - */ - _wrapper: null, - - /** - * A reference to the content editable Node. - * - * @property editor - * @type Node - */ - editor: null, - - /** - * A reference to the toolbar Node. - * - * @property toolbar - * @type Node - */ - toolbar: null, - - /** - * A reference to the original text area. - * - * @property textarea - * @type Node - */ - textarea: null, - - /** - * A reference to the label associated with the original text area. - * - * @property textareaLabel - * @type Node - */ - textareaLabel: null, - - /** - * A reference to the list of plugins. - * - * @property plugins - * @type object - */ - plugins: null, - - /** - * Event Handles to clear on editor destruction. - * - * @property _eventHandles - * @private - */ - _eventHandles: null, - - /** - * The current focal point for tabbing. - * - * @property _tabFocus - * @type Node - * @default null - * @private - */ - _tabFocus: null, - - /** - * The maximum saved number of undo steps. - * - * @property _maxUndos - * @type {Number} The maximum number of saved undos. - * @default 40 - * @private - */ - _maxUndos: 40, - - /** - * History of edits. - * - * @property _undoStack - * @type {Array} The elements of the array are the html strings that make a snapshot - * @private - */ - _undoStack: null, - - /** - * History of edits. - * - * @property _redoStack - * @type {Array} The elements of the array are the html strings that make a snapshot - * @private - */ - _redoStack: null, - - initializer: function() { - // Note - it is not safe to use a CSS selector like '#' + elementid because the id - // may have colons in it - e.g. quiz. - this.textarea = Y.one(document.getElementById(this.get('elementid'))); - - if (!this.textarea) { - // No text area found. - Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'), - 'error', LOGNAME); - return; - } - - // Add the editor to the manager. - Y.M.editor_ousupsub.addEditorReference(this.get('elementid'), this); - - this._eventHandles = []; - - this._wrapper = Y.Node.create('
'); - this.editor = Y.Node.create('
'); - - // Add a labelled-by attribute to the contenteditable. - this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]'); - if (this.textareaLabel) { - this.textareaLabel.generateID(); - this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id")); - } - - // Add everything to the wrapper. - this.setupToolbar(); - - this.setupTemplateEditor(); - - // Disable odd inline CSS styles. - this.disableCssStyling(); - - // Use paragraphs not divs. - if (document.queryCommandSupported('DefaultParagraphSeparator')) { - document.execCommand('DefaultParagraphSeparator', false, 'p'); - } - - // Add the toolbar and editable zone to the page. - this.textarea.get('parentNode').insert(this._wrapper, this.textarea); - - // Hide the old textarea. - this.textarea.hide(); - - // Copy the text to the contenteditable div. - this.updateFromTextArea(); - - // Add keyboard navigation for the textarea. - this.setupTextareaNavigation(); - - // Prevent carriage return to produce a new line. - this._preventEnter(); - - // Publish the events that are defined by this editor. - this.publishEvents(); - - // Add handling for saving and restoring selections on cursor/focus changes. - this.setupSelectionWatchers(); - - // Add polling to update the textarea periodically when typing long content. - this.setupAutomaticPolling(); - - // Setup plugins. - this.setupPlugins(); - }, - - destructor: function() { - // Destroy each of the plugins - they may have destruction phases. - Y.Array.each(this.plugins, function(item, key) { - item.destroy(); - this.plugins[key] = undefined; - }, this); - - // Clear any event handles we created. - new Y.EventHandle(this._eventHandles).detach(); - - // Return the editor back to it's original state. - this.textarea.show(); - this._wrapper.remove(true); - - // Finally remove this reference from the manager. - Y.M.editor_ousupsub.removeEditorReference(this.get('elementid'), this); - }, - - /** - * Focus on the editable area for this editor. - * - * @method focus - * @chainable - */ - focus: function() { - this.editor.focus(); - return this; - }, - - /** - * Publish events for this editor instance. - * - * @method publishEvents - * @private - * @chainable - */ - publishEvents: function() { - /** - * Fired when changes are made within the editor. - * - * @event change - */ - this.publish('change', { - broadcast: true, - preventable: true - }); - - /** - * Fired when all plugins have completed loading. - * - * @event pluginsloaded - */ - this.publish('pluginsloaded', { - fireOnce: true - }); - - this.publish('ousupsub:selectionchanged', { - prefix: 'ousupsub' - }); - - return this; - }, - - /** - * Set up automated polling of the text area to update the textarea. - * - * @method setupAutomaticPolling - * @chainable - */ - setupAutomaticPolling: function() { - this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this)); - this._registerEventHandle(this.editor.on(['keypress', 'delete'], this.cleanEditorHTMLSimple, this)); - this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this)); - - // Call this.updateOriginal after dropped content has been processed. - this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this)); - - return this; - }, - - /** - * Calls updateOriginal on a short timer to allow native event handlers to run first. - * - * @method updateOriginalDelayed - * @chainable - */ - updateOriginalDelayed: function() { - setTimeout(Y.bind(this.updateOriginal, this), 0); - - return this; - }, - - setupPlugins: function() { - // Clear the list of plugins. - this.plugins = {}; - - var plugins = this.get('plugins'); - - var groupIndex, - group, - pluginIndex, - plugin; - - for (groupIndex in plugins) { - group = plugins[groupIndex]; - if (!group.plugins) { - // No plugins in this group - skip it. - continue; - } - for (pluginIndex in group.plugins) { - plugin = group.plugins[pluginIndex]; - if (plugin.name === 'superscript') { - this.plugins.superscript = new Y.M.editor_ousupsub.EditorPlugin({ - name: 'superscript', - group: group.group, - editor: this.editor, - toolbar: this.toolbar, - host: this, - exec: 'superscript', - tags: 'sup', - // Key code (up arrow) for the keyboard shortcut which triggers this button: - // Up arrow should be 38 but doesn't register and is handled elsewhere. - keys: ['94'], - icon: 'e/superscript', - keyDescription: "Shift + ^ or Up arrow" - }); - } else if (plugin.name === 'subscript') { - this.plugins.subscript = new Y.M.editor_ousupsub.EditorPlugin({ - name: 'subscript', - group: group.group, - editor: this.editor, - toolbar: this.toolbar, - host: this, - exec: 'subscript', - tags: 'sub', - // Key codes (underscore) for the keyboard shortcut which triggers this button: - // Down arrow should be 40 but doesn't register. - keys: ['95'], - icon: 'e/subscript', - keyDescription: "Shift + _ or Down arrow" - }); - } - } - } - - // Initialise the undo and redo stacks. - this._undoStack = []; - this._redoStack = []; - - // Add undo plugin - this.plugins.undo = new Y.M.editor_ousupsub.EditorPlugin({ - name: 'undo', - group: group.group, - editor: this.editor, - toolbar: this.toolbar, - host: this, - keys: ['90'], - callback: this._undoHandler - }); - - // Add redo plugin - this.plugins.redo = new Y.M.editor_ousupsub.EditorPlugin({ - name: 'redo', - group: group.group, - editor: this.editor, - toolbar: this.toolbar, - host: this, - keys: ['89'], - callback: this._redoHandler - }); - - // Enable the undo once everything has loaded. - this.on('pluginsloaded', function() { - // Adds the current value to the stack. - this._addToUndo(this._getHTML()); - this.on('ousupsub:selectionchanged', this._changeListener, this); - }, this); - - this._updateButtonsStates(); - this.setupUndoHandlers(); - - // Some plugins need to perform actions once all plugins have loaded. - this.fire('pluginsloaded'); - - return this; - }, - - /** - * Set up the watchers for undo/redo. - * - * @method setupUndoHandlers - * @chainable - */ - setupUndoHandlers: function() { - // Listen for ctrl+z and ctrl+y keys. - this._registerEventHandle(this._wrapper.delegate('key', - this._undoHandler, - 'down:90+ctrl', - '.' + CSS.CONTENT, - this)); - this._registerEventHandle(this._wrapper.delegate('key', - this._redoHandler, - 'down:89+ctrl', - '.' + CSS.CONTENT, - this)); - - return this; - }, - - pluginEnabled: function(plugin) { - return this.plugins[plugin] ? true : false; - }, - - enablePlugins: function(plugin) { - this._setPluginState(true, plugin); - }, - - disablePlugins: function(plugin) { - this._setPluginState(false, plugin); - }, - - _setPluginState: function(enable, plugin) { - var target = 'disableButtons'; - if (enable) { - target = 'enableButtons'; - } - - if (plugin) { - this.plugins[plugin][target](); - } else { - Y.Object.each(this.plugins, function(currentPlugin) { - currentPlugin[target](); - }, this); - } - }, - - /** - * Register an event handle for disposal in the destructor. - * - * @method _registerEventHandle - * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate. - * @private - */ - _registerEventHandle: function(handle) { - this._eventHandles.push(handle); - }, - - /** - * Setup the toolbar on the editor. - * - * @method setupToolbar - * @chainable - */ - setupToolbar: function() { - this.toolbar = Y.Node.create(''); - this._wrapper.appendChild(this.toolbar); - - if (this.textareaLabel) { - this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id")); - } - - // Add keyboard navigation for the toolbar. - this.setupToolbarNavigation(); - - return this; - }, - - /** - * Set up the watchers for toolbar navigation. - * - * @method setupToolbarNavigation - * @chainable - */ - setupToolbarNavigation: function() { - // Listen for Arrow left and Arrow right keys. - this._wrapper.delegate('key', - this.toolbarKeyboardNavigation, - 'down:37,39', - '.' + CSS.TOOLBAR, - this); - this._wrapper.delegate('focus', - function(e) { - this._setTabFocus(e.currentTarget); - }, '.' + CSS.TOOLBAR + ' button', this); - - return this; - }, - - /** - * Implement arrow key navigation for the buttons in the toolbar. - * - * @method toolbarKeyboardNavigation - * @param {EventFacade} e - the keyboard event. - */ - toolbarKeyboardNavigation: function(e) { - // Prevent the default browser behaviour. - e.preventDefault(); - - // On cursor moves we loops through the buttons. - var buttons = this.toolbar.all('button'), - direction = 1, - button, - current = e.target.ancestor('button', true); - - if (e.keyCode === 37) { - // Moving left so reverse the direction. - direction = -1; - } - - button = this._findFirstFocusable(buttons, current, direction); - if (button) { - button.focus(); - this._setTabFocus(button); - } else { - Y.log("Unable to find a button to focus on", 'debug', LOGNAME); - } - }, - - /** - * Find the first focusable button. - * - * @param {NodeList} buttons A list of nodes. - * @param {Node} startAt The node in the list to start the search from. - * @param {Number} direction The direction in which to search (1 or -1). - * @return {Node | Undefined} The Node or undefined. - * @method _findFirstFocusable - * @private - */ - _findFirstFocusable: function(buttons, startAt, direction) { - var checkCount = 0, - group, - candidate, - button, - index; - - // Determine which button to start the search from. - index = buttons.indexOf(startAt); - if (index < -1) { - Y.log("Unable to find the button in the list of buttons", 'debug', LOGNAME); - index = 0; - } - - // Try to find the next. - while (checkCount < buttons.size()) { - index += direction; - if (index < 0) { - index = buttons.size() - 1; - } else if (index >= buttons.size()) { - // Handle wrapping. - index = 0; - } - - candidate = buttons.item(index); - - // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. - checkCount++; - - // Loop while: - // * we haven't checked every button; - // * the button is hidden or disabled; - // * the group is hidden. - if (candidate.hasAttribute('hidden') || candidate.hasAttribute('disabled')) { - continue; - } - group = candidate.ancestor('.ousupsub_group'); - if (group.hasAttribute('hidden')) { - continue; - } - - button = candidate; - break; - } - - return button; - }, - - /** - * Check the tab focus. - * - * When we disable or hide a button, we should call this method to ensure that the - * focus is not currently set on an inaccessible button, otherwise tabbing to the toolbar - * would be impossible. - * - * @method checkTabFocus - * @chainable - */ - checkTabFocus: function() { - if (this._tabFocus) { - if (this._tabFocus.hasAttribute('disabled') || this._tabFocus.hasAttribute('hidden') - || this._tabFocus.ancestor('.ousupsub_group').hasAttribute('hidden')) { - // Find first available button. - var button = this._findFirstFocusable(this.toolbar.all('button'), this._tabFocus, -1); - if (button) { - if (this._tabFocus.compareTo(document.activeElement)) { - // We should also move the focus, because the inaccessible button also has the focus. - button.focus(); - } - this._setTabFocus(button); - } - } - } - return this; - }, - - /** - * Sets tab focus for the toolbar to the specified Node. - * - * @method _setTabFocus - * @param {Node} button The node that focus should now be set to - * @chainable - * @private - */ - _setTabFocus: function(button) { - if (this._tabFocus) { - // Unset the previous entry. - this._tabFocus.setAttribute('tabindex', '-1'); - } - - // Set up the new entry. - this._tabFocus = button; - this._tabFocus.setAttribute('tabindex', 0); - - // And update the activedescendant to point at the currently selected button. - this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID()); - - return this; - }, - - /** - * Disable CSS styling. Use HTML elements instead. - * - * @method disableCssStyling - */ - disableCssStyling: function() { - try { - document.execCommand("styleWithCSS", 0, false); - } catch (e1) { - try { - document.execCommand("useCSS", 0, true); - } catch (e2) { - try { - document.execCommand('styleWithCSS', false, false); - } catch (e3) { - // We did our best. - } - } - } - }, - - /** - * Setup Template for Editor. - * Because of the limitation of css when make align question text and answer text on same baseline in differences qtypes, - * also themes. Example font-size, line-height difference each other is very small: 0.1px. - * So we need to use this function to get the computed style to apply to both question text and answer text, - * to make sure they have the same baseline. - * - * - * @method setupTemplateEditor - */ - setupTemplateEditor: function() { - var content = Y.Node.create('
'); - content.appendChild(this.editor); - this._wrapper.appendChild(content); - // Set the visible width and height. - var width = (this.textarea.getAttribute('cols') * 6 + 41) + 'px'; - this.editor.setStyle('width', width); - this.editor.setStyle('minWidth', width); - this.editor.setStyle('maxWidth', width); - var rows = this.textarea.getAttribute('rows'); - var height = (rows * 6 + 13); - var heightEditor = (height - 10) + 'px'; - var lineHeightEditor = (height - 6) + 'px'; - // Style the editor. - this.editor.setStyle('height', heightEditor); - this.editor.setStyle('minHeight', heightEditor); - this.editor.setStyle('maxHeight', heightEditor); - // Align the content in the editor with the textarea label. - this.editor.setStyle('line-height', lineHeightEditor); - // Style the textarea label with the content editor. - var heightContent = (height + 1) + 'px'; - content.setStyle('minHeight', heightContent); - // Align the textarea label with the content editor. - this.textareaLabel.setStyle('display', 'inline-block'); - this.textareaLabel.setStyle('margin', 0); - this.textareaLabel.setStyle('height', heightContent); - this.textareaLabel.setStyle('minHeight', heightContent); - this.textareaLabel.setStyle('maxheight', heightContent); - // Align for the case using Supsub on the editor. - if (this.textareaLabel.hasClass('accesshide')) { - this.textareaLabel.removeClass('accesshide'); - this.textareaLabel.setStyle('visibility', 'hidden'); - this._wrapper.setStyle('margin-left', -parseInt(this.textareaLabel.get('offsetWidth'))); - } else { - // Get parent node of the label. - var labelParentNode = this.textareaLabel.getDOMNode().parentNode; - labelParentNode.style.paddingBottom = heightEditor; - this.textareaLabel.setStyle('vertical-align', 'bottom'); - } - // Update the height of the editor and label for correct align after document ready. - var self = this; - var selectorEditor = '#' + self.get('elementid').replace(/:/g, "\\:") + 'editable'; - Y.on('contentready', function() { - self.textareaLabel.setStyle('line-height', self.editor.getComputedStyle('line-height')); - var heightWrapper = height + 1 + parseInt(self.toolbar.get('offsetHeight')); - self._wrapper.setStyle('height', heightWrapper); - self._wrapper.setStyle('minHeight', heightWrapper); - self._wrapper.setStyle('maxHeight', heightWrapper); - if (Y.UA.ie && self.textareaLabel.getComputedStyle('visibility') === 'hidden') { - // IE have problem with vertical-align: bottom. We need to calculate the exact pixel for it. - self._wrapper.setStyle('vertical-align', parseInt(self.toolbar.get('offsetHeight')) - 1 + 'px'); - } - }, selectorEditor); - }, - - /** - * Return the appropriate empty content value for the current browser. - * - * Different browsers use a different content when they are empty and - * we must set this reliable across the board. - * - * @method _getEmptyContent - * @return String The content to use representing no user-provided content - * @private - */ - _getEmptyContent: function() { - return ''; - }, - - /** - * Copy and clean the text from the textarea into the contenteditable div. - * - * If the text is empty, provide a default paragraph tag to hold the content. - * - * @method updateFromTextArea - * @chainable - */ - updateFromTextArea: function() { - // Clear it first. - this.editor.setHTML(''); - - // Copy text to editable div. - this.editor.append(this.textarea.get('value')); - - // Clean it. - this.cleanEditorHTML(); - - // Insert a paragraph in the empty contenteditable div. - if (this.editor.getHTML() === '') { - this.editor.setHTML(this._getEmptyContent()); - } - }, - - /** - * Copy the text from the contenteditable to the textarea which it replaced. - * - * @method updateOriginal - * @chainable - */ - updateOriginal : function() { - // Get the previous and current value to compare them. - var oldValue = this.textarea.get('value'), - newValue = this.getCleanHTML(); - - if (newValue === "" && this.isActive()) { - // The content was entirely empty so get the empty content placeholder. - newValue = this._getEmptyContent(); - } - - newValue = this._removeUnicodeCharacters(newValue); - newValue = newValue.trim(); - - // Only call this when there has been an actual change to reduce processing. - if (oldValue !== newValue) { - // Insert the cleaned content. - this.textarea.set('value', newValue); - - // Trigger handlers for this action. - this.fire('change'); - } - - return this; - }, - - /** - * Set up the watchers for textarea navigation. - * - * @method setupTextareaNavigation - * @chainable - */ - setupTextareaNavigation: function() { - // Listen for Up and down Arrow keys. - this._registerEventHandle(this._wrapper.delegate('key', - this.textareaKeyboardNavigation, - 'down:38,40', - '.' + CSS.CONTENT, - this)); - - // Listen for hat (^), underscore. - this._registerEventHandle(this._wrapper.delegate('key', - this.textareaKeyboardNavigation, - 'press:94, 95', - '.' + CSS.CONTENT, - this)); - - return this; - }, - - /** - * Implement arrow key navigation for the buttons in the toolbar. - * - * @method textareaKeyboardNavigation - * @param {EventFacade} e - the keyboard event. - */ - textareaKeyboardNavigation: function(e) { - - // Prevent the default browser behaviour. - e.preventDefault(); - - // From editor-plugins_buttons::callbackWrapper(). - if (!(YUI.Env.UA.android || this.isActive())) { - // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion - // changes the cursor position. - // If we save that change, then when we restore the change later we get put in the wrong place. - // Android is fine to save the selection without the editor being in focus. - this.focus(); - } - - var command = '', mode = 1; - // Cross browser event object. - var evt = window.event || e; - var code = evt.keyCode ? evt.keyCode : evt.charCode; - // Call superscript. - if ((code === 38) || (code === 94)) { - command = 'superscript'; - // Call subscript. - } else if ((code === 40) || (code === 95)) { - command = 'subscript'; - } - - if (!command) { - return; - } - - this._applyTextCommand(command, mode); - }, - - /** - * Prevent carriage return to produce a new line. - */ - _preventEnter: function() { - var keyEvent = 'keypress'; - if (Y.UA.webkit || Y.UA.ie) { - keyEvent = 'keydown'; - } - this.editor.on(keyEvent, function(e) { - //Cross browser event object. - var evt = window.event || e; - if (evt.keyCode === 13) { // Enter. - // do nothing. - if(!evt.preventDefault) { - evt.returnValue = false; - return; - } - evt.preventDefault(); - } - }, this); - }, - - /** - * Adds an element to the redo stack. - * - * @method _addToRedo - * @private - * @param {String} html The HTML content to save. - */ - _addToRedo: function(html) { - this._redoStack.push(html); - }, - - /** - * Adds an element to the undo stack. - * - * @method _addToUndo - * @private - * @param {String} html The HTML content to save. - * @param {Boolean} [clearRedo=false] Whether or not we should clear the redo stack. - */ - _addToUndo: function(html, clearRedo) { - var last = this._undoStack[this._undoStack.length - 1]; - - if (typeof clearRedo === 'undefined') { - clearRedo = false; - } - - if (last !== html) { - this._undoStack.push(html); - if (clearRedo) { - this._redoStack = []; - } - } - - while (this._undoStack.length > this._maxUndos) { - this._undoStack.shift(); - } - }, - - /** - * Get the editor HTML. - * - * @method _getHTML - * @private - * @return {String} The HTML. - */ - _getHTML: function() { - return this.getCleanHTML(); - }, - - /** - * Get an element on the redo stack. - * - * @method _getRedo - * @private - * @return {String} The HTML to restore, or undefined. - */ - _getRedo: function() { - return this._redoStack.pop(); - }, - - /** - * Get an element on the undo stack. - * - * @method _getUndo - * @private - * @param {String} current The current HTML. - * @return {String} The HTML to restore. - */ - _getUndo: function(current) { - if (this._undoStack.length === 1) { - return this._undoStack[0]; - } - - var last = this._undoStack.pop(); - if (last === current) { - // Oops, the latest undo step is the current content, we should unstack once more. - // There is no need to do that in a loop as the same stack should never contain duplicates. - last = this._undoStack.pop(); - } - - // We always need to keep the first element of the stack. - if (this._undoStack.length === 0) { - this._addToUndo(last); - } - - return last; - }, - - /** - * Restore a value from a stack. - * - * @method _restoreValue - * @private - * @param {String} html The HTML to restore in the editor. - */ - _restoreValue: function(html) { - this.editor.setHTML(html); - // We always add the restored value to the stack, otherwise an event could think that - // the content has changed and clear the redo stack. - this._addToUndo(html); - }, - - /** - * Update the states of the buttons. - * - * @method _updateButtonsStates - * @private - */ - _updateButtonsStates: function() { - if (this._undoStack.length > 1) { - this.enablePlugins('undo'); - } else { - this.disablePlugins('undo'); - } - - if (this._redoStack.length > 0) { - this.enablePlugins('redo'); - } else { - this.disablePlugins('redo'); - } - }, - - /** - * Handle a click on undo - * - * @method _undoHandler - * @param {Event} The click event - * @private - */ - _undoHandler: function(e) { - e.preventDefault(); - var html = this._getHTML(), - undo = this._getUndo(html); - - // Edge case, but that could happen. We do nothing when the content equals the undo step. - if (html === undo) { - this._updateButtonsStates(); - return; - } - - // Restore the value. - this._restoreValue(undo); - - // Add to the redo stack. - this._addToRedo(html); - - // Update the button states. - this._updateButtonsStates(); - }, - - /** - * Handle a click on redo - * - * @method _redoHandler - * @param {Event} The click event - * @private - */ - _redoHandler: function(e) { - e.preventDefault(); - var html = this._getHTML(), - redo = this._getRedo(); - - // Edge case, but that could happen. We do nothing when the content equals the redo step. - if (redo === undefined || html === redo) { - this._updateButtonsStates(); - return; - } - // Restore the value. - this._restoreValue(redo); - - // Update the button states. - this._updateButtonsStates(); - }, - - /** - * If we are significantly different from the last saved version, save a new version. - * - * @method _changeListener - * @param {EventFacade} The click event - * @private - */ - _changeListener: function(e) { - if (e.event && e.event.type.indexOf('key') !== -1) { - // These are the 4 arrow keys. - if ((e.event.keyCode !== 39) && - (e.event.keyCode !== 37) && - (e.event.keyCode !== 40) && - (e.event.keyCode !== 38)) { - // Skip this event type. We only want focus/mouse/arrow events. - return; - } - } - - this._addToUndo(this._getHTML(), true); - this._updateButtonsStates(); - } - -}, { - NS: 'editor_ousupsub', - ATTRS: { - /** - * The unique identifier for the form element representing the editor. - * - * @attribute elementid - * @type String - * @writeOnce - */ - elementid: { - value: null, - writeOnce: true - }, - - /** - * The contextid of the form. - * - * @attribute contextid - * @type Integer - * @writeOnce - */ - contextid: { - value: null, - writeOnce: true - }, - - /** - * Plugins with their configuration. - * - * The plugins structure is: - * - * [ - * { - * "group": "groupName", - * "plugins": [ - * "pluginName": { - * "configKey": "configValue" - * }, - * "pluginName": { - * "configKey": "configValue" - * } - * ] - * }, - * { - * "group": "groupName", - * "plugins": [ - * "pluginName": { - * "configKey": "configValue" - * } - * ] - * } - * ] - * - * @attribute plugins - * @type Object - * @writeOnce - */ - plugins: { - value: {}, - writeOnce: true - } - } -}); - -// The Editor publishes custom events that can be subscribed to. -Y.augment(Editor, Y.EventTarget); - -Y.namespace('M.editor_ousupsub').Editor = Editor; -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * @module moodle-editor_ousupsub-editor - * @submodule clean - */ - -/** - * Functions for the ousupsub editor to clean the generated content. - * - * See {{#crossLink "M.editor_ousupsub.Editor"}}{{/crossLink}} for details. - * - * @namespace M.editor_ousupsub - * @class EditorClean - */ - -function EditorClean() {} - -EditorClean.ATTRS = { -}; - -EditorClean.prototype = { - /** - * Clean the generated HTML content without modifying the editor content. - * - * This includes removes all YUI ids from the generated content. - * - * @return {string} The cleaned HTML content. - */ - getCleanHTML: function() { - // Clone the editor so that we don't actually modify the real content. - var editorClone = this.editor.cloneNode(true), - html, startParagraph = '', endParagraph = ''; - - // Remove all YUI IDs. - Y.each(editorClone.all('[id^="yui"]'), function(node) { - node.removeAttribute('id'); - }); - - editorClone.all('.ousupsub_control').remove(true); - html = editorClone.get('innerHTML'); - - // Revert untouched editor contents to an empty string. - if (html === '' || html === '
') { - return ''; - } - - // Revert untouched editor contents to an empty string. - if (html.indexOf(startParagraph) === 0) { - var length = html.length - (startParagraph.length + endParagraph.length); - html = html.substr(startParagraph.length, length); - } - - // Remove any and all nasties from source. - return this._cleanHTML(html); - }, - - /** - * Clean the HTML content of the editor. - * - * @method cleanEditorHTML - * @chainable - */ - cleanEditorHTML: function() { - this.editor.set('innerHTML', this._cleanHTML(this.editor.get('innerHTML'))); - return this; - }, - - /** - * Clean the HTML content of the editor. - * - * @method cleanEditorHTML - * @chainable - */ - cleanEditorHTMLSimple: function() { - // Using saveSelection as it produces a more consistent experience. - var selection = window.rangy.saveSelection(); - - // Update the content. - this.editor.set('innerHTML', this._cleanHTMLSimple(this.editor.get('innerHTML'))); - - // Restore the selection, and collapse to end. - window.rangy.restoreSelection(selection, true); - return this; - }, - - /** - * Clean the specified HTML content and remove any content which could cause issues. - * - * @method _cleanHTML - * @private - * @param {String} content The content to clean - * @return {String} The cleaned HTML - */ - _cleanHTMLSimple: function(content) { - // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc. - - var rules = [ - //Remove empty spans, but not ones from Rangy. - {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>(.+)<\/span>/gi, replace: "$1"} - ]; - - return this._filterContentWithRules(content, rules); - }, - - /** - * Clean the specified HTML content and remove any content which could cause issues. - * - * @method _cleanHTML - * @private - * @param {String} content The content to clean - * @return {String} The cleaned HTML - */ - _cleanHTML: function(content) { - // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc. - - var rules = [ - //Remove empty paragraphs. - {regex: /]*>( |\s)*<\/p>/gi, replace: ""}, - - //Remove attributes on sup and sub tags. - {regex: /]*( |\s)*>/gi, replace: ""}, - {regex: /]*( |\s)*>/gi, replace: ""}, - - //Replace   with space. - {regex: / /gi, replace: " "}, - - //Combine matching tags with spaces in between. - {regex: /<\/sup>(\s*)+/gi, replace: "$1"}, - {regex: /<\/sub>(\s*)+/gi, replace: "$1"}, - - //Move spaces after start sup and sub tags to before. - {regex: /(\s*)+/gi, replace: "$1"}, - {regex: /(\s*)+/gi, replace: "$1"}, - - //Move spaces before end sup and sub tags to after. - {regex: /(\s*)+<\/sup>/gi, replace: "$1"}, - {regex: /(\s*)+<\/sub>/gi, replace: "$1"}, - - //Remove empty br tags. - {regex: /
/gi, replace: ""}, - - // Remove any style blocks. Some browsers do not work well with them in a contenteditable. - // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015. - // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work" - {regex: /]*>[\s\S]*?<\/style>/gi, replace: ""}, - - // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout. - {regex: /)/gi, replace: ""}, - - // Remove elements that can not contain visible text. - {regex: /]*>[\s\S]*?<\/script>/gi, replace: ""}, - - // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html" - // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link. - {regex: /<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi, replace: ""}, - // Source:"https://developer.mozilla.org/en/docs/Web/HTML/Element" - // Remove all elements except sup and sub. - {regex: /<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:var|wbr|video)[^>]*?>/gi, replace: ""}, - - // Deprecated elements that might still be used by older sites. - {regex: /<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi, replace: ""}, - {regex: /<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi, replace: ""}, - - // Elements from common sites including google.com. - {regex: /<\/?(?:jsl|nobr)[^>]*?>/gi, replace: ""}, - - {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi, replace: "$1"}, - - // Remove empty spans, but not ones from Rangy. - {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi, replace: ""}, - {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi, replace: "$1"}, - - // Remove empty sup and sub tags that appear after pasting text. - {regex: /]*>( |\s)*<\/sup>/gi, replace: ""}, - {regex: /]*>( |\s)*<\/sub>/gi, replace: ""}, - - // Remove special xml namespace tag xmlns generate by browser plugin. - {regex: /(.*?)<\/xmlns.*?>/gi, replace: "$1"} - ]; - - return this._filterContentWithRules(content, rules); - }, - - /** - * Clean the HTML content of the editor by removing empty sup and sub tags. - * - * @method cleanEditorHTMLEmptySupAndSubTags - * @chainable - */ - cleanEditorHTMLEmptySupAndSubTags: function() { - // Using saveSelection as it produces a more consistent experience. - var selection = window.rangy.saveSelection(); - - var newValue = this.editor.get('innerHTML'); - newValue = this._cleanEditorHTMLEmptySupAndSubTags(newValue); - newValue = this._removeUnicodeCharacters(newValue); - // Update the content. - this.editor.set('innerHTML', newValue); - - // Restore the selection, and collapse to end. - window.rangy.restoreSelection(selection, true); - return this; - }, - - /** - * Clean the specified HTML content and remove any content which could cause issues. - * - * @method _cleanHTML - * @private - * @param {String} content The content to clean - * @return {String} The cleaned HTML - */ - _cleanEditorHTMLEmptySupAndSubTags: function(content) { - // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc. - - var rules = [ - //Remove empty sup tags. - {regex: /]*>(|\s)*<\/su[bp]>/gi, replace: ""} - ]; - - return this._filterContentWithRules(content, rules); - }, - - /** - * Take the supplied content and run on the supplied regex rules. - * - * @method _filterContentWithRules - * @private - * @param {String} content The content to clean - * @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...] - * @return {String} The cleaned content - */ - _filterContentWithRules: function(content, rules) { - var i = 0; - for (i = 0; i < rules.length; i++) { - content = content.replace(rules[i].regex, rules[i].replace); - } - return content; - }, - - /** - * Intercept and clean html paste events. - * - * @method pasteCleanup - * @param {Object} sourceEvent The YUI EventFacade object - * @return {Boolean} True if the passed event should continue, false if not. - */ - pasteCleanup: function(sourceEvent) { - // We only expect paste events, but we will check anyways. - if (sourceEvent.type === 'paste') { - // The YUI event wrapper doesn't provide paste event info, so we need the underlying event. - var event = sourceEvent._event; - // Check if we have a valid clipboardData object in the event. - // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access. - if (event && event.clipboardData && event.clipboardData.getData) { - // Check if there is HTML type to be pasted, this is all we care about. - var types = event.clipboardData.types; - var isHTML = false; - // Different browsers use different things to hold the types, so test various functions. - if (!types) { - isHTML = false; - } else if (typeof types.contains === 'function') { - isHTML = types.contains('text/html'); - } else if (typeof types.indexOf === 'function') { - isHTML = (types.indexOf('text/html') > -1); - if (!isHTML) { - if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) { - // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything. - this.fallbackPasteCleanupDelayed(); - return true; - } - } - } else { - // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback. - this.fallbackPasteCleanupDelayed(); - return true; - } - - if (isHTML) { - // Get the clipboard content. - var content; - try { - content = event.clipboardData.getData('text/html'); - } catch (error) { - // Something went wrong. Fallback. - this.fallbackPasteCleanupDelayed(); - return true; - } - - // Stop the original paste. - sourceEvent.preventDefault(); - - // Scrub the paste content. - content = this._cleanPasteHTML(content); - - // Save the current selection. - // Using saveSelection as it produces a more consistent experience. - var selection = window.rangy.saveSelection(); - - // Insert the content. - this.insertContentAtFocusPoint(content); - - // Restore the selection, and collapse to end. - window.rangy.restoreSelection(selection); - window.rangy.getSelection().collapseToEnd(); - - // Update the text area. - this.updateOriginal(); - this._normaliseTextarea(); - return false; - } else { - // Due to poor cross browser clipboard compatibility, the - // failure to find HTML doesn't mean it isn't there. - // Wait for the clipboard event to finish then fallback - // clean the entire editor. - this.fallbackPasteCleanupDelayed(); - return true; - } - } else { - // If we reached a here, this probably means the browser has limited (or no) clipboard support. - // Wait for the clipboard event to finish then fallback. - this.fallbackPasteCleanupDelayed(); - return true; - } - } - - // We should never get here - we must have received a non-paste event for some reason. - // Um, just call updateOriginalDelayed() - it's safe. - this.updateOriginalDelayed(); - return true; - }, - - /** - * Cleanup code after a paste event if we couldn't intercept the paste content. - * - * @method fallbackPasteCleanup - * @chainable - */ - fallbackPasteCleanup: function() { - Y.log('Using fallbackPasteCleanup for ousupsub cleanup', 'debug', LOGNAME); - - // Save the current selection (cursor position). - var selection = window.rangy.saveSelection(); - - // Get, clean, and replace the content in the editable. - var content = this.editor.get('innerHTML'); - this.editor.set('innerHTML', this._cleanPasteHTML(content)); - - // Update the textarea. - this.updateOriginal(); - - // Restore the selection (cursor position). - window.rangy.restoreSelection(selection, true); - - return this; - }, - - /** - * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete. - * - * @method fallbackPasteCleanupDelayed - * @chainable - */ - fallbackPasteCleanupDelayed: function() { - setTimeout(Y.bind(this.fallbackPasteCleanup, this), 0); - - return this; - }, - - /** - * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip. - * - * @method _cleanPasteHTML - * @private - * @param {String} content The html content to clean - * @return {String} The cleaned HTML - */ - _cleanPasteHTML: function(content) { - // Return an empty string if passed an invalid or empty object. - if (!content || content.length === 0) { - return ""; - } - - // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc). - var rules = [ - // Stuff that is specifically from MS Word and similar office packages. - // Remove all garbage after closing html tag. - {regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""}, - // Remove if comment blocks. - {regex: //gi, replace: ""}, - // Remove start and end fragment comment blocks. - {regex: //gi, replace: ""}, - // Remove any xml blocks. - {regex: /]*>[\s\S]*?<\/xml>/gi, replace: ""}, - // Remove any <\?xml> blocks. - {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""}, - // Remove , <\o:blah>. - {regex: /<\/?\w+:[^>]*>/gi, replace: ""} - ]; - - // Apply the first set of harsher rules. - content = this._filterContentWithRules(content, rules); - - // Apply the standard rules, which mainly cleans things like headers, links, and style blocks. - content = this._cleanHTML(content); - - // Check if the string is empty or only contains whitespace. - if (content.length === 0 || !content.match(/\S/)) { - return content; - } - - // Now we let the browser normalize the code by loading it into the DOM and then get the html back. - // This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code. - var holder = document.createElement('div'); - holder.innerHTML = content; - content = holder.innerHTML; - // Free up the DOM memory. - holder.innerHTML = ""; - - // Run some more rules that care about quotes and whitespace. - rules = [ - // Remove MSO-blah, MSO:blah in style attributes. Only removes one or more that appear in succession. - {regex: /(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi, replace: "$1"}, - // Remove MSO classes in class attributes. Only removes one or more that appear in succession. - {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9\-]*)+/gi, replace: "$1"}, - // Remove Apple- classes in class attributes. Only removes one or more that appear in succession. - {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9\-]*)+/gi, replace: "$1"}, - // Remove OLE_LINK# anchors that may litter the code. - {regex: /]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""} - ]; - - // Apply the rules. - content = this._filterContentWithRules(content, rules); - - // Reapply the standard cleaner to the content. - content = this._cleanHTML(content); - - return content; - }, - - /** - * Apply the given document.execCommand and tidy up the editor dom afterwards. - * - * @method _applyTextCommand - * @private - * @param int mode (optional) default is button (0), keyboard is 1 - * @return void - */ - _applyTextCommand: function(command, mode) { - var selection, tag; - // Handle keyboard mode. - if (mode) { - tag = this.getCursorTag(); - if (tag === 'superscript' && command === tag || - tag === 'subscript' && command === tag) { - return; // Do nothing. - } else if (tag === 'superscript' && command === 'subscript') { - command = 'superscript'; - } else if (tag === 'subscript' && command === 'superscript') { - command = 'subscript'; - } - - if (!this.pluginEnabled(command)) { - return; - } - } - - // Apply command. - document.execCommand(command, false, null); - - // If nothing is selected add a relevant tag. - selection = window.rangy.getSelection(); - // If it's a collapsed selection the cursor is in the editor but no selection has been made. - if (selection.isCollapsed) { - - // Remove empty sup and sub tags. - this.cleanEditorHTMLEmptySupAndSubTags(); - // Insert tag at cursor focus point. - tag = command === 'superscript' ? 'sup' : 'sub'; - //  is is the Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF). Used - // by TinyMCE to add empty sup/sub tags when nothing is selected. This causes lint - // errors but I couldn't find a better solution. - // http://stackoverflow.com/questions/9691771/why-is-65279-appearing-in-my-html. - var node = this.insertContentAtFocusPoint('<' + tag + '>'); - var range = window.rangy.createRange(); - range.selectNode(node._node.childNodes[0]); - this.setSelection([range]); - // Restore the selection (cursor position). - if (selection.rangeCount) { - selection.collapseToEnd(); - } - } - this._normaliseTextarea(); - this.cleanEditorHTMLSimple(); - - // And mark the text area as updated. - // Save selection after changes to the DOM. If you don't do this here, - // subsequent calls to restoreSelection() will fail expecting the - // previous DOM state. - this.saveSelection(); - this.updateOriginal(); - }, - - /** - * What type of tag surrounds the cursor. - * - * @method _getCursorTag - * @private - * @return string - */ - getCursorTag: function() { - var tag = 'text'; - var selection = window.rangy.getSelection(); - var nodeName = selection.focusNode.nodeName.toLowerCase(); - var parentNodeName = selection.focusNode.parentNode.nodeName.toLowerCase(); - - var childNodeName = ''; - if (selection.focusNode.childNodes && selection.focusNode.childNodes[selection.focusOffset-1]) { - childNodeName = selection.focusNode.childNodes[selection.focusOffset-1].nodeName.toLowerCase(); - } - if (nodeName === 'sup' || parentNodeName === 'sup' || childNodeName === 'sup') { - tag = 'superscript'; - } else if (nodeName === 'sub' || parentNodeName === 'sub' || childNodeName === 'sub') { - tag = 'subscript'; - } - return tag; - }, - - /** - * Get a normalised array of the currently selected nodes. Chrome splits text nodes - * at the end of each selection and also creates empty text nodes. Fix these changes - * and provide a standard array of nodes to match the existing selection to. - * - * @method _normaliseTextarea - * @private - * @return string - */ - _normaliseTextarea: function() { - // Save the current selection (cursor position). - var selection = window.rangy.saveSelection(); - // Remove all the span tags added to the editor textarea by the browser. - // Get the html directly inside the editor

tag and remove span tags from the html inside it. - - var editor_node = this._getEditorNode(); - this._removeSingleNodesByName(editor_node, 'br'); - - // Remove specific tags that can be added through keyboard shortcuts. - var tagsToRemove = ['p', 'b', 'i', 'u', 'ul', 'ol', 'li']; - for (var i = 0; i < tagsToRemove.length; i++) { - this._removeNodesByName(editor_node, tagsToRemove[i]); - } - this._normaliseTagInTextarea('sup'); - this._normaliseTagInTextarea('sub'); - this._removeNodesByName(editor_node, 'span'); - - // Restore the selection (cursor position). - window.rangy.restoreSelection(selection, true); - - // Normalise the editor html. - editor_node.normalize(); - }, - - /** - * Remove all tags nested inside other tags of the same name. No nesting of - * similar tags e.g. is not allowed. - * - * @method _normaliseTagInTextarea - * @private - * @param string name Name of tag to normalise. - * @return string. - */ - _normaliseTagInTextarea: function(name) { - var nodes = [], container = this._getEditorNode(), parentNode, removeParent = false, node; - - // Remove nested nodes. - /* - * Where the node.firstChild == nodes[i+1] since it ignores text elements - * I know it's the first node. Since the two elements match they should cancel - * each other out. Currently we remove only the child sup. We should remove - * both and move their children out. - */ - // Nodelists change as nodes are added and removed. Use an array of nodes instead. - nodes = this._copyArray(container.querySelectorAll(name), nodes); - - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - parentNode = node.parentNode; - removeParent = false; - if (parentNode === container ) { - continue; - } - if (parentNode.firstChild === node && parentNode.lastChild === node && - parentNode.nodeName.toLowerCase() === name) { - removeParent = true; - } - if (!removeParent && node && parentNode.nodeName.toLowerCase() === name) { - removeParent = true; - this._splitParentNode(parentNode, name); - } - this._removeNodesByName(node, name); - if (removeParent) { - this._removeNodesByName(parentNode, name); - } - } - - // Combine Sibling nodes. - // Get a new node array and fill with the a fresh nodelist. - nodes = []; - nodes = this._copyArray(container.querySelectorAll(name), nodes); - - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (!node.previousSibling || node.previousSibling.nodeName.toLowerCase() !== name) { - continue; - } - this._mergeNodes(node, node.previousSibling); - } - }, - - /** - * Merge the from and to nodes by moving all elements in from node to the to node. - * Append nodes in order to the to node. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _mergeNodes - * @private - * @return void. - */ - _mergeNodes: function(from, to) { - var nodes = []; - var merge_nodes = from.childNodes; - - // Node lists reduce in size as nodes are removed. Use an array of nodes instead. - for (var i = 0; i < merge_nodes.length; i++) { - nodes.push(merge_nodes.item(i)); - } - - for (i = 0; i < nodes.length; i++) { - to.appendChild(nodes[i]); - } - this._removeNode(from); - }, - - /** - * Split the parent node into two with the node with the given name in the middle. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _splitParentNode - * @private - * @return void. - */ - _splitParentNode: function(container_node, name) { - var nodes = [], node, nodesToAppend = []; - nodes = this._copyArray(container_node.childNodes, nodes); - - var i,j; - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - nodesToAppend = []; - if (node.nodeName.toLowerCase() === name) { - nodesToAppend = this._copyArray(node.childNodes, nodesToAppend); - } else { - nodesToAppend[0] = document.createElement(name); - nodesToAppend[0].appendChild(node); - } - for (j = 0; j < nodesToAppend.length; j++) { - container_node.parentNode.insertBefore(nodesToAppend[j], container_node); - } - } - }, - - /** - * Copy array values from a dom node list to the given array. - * - * A dom node list reduces as children are removed. Copying to a standard array provides - * an array that doesn't change. - * @method _copyArray - * @private - * @return array. - */ - _copyArray: function(from, to) { - for (var i = 0; i < from.length; i++) { - to.push(from[i]); - } - - return to; - }, - - /** - * Move all elements in container node before the reference node. - * If recursive mode is equired then where childnodes exist that are not - * text nodes. Move their children and remove the existing node. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _removeNodesByName - * @private - * @return void. - */ - _removeNodesByName: function(container_node, name) { - var node, remove_node = container_node.nodeName.toLowerCase() === name; - var nodes = []; - var container_nodes = container_node.childNodes; - - // Don't remove the span used by rangy to save and restore the user selection. - if (container_node.nodeName.toLowerCase() === 'span' && - container_node.id.indexOf('selectionBoundary_') > -1) { - remove_node = false; - } - - nodes = this._copyArray(container_nodes, nodes); - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (node.childNodes && node.childNodes.length) { - this._removeNodesByName(node, name); - - } - if (remove_node) { - var parentNode = container_node.parentNode; - parentNode.insertBefore(node, container_node); - } - - } - if (remove_node) { - this._removeNode(container_node); - } - }, - - /** - * Recursively remove any tag with the given name. Removes child nodes too. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _removeSingleNodesByName - * @private - * @return void. - */ - _removeSingleNodesByName: function(container_node, name) { - if (!container_node.childNodes) { - return; - } - var node; - var nodes = []; - nodes = this._copyArray(container_node.childNodes, nodes); - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (node.childNodes && node.childNodes.length) { - this._removeSingleNodesByName(node, name); - } - - if (node.nodeName.toLowerCase() === name) { - this._removeNode(node); - } - } - }, - - /** - * Remove a dom node in a cross browser way. - * - * @method _removeNode - * @private - * @return bool. - */ - _removeNode: function(node) { - if(!node.remove) { - return node.parentNode.removeChild(node); - } - return node.remove(); - }, - - /** - * Get the editor object. - * - * @method _getEditor - * @private - * @return node. - */ - _getEditor: function(host) { - if (!host) { - host = this.get('host'); - } - - return this; - }, - - /** - * Get the node containing the editor html to be updated. - * - * @method _getEditorNode - * @private - * @return node. - */ - _getEditorNode: function(host) { - return this._getEditor(host).editor._node; - }, - - /** - * Remove specific unicode characters from the given string. - * - * @method _removeUnicodeCharacters - * @private - * @return string. - */ - _removeUnicodeCharacters: function(text) { - var values = []; - for ( var i = 0; i < text.length; i++ ) { - if (text.charCodeAt(i) == "65279") { - continue; - } - values.push(text.charAt(i)); - } - return values.join(''); - } -}; - -Y.Base.mix(Y.M.editor_ousupsub.Editor, [EditorClean]); -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * @module moodle-editor_ousupsub-editor - * @submodule selection - */ - -/** - * Selection functions for the ousupsub editor. - * - * See {{#crossLink "M.editor_ousupsub.Editor"}}{{/crossLink}} for details. - * - * @namespace M.editor_ousupsub - * @class EditorSelection - */ - -function EditorSelection() {} - -EditorSelection.ATTRS = { -}; - -EditorSelection.prototype = { - - /** - * List of saved selections per editor instance. - * - * @property _selections - * @private - */ - _selections: null, - - /** - * A unique identifier for the last selection recorded. - * - * @property _lastSelection - * @param lastselection - * @type string - * @private - */ - _lastSelection: null, - - /** - * Whether focus came from a click event. - * - * This is used to determine whether to restore the selection or not. - * - * @property _focusFromClick - * @type Boolean - * @default false - * @private - */ - _focusFromClick: false, - - /** - * Set up the watchers for selection save and restoration. - * - * @method setupSelectionWatchers - * @chainable - */ - setupSelectionWatchers: function() { - // Save the selection when a change was made. - this._registerEventHandle(this.on('ousupsub:selectionchanged', this.saveSelection, this)); - - this._registerEventHandle(this.editor.on('focus', this.restoreSelection, this)); - - // Do not restore selection when focus is from a click event. - this._registerEventHandle(this.editor.on('mousedown', function() { - this._focusFromClick = true; - }, this)); - - // Copy the current value back to the textarea when focus leaves us and save the current selection. - this._registerEventHandle(this.editor.on('blur', function() { - // Clear the _focusFromClick value. - this._focusFromClick = false; - - // Update the original text area. - this.updateOriginal(); - }, this)); - - this._registerEventHandle(this.editor.on(['keyup', 'focus'], function(e) { - setTimeout(Y.bind(this._hasSelectionChanged, this, e), 0); - }, this)); - - // To capture both mouseup and touchend events, we need to track the gesturemoveend event in standAlone mode. Without - // standAlone, it will only fire if we listened to a gesturemovestart too. - this._registerEventHandle(this.editor.on('gesturemoveend', function(e) { - setTimeout(Y.bind(this._hasSelectionChanged, this, e), 0); - }, { - standAlone: true - }, this)); - - return this; - }, - - /** - * Work out if the cursor is in the editable area for this editor instance. - * - * @method isActive - * @return {boolean} - */ - isActive: function() { - var range = window.rangy.createRange(), - selection = window.rangy.getSelection(); - - if (!selection.rangeCount) { - // If there was no range count, then there is no selection. - return false; - } - - // We can't be active if the editor doesn't have focus at the moment. - if (!document.activeElement || - !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) { - return false; - } - - // Check whether the range intersects the editor selection. - range.selectNode(this.editor.getDOMNode()); - return range.intersectsRange(selection.getRangeAt(0)); - }, - - /** - * Create a cross browser selection object that represents a YUI node. - * - * @method getSelectionFromNode - * @param {Node} YUI Node to base the selection upon. - * @return {[rangy.Range]} - */ - getSelectionFromNode: function(node) { - var range = window.rangy.createRange(); - range.selectNode(node.getDOMNode()); - return [range]; - }, - - /** - * Save the current selection to an internal property. - * - * This allows more reliable return focus, helping improve keyboard navigation. - * - * Should be used in combination with {{#crossLink "M.editor_ousupsub.EditorSelection/restoreSelection"}}{{/crossLink}}. - * - * @method saveSelection - */ - saveSelection: function() { - if (this.isActive()) { - this._selections = this.getSelection(); - } - }, - - /** - * Restore any stored selection when the editor gets focus again. - * - * Should be used in combination with {{#crossLink "M.editor_ousupsub.EditorSelection/saveSelection"}}{{/crossLink}}. - * - * @method restoreSelection - */ - restoreSelection: function() { - if (!this._focusFromClick) { - if (this._selections) { - this.setSelection(this._selections); - } - } - this._focusFromClick = false; - }, - - /** - * Get the selection object that can be passed back to setSelection. - * - * @method getSelection - * @return {array} An array of rangy ranges. - */ - getSelection: function() { - return window.rangy.getSelection().getAllRanges(); - }, - - /** - * Check that a YUI node it at least partly contained by the current selection. - * - * @method selectionContainsNode - * @param {Node} The node to check. - * @return {boolean} - */ - selectionContainsNode: function(node) { - return window.rangy.getSelection().containsNode(node.getDOMNode(), true); - }, - - /** - * Runs a filter on each node in the selection, and report whether the - * supplied selector(s) were found in the supplied Nodes. - * - * By default, all specified nodes must match the selection, but this - * can be controlled with the requireall property. - * - * @method selectionFilterMatches - * @param {String} selector - * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time. - * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough. - * @return {Boolean} - */ - selectionFilterMatches: function(selector, selectednodes, requireall) { - if (typeof requireall === 'undefined') { - requireall = true; - } - if (!selectednodes) { - // Find this because it was not passed as a param. - selectednodes = this.getSelectedNodes(); - } - var allmatch = selectednodes.size() > 0, - anymatch = false; - - var editor = this.editor, - stopFn = function(node) { - // The function getSelectedNodes only returns nodes within the editor, so this test is safe. - return node === editor; - }; - - // If we do not find at least one match in the editor, no point trying to find them in the selection. - if (!editor.one(selector)) { - return false; - } - - selectednodes.each(function(node){ - // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing. - if (requireall) { - // Check for at least one failure. - if (!allmatch || !node.ancestor(selector, true, stopFn)) { - allmatch = false; - } - } else { - // Check for at least one match. - if (!anymatch && node.ancestor(selector, true, stopFn)) { - anymatch = true; - } - } - }, this); - if (requireall) { - return allmatch; - } else { - return anymatch; - } - }, - - /** - * Get the deepest possible list of nodes in the current selection. - * - * @method getSelectedNodes - * @return {NodeList} - */ - getSelectedNodes: function() { - var results = new Y.NodeList(), - nodes, - selection, - range, - node, - i; - - selection = window.rangy.getSelection(); - - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } else { - // Empty range. - range = window.rangy.createRange(); - } - - if (range.collapsed) { - // We do not want to select all the nodes in the editor if we managed to - // have a collapsed selection directly in the editor. - // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle - // so we must filter that out here too. - if (range.commonAncestorContainer !== this.editor.getDOMNode() - && range.commonAncestorContainer !== Y.config.doc) { - range = range.cloneRange(); - range.selectNode(range.commonAncestorContainer); - } - } - - nodes = range.getNodes(); - - for (i = 0; i < nodes.length; i++) { - node = Y.one(nodes[i]); - if (this.editor.contains(node)) { - results.push(node); - } - } - return results; - }, - - /** - * Check whether the current selection has changed since this method was last called. - * - * If the selection has changed, the ousupsub:selectionchanged event is also fired. - * - * @method _hasSelectionChanged - * @private - * @param {EventFacade} e - * @return {Boolean} - */ - _hasSelectionChanged: function(e) { - var selection = window.rangy.getSelection(), - range, - changed = false; - - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } else { - // Empty range. - range = window.rangy.createRange(); - } - - if (this._lastSelection) { - if (!this._lastSelection.equals(range)) { - changed = true; - return this._fireSelectionChanged(e); - } - } - this._lastSelection = range; - return changed; - }, - - /** - * Fires the ousupsub:selectionchanged event. - * - * When the selectionchanged event is fired, the following arguments are provided: - * - event : the original event that lead to this event being fired. - * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content. - * - * @method _fireSelectionChanged - * @private - * @param {EventFacade} e - */ - _fireSelectionChanged: function(e) { - this.fire('ousupsub:selectionchanged', { - event: e, - selectedNodes: this.getSelectedNodes() - }); - }, - - /** - * Get the DOM node representing the common anscestor of the selection nodes. - * - * @method getSelectionParentNode - * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made. - */ - getSelectionParentNode: function() { - var selection = window.rangy.getSelection(); - if (selection.rangeCount) { - return selection.getRangeAt(0).commonAncestorContainer; - } - return false; - }, - - /** - * Set the current selection. Used to restore a selection. - * - * @method selection - * @param {array} ranges A list of rangy.range objects in the selection. - */ - setSelection: function(ranges) { - var selection = window.rangy.getSelection(); - selection.setRanges(ranges); - }, - - /** - * Inserts the given HTML into the editable content at the currently focused point. - * - * @method insertContentAtFocusPoint - * @param {String} html - * @return {Node} The YUI Node object added to the DOM. - */ - insertContentAtFocusPoint: function(html) { - var selection = window.rangy.getSelection(), - range, - node = Y.Node.create(html); - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } - if (range) { - range.deleteContents(); - range.insertNode(node.getDOMNode()); - } - return node; - } - -}; - -Y.Base.mix(Y.M.editor_ousupsub.Editor, [EditorSelection]); -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * ousupsub editor plugin. - * - * @module moodle-editor_ousupsub-editor - * @submodule plugin-base - * @package editor_ousupsub - * @copyright 2014 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * A Plugin for the ousupsub Editor used in Moodle. - * - * This class should not be directly instantiated, and all Editor plugins - * should extend this class. - * - * @namespace M.editor_ousupsub - * @class EditorPlugin - * @main - * @constructor - * @uses M.editor_ousupsub.EditorPluginButtons - */ - -function EditorPlugin() { - EditorPlugin.superclass.constructor.apply(this, arguments); -} - -var DISABLED = 'disabled', - HIGHLIGHT = 'highlight', - GROUPSELECTOR = '.ousupsub_group.', - GROUP = '_group'; - -Y.extend(EditorPlugin, Y.Base, { - /** - * The name of the current plugin. - * - * @property name - * @type string - */ - name: null, - - /** - * The name of the command to execute when the button is clicked. - * - * @property exec - * @type string - */ - exec: null, - - /** - * A Node reference to the editor. - * - * @property editor - * @type Node - */ - editor: null, - - /** - * A Node reference to the editor toolbar. - * - * @property toolbar - * @type Node - */ - toolbar: null, - - /** - * Event Handles to clear on plugin destruction. - * - * @property _eventHandles - * @private - */ - _eventHandles: null, - - /** - * All of the buttons that belong to this plugin instance. - * - * Buttons are stored by button name. - * - * @property buttons - * @type object - */ - buttons: null, - - /** - * A list of each of the button names. - * - * @property buttonNames - * @type array - */ - buttonNames: null, - - /** - * A read-only view of the current state for each button. Mappings are stored by name. - * - * Possible states are: - *

    - *
  • {{#crossLink "M.editor_ousupsub.EditorPluginButtons/ENABLED:property"}}{{/crossLink}}; and
  • - *
  • {{#crossLink "M.editor_ousupsub.EditorPluginButtons/DISABLED:property"}}{{/crossLink}}.
  • - *
- * - * @property buttonStates - * @type object - */ - buttonStates: null, - - /** - * The state for a disabled button. - * - * @property DISABLED - * @type Number - * @static - * @value 0 - */ - DISABLED: 0, - - /** - * The state for an enabled button. - * - * @property ENABLED - * @type Number - * @static - * @value 1 - */ - ENABLED: 1, - - /** - * The list of Event Handlers for buttons. - * - * @property _buttonHandlers - * @protected - * @type array - */ - _buttonHandlers: null, - - /** - * A textual description of the primary keyboard shortcut for this - * plugin. - * - * This will be null if no keyboard shortcut has been registered. - * - * @property _primaryKeyboardShortcut - * @protected - * @type String - * @default null - */ - _primaryKeyboardShortcut: null, - - /** - * An list of handles returned by setTimeout(). - * - * The keys will be the buttonName of the button, and the value the handles. - * - * @property _highlightQueue - * @protected - * @type Object - * @default null - */ - _highlightQueue: null, - - initializer: function(config) { - // Set the references to configuration parameters. - this.name = config.name; - this.exec = config.exec; - this.toolbar = config.toolbar; - this.editor = config.editor; - - // Set up the prototypal properties. - // These must be set up here because prototypal arrays and objects are copied across instances. - this.buttons = {}; - this.buttonNames = []; - this.buttonStates = {}; - this._primaryKeyboardShortcut = []; - this._buttonHandlers = []; - this._menuHideHandlers = []; - this._highlightQueue = {}; - this._eventHandles = []; - this.addButton(config); - }, - - destructor: function() { - // Detach all EventHandles. - new Y.EventHandle(this._eventHandles).detach(); - }, - - /** - * Mark the content ediable content as having been changed. - * - * This is a convenience function and passes through to - * {{#crossLink "M.editor_ousupsub.EditorTextArea/updateOriginal"}}updateOriginal{{/crossLink}}. - * - * @method markUpdated - */ - markUpdated: function() { - // Save selection after changes to the DOM. If you don't do this here, - // subsequent calls to restoreSelection() will fail expecting the - // previous DOM state. - this.get('host').saveSelection(); - - return this.get('host').updateOriginal(); - }, - - /** - * Register an event handle for disposal in the destructor. - * - * @method registerEventHandle - * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate. - */ - registerEventHandle: function(handle) { - this._eventHandles.push(handle); - }, - - /** - * Add a button for this plugin to the toolbar. - * - * @method addButton - * @param {object} config The configuration for this button - * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. - * @param {string} [config.icon] The icon identifier. - * @param {string} [config.iconComponent='core'] The icon component. - * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. - * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts. - * If not specified, this is automatically generated based on config.keys. - * If multiple key bindings are supplied to config.keys, then only the first is used. - * If set to false, then no description is added to the title. - * @param {string} [config.tags] The tags that trigger this button to be highlighted. - * @param {boolean} [config.tagMatchRequiresAll=true] Working in combination with the tags parameter, when true - * every tag of the selection has to match. When false, only one match is needed. Only set this to false when - * necessary as it is much less efficient. - * See {{#crossLink "M.editor_ousupsub.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. - * @param {string} [config.title=this.name] The string identifier in the plugin's language file. - * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if - * specified, in the class for the button. - * @param {function} config.callback A callback function to call when the button is clicked. - * @param {object} [config.callbackArgs] Any arguments to pass to the callback. - * @return {Node} The Node representing the newly created button. - */ - addButton: function(config) { - var group = this.get('group'), - pluginname = this.name, - buttonClass = 'ousupsub_' + pluginname + '_button', - button, - host = this.get('host'); - - if (config.exec) { - buttonClass = buttonClass + '_' + config.exec; - } - - if (!config.buttonName) { - // Set a default button name - this is used as an identifier in the button object. - config.buttonName = config.exec || pluginname; - } else { - buttonClass = buttonClass + '_' + config.buttonName; - } - config.buttonClass = buttonClass; - - // Normalize icon configuration. - config = this._normalizeIcon(config); - - if (!config.title) { - config.title = 'pluginname'; - } - var title = M.util.get_string(pluginname, 'editor_ousupsub'); - - // Create the actual button. - var icon = ''; - if (config.iconurl) { - icon = ''; - } - button = Y.Node.create(''); - button.setAttribute('title', title); - - // Append it to the group. - group.append(button); - - var currentfocus = this.toolbar.getAttribute('aria-activedescendant'); - if (!currentfocus) { - // Initially set the first button in the toolbar to be the default on keyboard focus. - // Initially set the first button in the toolbar to be the default on keyboard focus. - button.setAttribute('tabindex', '0'); - this.toolbar.setAttribute('aria-activedescendant', button.generateID()); - this.get('host')._tabFocus = button; - } - // Normalize the callback parameters. - if (!config.callback) { - config.callback = this._applyTextCommand; - } - config.callback = Y.rbind(this._callbackWrapper, this, config.callback); - - // Add the standard click handler to the button. - this._buttonHandlers.push( - this.toolbar.delegate('click', config.callback, '.' + buttonClass, this) - ); - - // Handle button click via shortcut key. - if (config.keys) { - if (typeof config.keyDescription !== 'undefined') { - // A keyboard shortcut description was specified - use it. - this._primaryKeyboardShortcut[buttonClass] = config.keyDescription; - } - this._addKeyboardListener(config.callback, config.keys, buttonClass); - - if (this._primaryKeyboardShortcut[buttonClass]) { - // If we have a valid keyboard shortcut description, then set it with the title. - button.setAttribute('title', M.util.get_string('plugin_title_shortcut', 'editor_ousupsub', { - title: title, - shortcut: this._primaryKeyboardShortcut[buttonClass] - })); - } - } - - // Handle highlighting of the button. - if (config.tags) { - var tagMatchRequiresAll = true; - if (typeof config.tagMatchRequiresAll === 'boolean') { - tagMatchRequiresAll = config.tagMatchRequiresAll; - } - this._buttonHandlers.push( - host.on(['ousupsub:selectionchanged', 'change'], function(e) { - if (typeof this._highlightQueue[config.buttonName] !== 'undefined') { - clearTimeout(this._highlightQueue[config.buttonName]); - } - // Async the highlighting. - this._highlightQueue[config.buttonName] = setTimeout(Y.bind(function(e) { - if (host.selectionFilterMatches(config.tags, e.selectedNodes, tagMatchRequiresAll)) { - this.highlightButtons(config.buttonName); - } else { - this.unHighlightButtons(config.buttonName); - } - }, this, e), 0); - }, this) - ); - } - - // Add the button reference to the buttons array for later reference. - this.buttonNames.push(config.buttonName); - this.buttons[config.buttonName] = button; - this.buttonStates[config.buttonName] = this.ENABLED; - return button; - }, - - /** - * Normalize and sanitize the configuration variables relating to callbacks. - * - * @method _normalizeCallback - * @param {object} config - * @param {function} config.callback A callback function to call when the button is clicked. - * @param {object} [config.callbackArgs] Any arguments to pass to the callback. - * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from. - * @return {object} The normalized configuration - * @private - */ - _normalizeCallback: function(config, inheritFrom) { - if (config._callbackNormalized) { - // Return early if the callback has already been normalized. - return config; - } - - if (!inheritFrom) { - // Create an empty inheritFrom to make life easier below. - inheritFrom = {}; - } - - // We wrap the callback in function to prevent the default action, check whether the editor is - // active and focus it, and then mark the field as updated. - config._callback = config.callback || inheritFrom.callback; - config.callback = Y.rbind(this._callbackWrapper, this, this._applyTextCommand, config.callbackArgs); - - config._callbackNormalized = true; - - return config; - }, - - /** - * Normalize and sanitize the configuration variables relating to icons. - * - * @method _normalizeIcon - * @param {object} config - * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. - * @param {string} [config.icon] The icon identifier. - * @param {string} [config.iconComponent='core'] The icon component. - * @return {object} The normalized configuration - * @private - */ - _normalizeIcon: function(config) { - if (config.icon && !config.iconurl) { - // The default icon component. - if (!config.iconComponent) { - config.iconComponent = 'core'; - } - config.iconurl = M.util.image_url(config.icon, config.iconComponent); - } - - return config; - }, - - /** - * A wrapper in which to run the callbacks. - * - * This handles common functionality such as: - *
    - *
  • preventing the default action; and
  • - *
  • focusing the editor if relevant.
  • - *
- * - * @method _callbackWrapper - * @param {EventFacade} e - * @param {Function} callback The function to call which makes the relevant changes. - * @param {Array} [callbackArgs] The arguments passed to this callback. - * @return {Mixed} The value returned by the callback. - * @private - */ - _callbackWrapper: function(e, callback, callbackArgs) { - e.preventDefault(); - - if (!this.isEnabled()) { - // Exit early if the plugin is disabled. - return; - } - - var creatorButton = e.currentTarget.ancestor('button', true); - - if (creatorButton && creatorButton.hasAttribute(DISABLED)) { - // Exit early if the clicked button was disabled. - return; - } - - if (!(YUI.Env.UA.android || this.get('host').isActive())) { - // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion - // changes the cursor position. - // If we save that change, then when we restore the change later we get put in the wrong place. - // Android is fine to save the selection without the editor being in focus. - this.get('host').focus(); - } - - // Save the selection. - this.get('host').saveSelection(); - - // Build the arguments list, but remove the callback we're calling. - var args = [e, callbackArgs]; - - // Restore selection before making changes. - this.get('host').restoreSelection(); - - // Actually call the callback now. - return callback.apply(this, args); - }, - - /** - * Add a keyboard listener to call the callback. - * - * The keyConfig will take either an array of keyConfigurations, in - * which case _addKeyboardListener is called multiple times; an object - * containing an optional eventtype, optional container, and a set of - * keyCodes, or just a string containing the keyCodes. When keyConfig is - * not an object, it is wrapped around a function that ensures that - * only the expected key modifiers were used. For instance, it checks - * that space+ctrl is not triggered when the user presses ctrl+shift+space. - * When using an object, the developer should check that manually. - * - * @method _addKeyboardListener - * @param {function} callback - * @param {array|object|string} keyConfig - * @param {string} [keyConfig.eventtype=key] The type of event - * @param {string} [keyConfig.container=.editor_ousupsub_content] The containing element. - * @param {string} keyConfig.keyCodes The keycodes to user for the event. - * @private - * - */ - _addKeyboardListener: function(callback, keyConfig, buttonName) { - var eventtype = 'key', - container = CSS.EDITORWRAPPER, - keys, - handler, - modifier; - - if (Y.Lang.isArray(keyConfig)) { - // If an Array was specified, call the add function for each element. - Y.Array.each(keyConfig, function(config) { - this._addKeyboardListener(callback, config, buttonName); - }, this); - - return this; - - } else if (typeof keyConfig === "object") { - if (keyConfig.eventtype) { - eventtype = keyConfig.eventtype; - } - - if (keyConfig.container) { - container = keyConfig.container; - } - - // Must be specified. - keys = keyConfig.keyCodes; - handler = callback; - - } else { - modifier = ''; - keys = keyConfig; - if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') { - this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig); - } - // Wrap the callback into a handler to check if it uses the specified modifiers, not more. - handler = Y.bind(function(modifiers, e) { - callback.apply(this, [e]); - }, this, [modifier]); - } - - this._buttonHandlers.push( - this.editor.delegate( - eventtype, - handler, - keys, - container, - this - ) - ); - - }, - - /** - * Checks if a key event was strictly defined for the modifiers passed. - * - * @method _eventUsesExactKeyModifiers - * @param {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift). - * @param {EventFacade} e The event facade. - * @return {Boolean} True if the event was stricly using the modifiers specified. - */ - _eventUsesExactKeyModifiers: function(modifiers, e) { - var exactMatch = true, - hasKey; - - if (e.type !== 'key') { - return false; - } - - hasKey = Y.Array.indexOf(modifiers, 'alt') > -1; - exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1; - exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'meta') > -1; - exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'shift') > -1; - exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey)); - - return exactMatch; - }, - - /** - * Determine if this plugin is enabled, based upon the state of it's buttons. - * - * @method isEnabled - * @return {boolean} - */ - isEnabled: function() { - // The first instance of an undisabled button will make this return true. - var found = Y.Object.some(this.buttonStates, function(button) { - return (button === this.ENABLED); - }, this); - - return found; - }, - - /** - * Enable one button, or all buttons relating to this Plugin. - * - * If no button is specified, all buttons are disabled. - * - * @method disableButtons - * @param {String} [button] The name of a specific plugin to enable. - * @chainable - */ - disableButtons: function(button) { - return this._setButtonState(false, button); - }, - - /** - * Enable one button, or all buttons relating to this Plugin. - * - * If no button is specified, all buttons are enabled. - * - * @method enableButtons - * @param {String} [button] The name of a specific plugin to enable. - * @chainable - */ - enableButtons: function(button) { - return this._setButtonState(true, button); - }, - - /** - * Set the button state for one button, or all buttons associated with this plugin. - * - * @method _setButtonState - * @param {Boolean} enable Whether to enable this button. - * @param {String} [button] The name of a specific plugin to set state for. - * @chainable - * @private - */ - _setButtonState: function(enable, button) { - var attributeChange = 'setAttribute'; - if (enable) { - attributeChange = 'removeAttribute'; - } - if (button) { - if (this.buttons[button]) { - this.buttons[button][attributeChange](DISABLED, DISABLED); - this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; - } - } else { - Y.Array.each(this.buttonNames, function(button) { - this.buttons[button][attributeChange](DISABLED, DISABLED); - this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; - }, this); - } - - this.get('host').checkTabFocus(); - return this; - }, - - /** - * Highlight a button, or buttons in the toolbar. - * - * If no button is specified, all buttons are highlighted. - * - * @method highlightButtons - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @chainable - */ - highlightButtons: function(button) { - return this._changeButtonHighlight(true, button); - }, - - /** - * Un-highlight a button, or buttons in the toolbar. - * - * If no button is specified, all buttons are un-highlighted. - * - * @method unHighlightButtons - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @chainable - */ - unHighlightButtons: function(button) { - return this._changeButtonHighlight(false, button); - }, - - /** - * Highlight a button, or buttons in the toolbar. - * - * @method _changeButtonHighlight - * @param {boolean} highlight true - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @protected - * @chainable - */ - _changeButtonHighlight: function(highlight, button) { - var method = 'addClass'; - - if (!highlight) { - method = 'removeClass'; - } - if (button) { - if (this.buttons[button]) { - this.buttons[button][method](HIGHLIGHT); - } - } else { - Y.Object.each(this.buttons, function(button) { - button[method](HIGHLIGHT); - }, this); - } - - return this; - }, - - /** - * Get the default meta key to use with keyboard events. - * - * On a Mac, this will be the 'meta' key for Command; otherwise it will - * be the Control key. - * - * @method _getDefaultMetaKey - * @return {string} - * @private - */ - _getDefaultMetaKey: function() { - if (Y.UA.os === 'macintosh') { - return 'meta'; - } else { - return 'ctrl'; - } - }, - - /** - * Get the user-visible description of the meta key to use with keyboard events. - * - * On a Mac, this will be 'Command' ; otherwise it will be 'Control'. - * - * @method _getDefaultMetaKeyDescription - * @return {string} - * @private - */ - _getDefaultMetaKeyDescription: function(keyCode) { - if (Y.UA.os === 'macintosh') { - return M.util.get_string('editor_command_keycode', 'editor_ousupsub', String.fromCharCode(keyCode).toLowerCase()); - } else { - return M.util.get_string('editor_control_keycode', 'editor_ousupsub', String.fromCharCode(keyCode).toLowerCase()); - } - }, - - /** - * Get the standard key event to use for keyboard events. - * - * @method _getKeyEvent - * @return {string} - * @private - */ - _getKeyEvent: function() { - return 'down:'; - }, - - /** - * Apply the given document.execCommand and tidy up the editor dom afterwards. - * - * @method _applyTextCommand - * @private - * @return void - */ - _applyTextCommand: function(e) { - var mode = 0; - - if(e && e.type === 'key') { - // handled by this._getEditor().textareaKeyboardNavigation(e); - return; - } - - this._getEditor()._applyTextCommand(this.exec, mode); - }, - - /** - * Get the editor object. - * - * @method _getEditor - * @private - * @return node. - */ - _getEditor: function(host) { - if (!host) { - host = this.get('host'); - } - - return host; - }, - - /** - * Get the node containing the editor html to be updated. - * - * @method _getEditorNode - * @private - * @return node. - */ - _getEditorNode: function(host) { - return this._getEditor(host).editor._node; - } - -}, { - NAME: 'editorPlugin', - ATTRS: { - /** - * The editor instance that this plugin was instantiated by. - * - * @attribute host - * @type M.editor_ousupsub.Editor - * @writeOnce - */ - host: { - writeOnce: true - }, - - /** - * The toolbar group that this button belongs to. - * - * When setting, the name of the group should be specified. - * - * When retrieving, the Node for the toolbar group is returned. If - * the group doesn't exist yet, then it is created first. - * - * @attribute group - * @type Node - * @writeOnce - */ - group: { - writeOnce: true, - getter: function(groupName) { - var group = this.toolbar.one(GROUPSELECTOR + groupName + GROUP); - if (!group) { - group = Y.Node.create('
'); - this.toolbar.append(group); - } - - return group; - } - } - } -}); - -Y.namespace('M.editor_ousupsub').EditorPlugin = EditorPlugin; -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * The manager for the OUSupSub Editor. - * - * @module moodle-editor_ousupsub-editor - * @submodule manager - * @package editor_ousupsub - * @copyright 2014 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @main moodle-editor_ousupsub-editor - */ - -/** - * @module moodle-editor_ousupsub-editor - */ - -/** - * The manager for the OUSupSub editor. - * - * @class editor_ousupsub - */ - -Y.M.editor_ousupsub = Y.M.editor_ousupsub || {}; - -/** - * List of editor_ousupsub instances. Intentionally placed on window.M, not - * something in the namespace, so we can be sure it is really global. - */ -M = M || {}; -M.editor_ousupsub = M.editor_ousupsub || {}; -M.editor_ousupsub._instances = M.editor_ousupsub._instances || {}; - -/** - * Add a reference to an editor. - * Note: This is an internal method which should only be called by the editor itself. - * - * @method addEditorReference - * @param {String} name The name of the editor instance to add - * @private - */ -Y.M.editor_ousupsub.addEditorReference = function(name, reference) { - Y.log("Registering a new ousupsub editor: " + name, 'debug', LOGNAME); - if (typeof M.editor_ousupsub._instances[name] === 'undefined') { - M.editor_ousupsub._instances[name] = reference; - } else { - Y.log("An ousupsub editor with the name '" + name + "' already exists. Unable to add.", 'warn', LOGNAME); - } - - return Y.M.editor_ousupsub; -}; - -/** - * Create a new editor using simple options. - * - * @method createEditor - * @param {String} id of the textarea to turn into an editor. - * @param {String} type 'superscript', 'subscript' or 'both'. - * @return {M.editor_ousupsub.Editor} The newly created editor instance - */ -Y.M.editor_ousupsub.createEditorSimple = function(id, type) { - var plugins = []; - if (type === 'both' || type === 'superscript') { - plugins.push({"name": "superscript", "params": []}); - } - if (type === 'both' || type === 'subscript') { - plugins.push({"name": "subscript", "params": []}); - } - - Y.M.editor_ousupsub.createEditor( - {"elementid" : id, "content_css" : "", "contextid" : 0, "language" : "en", - "directionality" : "ltr", "plugins" : [{"group" : "style1", "plugins" : plugins}],"pageHash" : ""}); -}; - -/** - * Create a new editor using the specified configuration. - * - * @method createEditor - * @param {Object} config See the attributes for {{#crossLink - * "M.editor_ousupsub.Editor"}}{{/crossLink}} for configuration options. The - * elementid provided will be used as the name of this editor within - * the editor Manager. - * @return {M.editor_ousupsub.Editor} The newly created editor instance - */ -Y.M.editor_ousupsub.createEditor = function(config) { - - var instance = new Y.M.editor_ousupsub.Editor(config); - Y.M.editor_ousupsub.fire('editor_ousupsub:created', { - id: instance.get('elementid'), - instance: instance - }); - return instance; -}; - -/** - * Get the requested Editor instance. - * - * @method getEditor - * @param {String} name The name of the editor instance to retrieve - * @return {M.editor_ousupsub.Editor} The requested editor instance - */ -Y.M.editor_ousupsub.getEditor = function(name) { - return M.editor_ousupsub._instances[name]; -}; - -/** - * Remove the reference for an editor. - * - * @method removeEditorReference - * @param {String} name The name of the editor instance to remove - */ -Y.M.editor_ousupsub.removeEditor = function(name) { - var instance = Y.M.editor_ousupsub.getEditor(name); - if (instance) { - instance.destroy(); - this.fire('editor_ousupsub:removed', { - id: name - }); - } - return Y.M.editor_ousupsub; -}; - -/** - * Remove the reference for an editor. - * Note: This is an internal method which should only be called by the editor itself. - * - * @method removeEditorReference - * @param {String} name The name of the editor instance to remove - * @private - */ -Y.M.editor_ousupsub.removeEditorReference = function(name) { - if (Y.M.editor_ousupsub.getEditor(name)) { - delete M.editor_ousupsub._instances[name]; - } -}; - -/** - * Add the supplied function to the manager using the specified name. - * - * @method addMethod - * @param {String} name The name to store the method on within the editor manager. - * @param {Function} fn The function to be added. - * @param {Object} [context] The context to apply the function with. If not specified, the Editor itself is used. - */ -Y.M.editor_ousupsub.addMethod = function(name, fn) { - if (typeof this[name] !== 'undefined') { - Y.log('Overwriting existing method: ' + name, 'warn', LOGNAME); - } - - Y.M.editor_ousupsub[name] = function() { - var ret = [], - args = arguments; - - Y.Object.each(M.editor_ousupsub._instances, function(editor) { - var result = fn.apply(editor, args); - - if (result !== undefined && result !== editor) { - ret[ret.length] = result; - } - }); - - // If we received a set of results, return them, otherwise make this method chainable. - return ret.length ? ret : this; - }; -}; - -Y.augment(Y.M.editor_ousupsub, Y.EventTarget); - -Y.Array.each(['saveSelection', 'updateFromTextArea', 'updateOriginal', 'cleanEditorHTML', 'destroy'], function(name) { - Y.M.editor_ousupsub.addMethod(name, Y.M.editor_ousupsub.Editor.prototype[name]); -}); - - -}, '@VERSION@', {"requires": ["base", "node", "event", "event-custom", "moodle-editor_ousupsub-rangy"]}); diff --git a/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-min.js b/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-min.js deleted file mode 100644 index 2671a85..0000000 --- a/yui/build/moodle-editor_ousupsub-editor/moodle-editor_ousupsub-editor-min.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("moodle-editor_ousupsub-editor",function(a,d){var n,s,i,e="editor_ousupsub_content",c="editor_ousupsub_content_wrap",h=".editor_ousupsub_content",t="editor_ousupsub_toolbar",p="editor_ousupsub";function o(){o.superclass.constructor.apply(this,arguments)}function r(){}function u(){}function l(){l.superclass.constructor.apply(this,arguments)}a.extend(o,a.Base,{BLOCK_TAGS:["address","article","aside","audio","blockquote","canvas","dd","div","dl","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","noscript","ol","output","p","pre","section","table","tfoot","ul","video"],PLACEHOLDER_CLASS:"ousupsub-tmp-class",ALL_NODES_SELECTOR:"[style],font[face]",FONT_FAMILY:"fontFamily",_wrapper:null,editor:null,toolbar:null,textarea:null,textareaLabel:null,plugins:null,_eventHandles:null,_tabFocus:null,_maxUndos:40,_undoStack:null,_redoStack:null,initializer:function(){this.textarea=a.one(document.getElementById(this.get("elementid"))),this.textarea&&(a.M.editor_ousupsub.addEditorReference(this.get("elementid"),this),this._eventHandles=[],this._wrapper=a.Node.create('
'),this.editor=a.Node.create('
'),this.textareaLabel=a.one('[for="'+this.get("elementid")+'"]'),this.textareaLabel&&(this.textareaLabel.generateID(),this.editor.setAttribute("aria-labelledby",this.textareaLabel.get("id"))),this.setupToolbar(),this.setupTemplateEditor(),this.disableCssStyling(),document.queryCommandSupported("DefaultParagraphSeparator")&&document.execCommand("DefaultParagraphSeparator",!1,"p"),this.textarea.get("parentNode").insert(this._wrapper,this.textarea),this.textarea.hide(),this.updateFromTextArea(),this.setupTextareaNavigation(),this._preventEnter(),this.publishEvents(),this.setupSelectionWatchers(),this.setupAutomaticPolling(),this.setupPlugins())},destructor:function(){a.Array.each(this.plugins,function(e,t){e.destroy(),this.plugins[t]=undefined},this),new a.EventHandle(this._eventHandles).detach(),this.textarea.show(),this._wrapper.remove(!0),a.M.editor_ousupsub.removeEditorReference(this.get("elementid"),this)},focus:function(){return this.editor.focus(),this},publishEvents:function(){return this.publish("change",{broadcast:!0,preventable:!0}),this.publish("pluginsloaded",{fireOnce:!0}),this.publish("ousupsub:selectionchanged",{prefix:"ousupsub"}),this},setupAutomaticPolling:function(){return this._registerEventHandle(this.editor.on(["keyup","cut"],this.updateOriginal,this)),this._registerEventHandle(this.editor.on(["keypress","delete"],this.cleanEditorHTMLSimple,this)),this._registerEventHandle(this.editor.on("paste",this.pasteCleanup,this)),this._registerEventHandle(this.editor.on("drop",this.updateOriginalDelayed,this)),this},updateOriginalDelayed:function(){return setTimeout(a.bind(this.updateOriginal,this),0),this},setupPlugins:function(){var e,t,i,s,n;for(t in this.plugins={},e=this.get("plugins"))if((i=e[t]).plugins)for(s in i.plugins)"superscript"===(n=i.plugins[s]).name?this.plugins.superscript=new a.M.editor_ousupsub.EditorPlugin({name:"superscript",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,exec:"superscript",tags:"sup",keys:["94"],icon:"e/superscript",keyDescription:"Shift + ^ or Up arrow"}):"subscript"===n.name&&(this.plugins.subscript=new a.M.editor_ousupsub.EditorPlugin({name:"subscript",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,exec:"subscript",tags:"sub",keys:["95"],icon:"e/subscript",keyDescription:"Shift + _ or Down arrow"}));return this._undoStack=[],this._redoStack=[],this.plugins.undo=new a.M.editor_ousupsub.EditorPlugin({name:"undo",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,keys:["90"],callback:this._undoHandler}),this.plugins.redo=new a.M.editor_ousupsub.EditorPlugin({name:"redo",group:i.group,editor:this.editor,toolbar:this.toolbar,host:this,keys:["89"],callback:this._redoHandler}),this.on("pluginsloaded",function(){this._addToUndo(this._getHTML()),this.on("ousupsub:selectionchanged",this._changeListener,this)},this),this._updateButtonsStates(),this.setupUndoHandlers(),this.fire("pluginsloaded"),this},setupUndoHandlers:function(){return this._registerEventHandle(this._wrapper.delegate("key",this._undoHandler,"down:90+ctrl","."+e,this)),this._registerEventHandle(this._wrapper.delegate("key",this._redoHandler,"down:89+ctrl","."+e,this)),this},pluginEnabled:function(e){return!!this.plugins[e]},enablePlugins:function(e){this._setPluginState(!0,e)},disablePlugins:function(e){this._setPluginState(!1,e)},_setPluginState:function(e,t){var i=e?"enableButtons":"disableButtons";t?this.plugins[t][i]():a.Object.each(this.plugins,function(e){e[i]()},this)},_registerEventHandle:function(e){this._eventHandles.push(e)},setupToolbar:function(){return this.toolbar=a.Node.create(''),this._wrapper.appendChild(this.toolbar),this.textareaLabel&&this.toolbar.setAttribute("aria-labelledby",this.textareaLabel.get("id")),this.setupToolbarNavigation(),this},setupToolbarNavigation:function(){return this._wrapper.delegate("key",this.toolbarKeyboardNavigation,"down:37,39","."+t,this),this._wrapper.delegate("focus",function(e){this._setTabFocus(e.currentTarget)},"."+t+" button",this),this},toolbarKeyboardNavigation:function(e){e.preventDefault();var t=this.toolbar.all("button"),i=1,s=e.target.ancestor("button",!0);37===e.keyCode&&(i=-1),(e=this._findFirstFocusable(t,s,i))&&(e.focus(),this._setTabFocus(e))},_findFirstFocusable:function(e,t,i){var s,n,o=0,r=e.indexOf(t);for(r<-1&&(r=0);o=e.size()&&(r=0),o++,!(s=e.item(r)).hasAttribute("hidden")&&!s.hasAttribute("disabled")&&!s.ancestor(".ousupsub_group").hasAttribute("hidden")){n=s;break}return n},checkTabFocus:function(){var e;return this._tabFocus&&(!( -this._tabFocus.hasAttribute("disabled")||this._tabFocus.hasAttribute("hidden")||this._tabFocus.ancestor(".ousupsub_group").hasAttribute("hidden"))||(e=this._findFirstFocusable(this.toolbar.all("button"),this._tabFocus,-1))&&(this._tabFocus.compareTo(document.activeElement)&&e.focus(),this._setTabFocus(e))),this},_setTabFocus:function(e){return this._tabFocus&&this._tabFocus.setAttribute("tabindex","-1"),this._tabFocus=e,this._tabFocus.setAttribute("tabindex",0),this.toolbar.setAttribute("aria-activedescendant",this._tabFocus.generateID()),this},disableCssStyling:function(){try{document.execCommand("styleWithCSS",0,!1)}catch(e){try{document.execCommand("useCSS",0,!0)}catch(t){try{document.execCommand("styleWithCSS",!1,!1)}catch(i){}}}},setupTemplateEditor:function(){var t,e,i,s,n=a.Node.create('
');n.appendChild(this.editor),this._wrapper.appendChild(n),i=6*this.textarea.getAttribute("cols")+41+"px",this.editor.setStyle("width",i),this.editor.setStyle("minWidth",i),this.editor.setStyle("maxWidth",i),i=this.textarea.getAttribute("rows"),i=(t=6*i+13)-6+"px",this.editor.setStyle("height",e=t-10+"px"),this.editor.setStyle("minHeight",e),this.editor.setStyle("maxHeight",e),this.editor.setStyle("line-height",i),n.setStyle("minHeight",i=1+t+"px"),this.textareaLabel.setStyle("display","inline-block"),this.textareaLabel.setStyle("margin",0),this.textareaLabel.setStyle("height",i),this.textareaLabel.setStyle("minHeight",i),this.textareaLabel.setStyle("maxheight",i),this.textareaLabel.hasClass("accesshide")?(this.textareaLabel.removeClass("accesshide"),this.textareaLabel.setStyle("visibility","hidden"),this._wrapper.setStyle("margin-left",-parseInt(this.textareaLabel.get("offsetWidth")))):(this.textareaLabel.getDOMNode().parentNode.style.paddingBottom=e,this.textareaLabel.setStyle("vertical-align","bottom")),n="#"+(s=this).get("elementid").replace(/:/g,"\\:")+"editable",a.on("contentready",function(){s.textareaLabel.setStyle("line-height",s.editor.getComputedStyle("line-height"));var e=1+t+parseInt(s.toolbar.get("offsetHeight"));s._wrapper.setStyle("height",e),s._wrapper.setStyle("minHeight",e),s._wrapper.setStyle("maxHeight",e),a.UA.ie&&"hidden"===s.textareaLabel.getComputedStyle("visibility")&&s._wrapper.setStyle("vertical-align",parseInt(s.toolbar.get("offsetHeight"))-1+"px")},n)},_getEmptyContent:function(){return""},updateFromTextArea:function(){this.editor.setHTML(""),this.editor.append(this.textarea.get("value")),this.cleanEditorHTML(),""===this.editor.getHTML()&&this.editor.setHTML(this._getEmptyContent())},updateOriginal:function(){var e=this.textarea.get("value"),t=this.getCleanHTML();return""===t&&this.isActive()&&(t=this._getEmptyContent()),e!==(t=(t=this._removeUnicodeCharacters(t)).trim())&&(this.textarea.set("value",t),this.fire("change")),this},setupTextareaNavigation:function(){return this._registerEventHandle(this._wrapper.delegate("key",this.textareaKeyboardNavigation,"down:38,40","."+e,this)),this._registerEventHandle(this._wrapper.delegate("key",this.textareaKeyboardNavigation,"press:94, 95","."+e,this)),this},textareaKeyboardNavigation:function(e){var t;e.preventDefault(),YUI.Env.UA.android||this.isActive()||this.focus(),t="",38===(e=(e=window.event||e).keyCode||e.charCode)||94===e?t="superscript":40!==e&&95!==e||(t="subscript"),t&&this._applyTextCommand(t,1)},_preventEnter:function(){var e="keypress";(a.UA.webkit||a.UA.ie)&&(e="keydown"),this.editor.on(e,function(e){e=window.event||e;13===e.keyCode&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},this)},_addToRedo:function(e){this._redoStack.push(e)},_addToUndo:function(e,t){for(void 0===t&&(t=!1),this._undoStack[this._undoStack.length-1]!==e&&(this._undoStack.push(e),t&&(this._redoStack=[]));this._undoStack.length>this._maxUndos;)this._undoStack.shift()},_getHTML:function(){return this.getCleanHTML()},_getRedo:function(){return this._redoStack.pop()},_getUndo:function(e){if(1===this._undoStack.length)return this._undoStack[0];var t=this._undoStack.pop();return t===e&&(t=this._undoStack.pop()),0===this._undoStack.length&&this._addToUndo(t),t},_restoreValue:function(e){this.editor.setHTML(e),this._addToUndo(e)},_updateButtonsStates:function(){1"===t?"":(0===t.indexOf("")&&(e=t.length-("".length+"".length),t=t.substr("".length,e)),this._cleanHTML(t))},cleanEditorHTML:function(){return this.editor.set("innerHTML",this._cleanHTML(this.editor.get("innerHTML"))),this},cleanEditorHTMLSimple:function(){var e=window.rangy.saveSelection();return this.editor.set("innerHTML",this._cleanHTMLSimple(this.editor.get("innerHTML"))),window.rangy.restoreSelection(e,!0),this},_cleanHTMLSimple:function(e){return this._filterContentWithRules(e,[{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>(.+)<\/span>/gi,replace:"$1"}])},_cleanHTML:function(e){return this._filterContentWithRules(e,[{ -regex:/]*>( |\s)*<\/p>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/ /gi,replace:" "},{regex:/<\/sup>(\s*)+/gi,replace:"$1"},{regex:/<\/sub>(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+<\/sup>/gi,replace:"$1"},{regex:/(\s*)+<\/sub>/gi,replace:"$1"},{regex:/
/gi,replace:""},{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/]*>[\s\S]*?<\/script>/gi,replace:""},{regex:/<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi,replace:""},{regex:/<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi,replace:""},{regex:/<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi,replace:""},{regex:/<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi,replace:""},{regex:/<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi,replace:""},{regex:/<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi,replace:""},{regex:/<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi,replace:""},{regex:/<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi,replace:""},{regex:/<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi,replace:""},{regex:/<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi,replace:""},{regex:/<\/?(?:var|wbr|video)[^>]*?>/gi,replace:""},{regex:/<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi,replace:""},{regex:/<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:jsl|nobr)[^>]*?>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*>( |\s)*<\/sup>/gi,replace:""},{regex:/]*>( |\s)*<\/sub>/gi,replace:""},{regex:/(.*?)<\/xmlns.*?>/gi,replace:"$1"}])},cleanEditorHTMLEmptySupAndSubTags:function(){var e=window.rangy.saveSelection(),t=this.editor.get("innerHTML"),t=this._cleanEditorHTMLEmptySupAndSubTags(t),t=this._removeUnicodeCharacters(t);return this.editor.set("innerHTML",t),window.rangy.restoreSelection(e,!0),this},_cleanEditorHTMLEmptySupAndSubTags:function(e){return this._filterContentWithRules(e,[{regex:/]*>(|\s)*<\/su[bp]>/gi,replace:""}])},_filterContentWithRules:function(e,t){for(var i=0,i=0;i([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}]),0!==(e=this._cleanHTML(e)).length&&e.match(/\S/)?((t=document.createElement("div")).innerHTML=e,e=t.innerHTML,t.innerHTML="",e=this._filterContentWithRules(e,[{regex:/(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9\-]*)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9\-]*)+/gi,replace:"$1"},{regex:/
]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}]),this._cleanHTML(e)):e):""},_applyTextCommand:function(e,t){var i;if(t){if("superscript"===(i=this.getCursorTag())&&e===i||"subscript"===i&&e===i)return;if("superscript"===i&&"subscript"===e?e="superscript":"subscript"===i&&"superscript"===e&&(e="subscript"),!this.pluginEnabled(e))return}document.execCommand(e,!1,null),(t=window.rangy.getSelection()).isCollapsed&&(this.cleanEditorHTMLEmptySupAndSubTags(),e=this.insertContentAtFocusPoint("<"+(i="superscript"===e?"sup":"sub")+">\ufeff"),(i=window.rangy.createRange()).selectNode(e._node.childNodes[0]),this.setSelection([i]),t.rangeCount&&t.collapseToEnd()),this._normaliseTextarea(),this.cleanEditorHTMLSimple(),this.saveSelection(),this.updateOriginal()},getCursorTag:function(){var e="text",t=window.rangy.getSelection(),i=t.focusNode.nodeName.toLowerCase(),s=t.focusNode.parentNode.nodeName.toLowerCase(),n=""; -return t.focusNode.childNodes&&t.focusNode.childNodes[t.focusOffset-1]&&(n=t.focusNode.childNodes[t.focusOffset-1].nodeName.toLowerCase()),"sup"===i||"sup"===s||"sup"===n?e="superscript":"sub"!==i&&"sub"!==s&&"sub"!==n||(e="subscript"),e},_normaliseTextarea:function(){var e,t,i=window.rangy.saveSelection(),s=this._getEditorNode();for(this._removeSingleNodesByName(s,"br"),e=["p","b","i","u","ul","ol","li"],t=0;t]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""} - ]; - - // Apply the rules. - content = this._filterContentWithRules(content, rules); - - // Reapply the standard cleaner to the content. - content = this._cleanHTML(content); - - return content; - }, - - /** - * Apply the given document.execCommand and tidy up the editor dom afterwards. - * - * @method _applyTextCommand - * @private - * @param int mode (optional) default is button (0), keyboard is 1 - * @return void - */ - _applyTextCommand: function(command, mode) { - var selection, tag; - // Handle keyboard mode. - if (mode) { - tag = this.getCursorTag(); - if (tag === 'superscript' && command === tag || - tag === 'subscript' && command === tag) { - return; // Do nothing. - } else if (tag === 'superscript' && command === 'subscript') { - command = 'superscript'; - } else if (tag === 'subscript' && command === 'superscript') { - command = 'subscript'; - } - - if (!this.pluginEnabled(command)) { - return; - } - } - - // Apply command. - document.execCommand(command, false, null); - - // If nothing is selected add a relevant tag. - selection = window.rangy.getSelection(); - // If it's a collapsed selection the cursor is in the editor but no selection has been made. - if (selection.isCollapsed) { - - // Remove empty sup and sub tags. - this.cleanEditorHTMLEmptySupAndSubTags(); - // Insert tag at cursor focus point. - tag = command === 'superscript' ? 'sup' : 'sub'; - //  is is the Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF). Used - // by TinyMCE to add empty sup/sub tags when nothing is selected. This causes lint - // errors but I couldn't find a better solution. - // http://stackoverflow.com/questions/9691771/why-is-65279-appearing-in-my-html. - var node = this.insertContentAtFocusPoint('<' + tag + '>'); - var range = window.rangy.createRange(); - range.selectNode(node._node.childNodes[0]); - this.setSelection([range]); - // Restore the selection (cursor position). - if (selection.rangeCount) { - selection.collapseToEnd(); - } - } - this._normaliseTextarea(); - this.cleanEditorHTMLSimple(); - - // And mark the text area as updated. - // Save selection after changes to the DOM. If you don't do this here, - // subsequent calls to restoreSelection() will fail expecting the - // previous DOM state. - this.saveSelection(); - this.updateOriginal(); - }, - - /** - * What type of tag surrounds the cursor. - * - * @method _getCursorTag - * @private - * @return string - */ - getCursorTag: function() { - var tag = 'text'; - var selection = window.rangy.getSelection(); - var nodeName = selection.focusNode.nodeName.toLowerCase(); - var parentNodeName = selection.focusNode.parentNode.nodeName.toLowerCase(); - - var childNodeName = ''; - if (selection.focusNode.childNodes && selection.focusNode.childNodes[selection.focusOffset-1]) { - childNodeName = selection.focusNode.childNodes[selection.focusOffset-1].nodeName.toLowerCase(); - } - if (nodeName === 'sup' || parentNodeName === 'sup' || childNodeName === 'sup') { - tag = 'superscript'; - } else if (nodeName === 'sub' || parentNodeName === 'sub' || childNodeName === 'sub') { - tag = 'subscript'; - } - return tag; - }, - - /** - * Get a normalised array of the currently selected nodes. Chrome splits text nodes - * at the end of each selection and also creates empty text nodes. Fix these changes - * and provide a standard array of nodes to match the existing selection to. - * - * @method _normaliseTextarea - * @private - * @return string - */ - _normaliseTextarea: function() { - // Save the current selection (cursor position). - var selection = window.rangy.saveSelection(); - // Remove all the span tags added to the editor textarea by the browser. - // Get the html directly inside the editor

tag and remove span tags from the html inside it. - - var editor_node = this._getEditorNode(); - this._removeSingleNodesByName(editor_node, 'br'); - - // Remove specific tags that can be added through keyboard shortcuts. - var tagsToRemove = ['p', 'b', 'i', 'u', 'ul', 'ol', 'li']; - for (var i = 0; i < tagsToRemove.length; i++) { - this._removeNodesByName(editor_node, tagsToRemove[i]); - } - this._normaliseTagInTextarea('sup'); - this._normaliseTagInTextarea('sub'); - this._removeNodesByName(editor_node, 'span'); - - // Restore the selection (cursor position). - window.rangy.restoreSelection(selection, true); - - // Normalise the editor html. - editor_node.normalize(); - }, - - /** - * Remove all tags nested inside other tags of the same name. No nesting of - * similar tags e.g. is not allowed. - * - * @method _normaliseTagInTextarea - * @private - * @param string name Name of tag to normalise. - * @return string. - */ - _normaliseTagInTextarea: function(name) { - var nodes = [], container = this._getEditorNode(), parentNode, removeParent = false, node; - - // Remove nested nodes. - /* - * Where the node.firstChild == nodes[i+1] since it ignores text elements - * I know it's the first node. Since the two elements match they should cancel - * each other out. Currently we remove only the child sup. We should remove - * both and move their children out. - */ - // Nodelists change as nodes are added and removed. Use an array of nodes instead. - nodes = this._copyArray(container.querySelectorAll(name), nodes); - - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - parentNode = node.parentNode; - removeParent = false; - if (parentNode === container ) { - continue; - } - if (parentNode.firstChild === node && parentNode.lastChild === node && - parentNode.nodeName.toLowerCase() === name) { - removeParent = true; - } - if (!removeParent && node && parentNode.nodeName.toLowerCase() === name) { - removeParent = true; - this._splitParentNode(parentNode, name); - } - this._removeNodesByName(node, name); - if (removeParent) { - this._removeNodesByName(parentNode, name); - } - } - - // Combine Sibling nodes. - // Get a new node array and fill with the a fresh nodelist. - nodes = []; - nodes = this._copyArray(container.querySelectorAll(name), nodes); - - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (!node.previousSibling || node.previousSibling.nodeName.toLowerCase() !== name) { - continue; - } - this._mergeNodes(node, node.previousSibling); - } - }, - - /** - * Merge the from and to nodes by moving all elements in from node to the to node. - * Append nodes in order to the to node. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _mergeNodes - * @private - * @return void. - */ - _mergeNodes: function(from, to) { - var nodes = []; - var merge_nodes = from.childNodes; - - // Node lists reduce in size as nodes are removed. Use an array of nodes instead. - for (var i = 0; i < merge_nodes.length; i++) { - nodes.push(merge_nodes.item(i)); - } - - for (i = 0; i < nodes.length; i++) { - to.appendChild(nodes[i]); - } - this._removeNode(from); - }, - - /** - * Split the parent node into two with the node with the given name in the middle. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _splitParentNode - * @private - * @return void. - */ - _splitParentNode: function(container_node, name) { - var nodes = [], node, nodesToAppend = []; - nodes = this._copyArray(container_node.childNodes, nodes); - - var i,j; - for (i = 0; i < nodes.length; i++) { - node = nodes[i]; - nodesToAppend = []; - if (node.nodeName.toLowerCase() === name) { - nodesToAppend = this._copyArray(node.childNodes, nodesToAppend); - } else { - nodesToAppend[0] = document.createElement(name); - nodesToAppend[0].appendChild(node); - } - for (j = 0; j < nodesToAppend.length; j++) { - container_node.parentNode.insertBefore(nodesToAppend[j], container_node); - } - } - }, - - /** - * Copy array values from a dom node list to the given array. - * - * A dom node list reduces as children are removed. Copying to a standard array provides - * an array that doesn't change. - * @method _copyArray - * @private - * @return array. - */ - _copyArray: function(from, to) { - for (var i = 0; i < from.length; i++) { - to.push(from[i]); - } - - return to; - }, - - /** - * Move all elements in container node before the reference node. - * If recursive mode is equired then where childnodes exist that are not - * text nodes. Move their children and remove the existing node. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _removeNodesByName - * @private - * @return void. - */ - _removeNodesByName: function(container_node, name) { - var node, remove_node = container_node.nodeName.toLowerCase() === name; - var nodes = []; - var container_nodes = container_node.childNodes; - - // Don't remove the span used by rangy to save and restore the user selection. - if (container_node.nodeName.toLowerCase() === 'span' && - container_node.id.indexOf('selectionBoundary_') > -1) { - remove_node = false; - } - - nodes = this._copyArray(container_nodes, nodes); - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (node.childNodes && node.childNodes.length) { - this._removeNodesByName(node, name); - - } - if (remove_node) { - var parentNode = container_node.parentNode; - parentNode.insertBefore(node, container_node); - } - - } - if (remove_node) { - this._removeNode(container_node); - } - }, - - /** - * Recursively remove any tag with the given name. Removes child nodes too. - * - * Can't use other dom methods like querySelectorAll because they don't return text elements. - * @method _removeSingleNodesByName - * @private - * @return void. - */ - _removeSingleNodesByName: function(container_node, name) { - if (!container_node.childNodes) { - return; - } - var node; - var nodes = []; - nodes = this._copyArray(container_node.childNodes, nodes); - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (node.childNodes && node.childNodes.length) { - this._removeSingleNodesByName(node, name); - } - - if (node.nodeName.toLowerCase() === name) { - this._removeNode(node); - } - } - }, - - /** - * Remove a dom node in a cross browser way. - * - * @method _removeNode - * @private - * @return bool. - */ - _removeNode: function(node) { - if(!node.remove) { - return node.parentNode.removeChild(node); - } - return node.remove(); - }, - - /** - * Get the editor object. - * - * @method _getEditor - * @private - * @return node. - */ - _getEditor: function(host) { - if (!host) { - host = this.get('host'); - } - - return this; - }, - - /** - * Get the node containing the editor html to be updated. - * - * @method _getEditorNode - * @private - * @return node. - */ - _getEditorNode: function(host) { - return this._getEditor(host).editor._node; - }, - - /** - * Remove specific unicode characters from the given string. - * - * @method _removeUnicodeCharacters - * @private - * @return string. - */ - _removeUnicodeCharacters: function(text) { - var values = []; - for ( var i = 0; i < text.length; i++ ) { - if (text.charCodeAt(i) == "65279") { - continue; - } - values.push(text.charAt(i)); - } - return values.join(''); - } -}; - -Y.Base.mix(Y.M.editor_ousupsub.Editor, [EditorClean]); -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * @module moodle-editor_ousupsub-editor - * @submodule selection - */ - -/** - * Selection functions for the ousupsub editor. - * - * See {{#crossLink "M.editor_ousupsub.Editor"}}{{/crossLink}} for details. - * - * @namespace M.editor_ousupsub - * @class EditorSelection - */ - -function EditorSelection() {} - -EditorSelection.ATTRS = { -}; - -EditorSelection.prototype = { - - /** - * List of saved selections per editor instance. - * - * @property _selections - * @private - */ - _selections: null, - - /** - * A unique identifier for the last selection recorded. - * - * @property _lastSelection - * @param lastselection - * @type string - * @private - */ - _lastSelection: null, - - /** - * Whether focus came from a click event. - * - * This is used to determine whether to restore the selection or not. - * - * @property _focusFromClick - * @type Boolean - * @default false - * @private - */ - _focusFromClick: false, - - /** - * Set up the watchers for selection save and restoration. - * - * @method setupSelectionWatchers - * @chainable - */ - setupSelectionWatchers: function() { - // Save the selection when a change was made. - this._registerEventHandle(this.on('ousupsub:selectionchanged', this.saveSelection, this)); - - this._registerEventHandle(this.editor.on('focus', this.restoreSelection, this)); - - // Do not restore selection when focus is from a click event. - this._registerEventHandle(this.editor.on('mousedown', function() { - this._focusFromClick = true; - }, this)); - - // Copy the current value back to the textarea when focus leaves us and save the current selection. - this._registerEventHandle(this.editor.on('blur', function() { - // Clear the _focusFromClick value. - this._focusFromClick = false; - - // Update the original text area. - this.updateOriginal(); - }, this)); - - this._registerEventHandle(this.editor.on(['keyup', 'focus'], function(e) { - setTimeout(Y.bind(this._hasSelectionChanged, this, e), 0); - }, this)); - - // To capture both mouseup and touchend events, we need to track the gesturemoveend event in standAlone mode. Without - // standAlone, it will only fire if we listened to a gesturemovestart too. - this._registerEventHandle(this.editor.on('gesturemoveend', function(e) { - setTimeout(Y.bind(this._hasSelectionChanged, this, e), 0); - }, { - standAlone: true - }, this)); - - return this; - }, - - /** - * Work out if the cursor is in the editable area for this editor instance. - * - * @method isActive - * @return {boolean} - */ - isActive: function() { - var range = window.rangy.createRange(), - selection = window.rangy.getSelection(); - - if (!selection.rangeCount) { - // If there was no range count, then there is no selection. - return false; - } - - // We can't be active if the editor doesn't have focus at the moment. - if (!document.activeElement || - !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) { - return false; - } - - // Check whether the range intersects the editor selection. - range.selectNode(this.editor.getDOMNode()); - return range.intersectsRange(selection.getRangeAt(0)); - }, - - /** - * Create a cross browser selection object that represents a YUI node. - * - * @method getSelectionFromNode - * @param {Node} YUI Node to base the selection upon. - * @return {[rangy.Range]} - */ - getSelectionFromNode: function(node) { - var range = window.rangy.createRange(); - range.selectNode(node.getDOMNode()); - return [range]; - }, - - /** - * Save the current selection to an internal property. - * - * This allows more reliable return focus, helping improve keyboard navigation. - * - * Should be used in combination with {{#crossLink "M.editor_ousupsub.EditorSelection/restoreSelection"}}{{/crossLink}}. - * - * @method saveSelection - */ - saveSelection: function() { - if (this.isActive()) { - this._selections = this.getSelection(); - } - }, - - /** - * Restore any stored selection when the editor gets focus again. - * - * Should be used in combination with {{#crossLink "M.editor_ousupsub.EditorSelection/saveSelection"}}{{/crossLink}}. - * - * @method restoreSelection - */ - restoreSelection: function() { - if (!this._focusFromClick) { - if (this._selections) { - this.setSelection(this._selections); - } - } - this._focusFromClick = false; - }, - - /** - * Get the selection object that can be passed back to setSelection. - * - * @method getSelection - * @return {array} An array of rangy ranges. - */ - getSelection: function() { - return window.rangy.getSelection().getAllRanges(); - }, - - /** - * Check that a YUI node it at least partly contained by the current selection. - * - * @method selectionContainsNode - * @param {Node} The node to check. - * @return {boolean} - */ - selectionContainsNode: function(node) { - return window.rangy.getSelection().containsNode(node.getDOMNode(), true); - }, - - /** - * Runs a filter on each node in the selection, and report whether the - * supplied selector(s) were found in the supplied Nodes. - * - * By default, all specified nodes must match the selection, but this - * can be controlled with the requireall property. - * - * @method selectionFilterMatches - * @param {String} selector - * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time. - * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough. - * @return {Boolean} - */ - selectionFilterMatches: function(selector, selectednodes, requireall) { - if (typeof requireall === 'undefined') { - requireall = true; - } - if (!selectednodes) { - // Find this because it was not passed as a param. - selectednodes = this.getSelectedNodes(); - } - var allmatch = selectednodes.size() > 0, - anymatch = false; - - var editor = this.editor, - stopFn = function(node) { - // The function getSelectedNodes only returns nodes within the editor, so this test is safe. - return node === editor; - }; - - // If we do not find at least one match in the editor, no point trying to find them in the selection. - if (!editor.one(selector)) { - return false; - } - - selectednodes.each(function(node){ - // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing. - if (requireall) { - // Check for at least one failure. - if (!allmatch || !node.ancestor(selector, true, stopFn)) { - allmatch = false; - } - } else { - // Check for at least one match. - if (!anymatch && node.ancestor(selector, true, stopFn)) { - anymatch = true; - } - } - }, this); - if (requireall) { - return allmatch; - } else { - return anymatch; - } - }, - - /** - * Get the deepest possible list of nodes in the current selection. - * - * @method getSelectedNodes - * @return {NodeList} - */ - getSelectedNodes: function() { - var results = new Y.NodeList(), - nodes, - selection, - range, - node, - i; - - selection = window.rangy.getSelection(); - - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } else { - // Empty range. - range = window.rangy.createRange(); - } - - if (range.collapsed) { - // We do not want to select all the nodes in the editor if we managed to - // have a collapsed selection directly in the editor. - // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle - // so we must filter that out here too. - if (range.commonAncestorContainer !== this.editor.getDOMNode() - && range.commonAncestorContainer !== Y.config.doc) { - range = range.cloneRange(); - range.selectNode(range.commonAncestorContainer); - } - } - - nodes = range.getNodes(); - - for (i = 0; i < nodes.length; i++) { - node = Y.one(nodes[i]); - if (this.editor.contains(node)) { - results.push(node); - } - } - return results; - }, - - /** - * Check whether the current selection has changed since this method was last called. - * - * If the selection has changed, the ousupsub:selectionchanged event is also fired. - * - * @method _hasSelectionChanged - * @private - * @param {EventFacade} e - * @return {Boolean} - */ - _hasSelectionChanged: function(e) { - var selection = window.rangy.getSelection(), - range, - changed = false; - - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } else { - // Empty range. - range = window.rangy.createRange(); - } - - if (this._lastSelection) { - if (!this._lastSelection.equals(range)) { - changed = true; - return this._fireSelectionChanged(e); - } - } - this._lastSelection = range; - return changed; - }, - - /** - * Fires the ousupsub:selectionchanged event. - * - * When the selectionchanged event is fired, the following arguments are provided: - * - event : the original event that lead to this event being fired. - * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content. - * - * @method _fireSelectionChanged - * @private - * @param {EventFacade} e - */ - _fireSelectionChanged: function(e) { - this.fire('ousupsub:selectionchanged', { - event: e, - selectedNodes: this.getSelectedNodes() - }); - }, - - /** - * Get the DOM node representing the common anscestor of the selection nodes. - * - * @method getSelectionParentNode - * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made. - */ - getSelectionParentNode: function() { - var selection = window.rangy.getSelection(); - if (selection.rangeCount) { - return selection.getRangeAt(0).commonAncestorContainer; - } - return false; - }, - - /** - * Set the current selection. Used to restore a selection. - * - * @method selection - * @param {array} ranges A list of rangy.range objects in the selection. - */ - setSelection: function(ranges) { - var selection = window.rangy.getSelection(); - selection.setRanges(ranges); - }, - - /** - * Inserts the given HTML into the editable content at the currently focused point. - * - * @method insertContentAtFocusPoint - * @param {String} html - * @return {Node} The YUI Node object added to the DOM. - */ - insertContentAtFocusPoint: function(html) { - var selection = window.rangy.getSelection(), - range, - node = Y.Node.create(html); - if (selection.rangeCount) { - range = selection.getRangeAt(0); - } - if (range) { - range.deleteContents(); - range.insertNode(node.getDOMNode()); - } - return node; - } - -}; - -Y.Base.mix(Y.M.editor_ousupsub.Editor, [EditorSelection]); -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * ousupsub editor plugin. - * - * @module moodle-editor_ousupsub-editor - * @submodule plugin-base - * @package editor_ousupsub - * @copyright 2014 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * A Plugin for the ousupsub Editor used in Moodle. - * - * This class should not be directly instantiated, and all Editor plugins - * should extend this class. - * - * @namespace M.editor_ousupsub - * @class EditorPlugin - * @main - * @constructor - * @uses M.editor_ousupsub.EditorPluginButtons - */ - -function EditorPlugin() { - EditorPlugin.superclass.constructor.apply(this, arguments); -} - -var DISABLED = 'disabled', - HIGHLIGHT = 'highlight', - GROUPSELECTOR = '.ousupsub_group.', - GROUP = '_group'; - -Y.extend(EditorPlugin, Y.Base, { - /** - * The name of the current plugin. - * - * @property name - * @type string - */ - name: null, - - /** - * The name of the command to execute when the button is clicked. - * - * @property exec - * @type string - */ - exec: null, - - /** - * A Node reference to the editor. - * - * @property editor - * @type Node - */ - editor: null, - - /** - * A Node reference to the editor toolbar. - * - * @property toolbar - * @type Node - */ - toolbar: null, - - /** - * Event Handles to clear on plugin destruction. - * - * @property _eventHandles - * @private - */ - _eventHandles: null, - - /** - * All of the buttons that belong to this plugin instance. - * - * Buttons are stored by button name. - * - * @property buttons - * @type object - */ - buttons: null, - - /** - * A list of each of the button names. - * - * @property buttonNames - * @type array - */ - buttonNames: null, - - /** - * A read-only view of the current state for each button. Mappings are stored by name. - * - * Possible states are: - *

    - *
  • {{#crossLink "M.editor_ousupsub.EditorPluginButtons/ENABLED:property"}}{{/crossLink}}; and
  • - *
  • {{#crossLink "M.editor_ousupsub.EditorPluginButtons/DISABLED:property"}}{{/crossLink}}.
  • - *
- * - * @property buttonStates - * @type object - */ - buttonStates: null, - - /** - * The state for a disabled button. - * - * @property DISABLED - * @type Number - * @static - * @value 0 - */ - DISABLED: 0, - - /** - * The state for an enabled button. - * - * @property ENABLED - * @type Number - * @static - * @value 1 - */ - ENABLED: 1, - - /** - * The list of Event Handlers for buttons. - * - * @property _buttonHandlers - * @protected - * @type array - */ - _buttonHandlers: null, - - /** - * A textual description of the primary keyboard shortcut for this - * plugin. - * - * This will be null if no keyboard shortcut has been registered. - * - * @property _primaryKeyboardShortcut - * @protected - * @type String - * @default null - */ - _primaryKeyboardShortcut: null, - - /** - * An list of handles returned by setTimeout(). - * - * The keys will be the buttonName of the button, and the value the handles. - * - * @property _highlightQueue - * @protected - * @type Object - * @default null - */ - _highlightQueue: null, - - initializer: function(config) { - // Set the references to configuration parameters. - this.name = config.name; - this.exec = config.exec; - this.toolbar = config.toolbar; - this.editor = config.editor; - - // Set up the prototypal properties. - // These must be set up here because prototypal arrays and objects are copied across instances. - this.buttons = {}; - this.buttonNames = []; - this.buttonStates = {}; - this._primaryKeyboardShortcut = []; - this._buttonHandlers = []; - this._menuHideHandlers = []; - this._highlightQueue = {}; - this._eventHandles = []; - this.addButton(config); - }, - - destructor: function() { - // Detach all EventHandles. - new Y.EventHandle(this._eventHandles).detach(); - }, - - /** - * Mark the content ediable content as having been changed. - * - * This is a convenience function and passes through to - * {{#crossLink "M.editor_ousupsub.EditorTextArea/updateOriginal"}}updateOriginal{{/crossLink}}. - * - * @method markUpdated - */ - markUpdated: function() { - // Save selection after changes to the DOM. If you don't do this here, - // subsequent calls to restoreSelection() will fail expecting the - // previous DOM state. - this.get('host').saveSelection(); - - return this.get('host').updateOriginal(); - }, - - /** - * Register an event handle for disposal in the destructor. - * - * @method registerEventHandle - * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate. - */ - registerEventHandle: function(handle) { - this._eventHandles.push(handle); - }, - - /** - * Add a button for this plugin to the toolbar. - * - * @method addButton - * @param {object} config The configuration for this button - * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. - * @param {string} [config.icon] The icon identifier. - * @param {string} [config.iconComponent='core'] The icon component. - * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. - * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts. - * If not specified, this is automatically generated based on config.keys. - * If multiple key bindings are supplied to config.keys, then only the first is used. - * If set to false, then no description is added to the title. - * @param {string} [config.tags] The tags that trigger this button to be highlighted. - * @param {boolean} [config.tagMatchRequiresAll=true] Working in combination with the tags parameter, when true - * every tag of the selection has to match. When false, only one match is needed. Only set this to false when - * necessary as it is much less efficient. - * See {{#crossLink "M.editor_ousupsub.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. - * @param {string} [config.title=this.name] The string identifier in the plugin's language file. - * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if - * specified, in the class for the button. - * @param {function} config.callback A callback function to call when the button is clicked. - * @param {object} [config.callbackArgs] Any arguments to pass to the callback. - * @return {Node} The Node representing the newly created button. - */ - addButton: function(config) { - var group = this.get('group'), - pluginname = this.name, - buttonClass = 'ousupsub_' + pluginname + '_button', - button, - host = this.get('host'); - - if (config.exec) { - buttonClass = buttonClass + '_' + config.exec; - } - - if (!config.buttonName) { - // Set a default button name - this is used as an identifier in the button object. - config.buttonName = config.exec || pluginname; - } else { - buttonClass = buttonClass + '_' + config.buttonName; - } - config.buttonClass = buttonClass; - - // Normalize icon configuration. - config = this._normalizeIcon(config); - - if (!config.title) { - config.title = 'pluginname'; - } - var title = M.util.get_string(pluginname, 'editor_ousupsub'); - - // Create the actual button. - var icon = ''; - if (config.iconurl) { - icon = ''; - } - button = Y.Node.create(''); - button.setAttribute('title', title); - - // Append it to the group. - group.append(button); - - var currentfocus = this.toolbar.getAttribute('aria-activedescendant'); - if (!currentfocus) { - // Initially set the first button in the toolbar to be the default on keyboard focus. - // Initially set the first button in the toolbar to be the default on keyboard focus. - button.setAttribute('tabindex', '0'); - this.toolbar.setAttribute('aria-activedescendant', button.generateID()); - this.get('host')._tabFocus = button; - } - // Normalize the callback parameters. - if (!config.callback) { - config.callback = this._applyTextCommand; - } - config.callback = Y.rbind(this._callbackWrapper, this, config.callback); - - // Add the standard click handler to the button. - this._buttonHandlers.push( - this.toolbar.delegate('click', config.callback, '.' + buttonClass, this) - ); - - // Handle button click via shortcut key. - if (config.keys) { - if (typeof config.keyDescription !== 'undefined') { - // A keyboard shortcut description was specified - use it. - this._primaryKeyboardShortcut[buttonClass] = config.keyDescription; - } - this._addKeyboardListener(config.callback, config.keys, buttonClass); - - if (this._primaryKeyboardShortcut[buttonClass]) { - // If we have a valid keyboard shortcut description, then set it with the title. - button.setAttribute('title', M.util.get_string('plugin_title_shortcut', 'editor_ousupsub', { - title: title, - shortcut: this._primaryKeyboardShortcut[buttonClass] - })); - } - } - - // Handle highlighting of the button. - if (config.tags) { - var tagMatchRequiresAll = true; - if (typeof config.tagMatchRequiresAll === 'boolean') { - tagMatchRequiresAll = config.tagMatchRequiresAll; - } - this._buttonHandlers.push( - host.on(['ousupsub:selectionchanged', 'change'], function(e) { - if (typeof this._highlightQueue[config.buttonName] !== 'undefined') { - clearTimeout(this._highlightQueue[config.buttonName]); - } - // Async the highlighting. - this._highlightQueue[config.buttonName] = setTimeout(Y.bind(function(e) { - if (host.selectionFilterMatches(config.tags, e.selectedNodes, tagMatchRequiresAll)) { - this.highlightButtons(config.buttonName); - } else { - this.unHighlightButtons(config.buttonName); - } - }, this, e), 0); - }, this) - ); - } - - // Add the button reference to the buttons array for later reference. - this.buttonNames.push(config.buttonName); - this.buttons[config.buttonName] = button; - this.buttonStates[config.buttonName] = this.ENABLED; - return button; - }, - - /** - * Normalize and sanitize the configuration variables relating to callbacks. - * - * @method _normalizeCallback - * @param {object} config - * @param {function} config.callback A callback function to call when the button is clicked. - * @param {object} [config.callbackArgs] Any arguments to pass to the callback. - * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from. - * @return {object} The normalized configuration - * @private - */ - _normalizeCallback: function(config, inheritFrom) { - if (config._callbackNormalized) { - // Return early if the callback has already been normalized. - return config; - } - - if (!inheritFrom) { - // Create an empty inheritFrom to make life easier below. - inheritFrom = {}; - } - - // We wrap the callback in function to prevent the default action, check whether the editor is - // active and focus it, and then mark the field as updated. - config._callback = config.callback || inheritFrom.callback; - config.callback = Y.rbind(this._callbackWrapper, this, this._applyTextCommand, config.callbackArgs); - - config._callbackNormalized = true; - - return config; - }, - - /** - * Normalize and sanitize the configuration variables relating to icons. - * - * @method _normalizeIcon - * @param {object} config - * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. - * @param {string} [config.icon] The icon identifier. - * @param {string} [config.iconComponent='core'] The icon component. - * @return {object} The normalized configuration - * @private - */ - _normalizeIcon: function(config) { - if (config.icon && !config.iconurl) { - // The default icon component. - if (!config.iconComponent) { - config.iconComponent = 'core'; - } - config.iconurl = M.util.image_url(config.icon, config.iconComponent); - } - - return config; - }, - - /** - * A wrapper in which to run the callbacks. - * - * This handles common functionality such as: - *
    - *
  • preventing the default action; and
  • - *
  • focusing the editor if relevant.
  • - *
- * - * @method _callbackWrapper - * @param {EventFacade} e - * @param {Function} callback The function to call which makes the relevant changes. - * @param {Array} [callbackArgs] The arguments passed to this callback. - * @return {Mixed} The value returned by the callback. - * @private - */ - _callbackWrapper: function(e, callback, callbackArgs) { - e.preventDefault(); - - if (!this.isEnabled()) { - // Exit early if the plugin is disabled. - return; - } - - var creatorButton = e.currentTarget.ancestor('button', true); - - if (creatorButton && creatorButton.hasAttribute(DISABLED)) { - // Exit early if the clicked button was disabled. - return; - } - - if (!(YUI.Env.UA.android || this.get('host').isActive())) { - // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion - // changes the cursor position. - // If we save that change, then when we restore the change later we get put in the wrong place. - // Android is fine to save the selection without the editor being in focus. - this.get('host').focus(); - } - - // Save the selection. - this.get('host').saveSelection(); - - // Build the arguments list, but remove the callback we're calling. - var args = [e, callbackArgs]; - - // Restore selection before making changes. - this.get('host').restoreSelection(); - - // Actually call the callback now. - return callback.apply(this, args); - }, - - /** - * Add a keyboard listener to call the callback. - * - * The keyConfig will take either an array of keyConfigurations, in - * which case _addKeyboardListener is called multiple times; an object - * containing an optional eventtype, optional container, and a set of - * keyCodes, or just a string containing the keyCodes. When keyConfig is - * not an object, it is wrapped around a function that ensures that - * only the expected key modifiers were used. For instance, it checks - * that space+ctrl is not triggered when the user presses ctrl+shift+space. - * When using an object, the developer should check that manually. - * - * @method _addKeyboardListener - * @param {function} callback - * @param {array|object|string} keyConfig - * @param {string} [keyConfig.eventtype=key] The type of event - * @param {string} [keyConfig.container=.editor_ousupsub_content] The containing element. - * @param {string} keyConfig.keyCodes The keycodes to user for the event. - * @private - * - */ - _addKeyboardListener: function(callback, keyConfig, buttonName) { - var eventtype = 'key', - container = CSS.EDITORWRAPPER, - keys, - handler, - modifier; - - if (Y.Lang.isArray(keyConfig)) { - // If an Array was specified, call the add function for each element. - Y.Array.each(keyConfig, function(config) { - this._addKeyboardListener(callback, config, buttonName); - }, this); - - return this; - - } else if (typeof keyConfig === "object") { - if (keyConfig.eventtype) { - eventtype = keyConfig.eventtype; - } - - if (keyConfig.container) { - container = keyConfig.container; - } - - // Must be specified. - keys = keyConfig.keyCodes; - handler = callback; - - } else { - modifier = ''; - keys = keyConfig; - if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') { - this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig); - } - // Wrap the callback into a handler to check if it uses the specified modifiers, not more. - handler = Y.bind(function(modifiers, e) { - callback.apply(this, [e]); - }, this, [modifier]); - } - - this._buttonHandlers.push( - this.editor.delegate( - eventtype, - handler, - keys, - container, - this - ) - ); - - }, - - /** - * Checks if a key event was strictly defined for the modifiers passed. - * - * @method _eventUsesExactKeyModifiers - * @param {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift). - * @param {EventFacade} e The event facade. - * @return {Boolean} True if the event was stricly using the modifiers specified. - */ - _eventUsesExactKeyModifiers: function(modifiers, e) { - var exactMatch = true, - hasKey; - - if (e.type !== 'key') { - return false; - } - - hasKey = Y.Array.indexOf(modifiers, 'alt') > -1; - exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1; - exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'meta') > -1; - exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey)); - hasKey = Y.Array.indexOf(modifiers, 'shift') > -1; - exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey)); - - return exactMatch; - }, - - /** - * Determine if this plugin is enabled, based upon the state of it's buttons. - * - * @method isEnabled - * @return {boolean} - */ - isEnabled: function() { - // The first instance of an undisabled button will make this return true. - var found = Y.Object.some(this.buttonStates, function(button) { - return (button === this.ENABLED); - }, this); - - return found; - }, - - /** - * Enable one button, or all buttons relating to this Plugin. - * - * If no button is specified, all buttons are disabled. - * - * @method disableButtons - * @param {String} [button] The name of a specific plugin to enable. - * @chainable - */ - disableButtons: function(button) { - return this._setButtonState(false, button); - }, - - /** - * Enable one button, or all buttons relating to this Plugin. - * - * If no button is specified, all buttons are enabled. - * - * @method enableButtons - * @param {String} [button] The name of a specific plugin to enable. - * @chainable - */ - enableButtons: function(button) { - return this._setButtonState(true, button); - }, - - /** - * Set the button state for one button, or all buttons associated with this plugin. - * - * @method _setButtonState - * @param {Boolean} enable Whether to enable this button. - * @param {String} [button] The name of a specific plugin to set state for. - * @chainable - * @private - */ - _setButtonState: function(enable, button) { - var attributeChange = 'setAttribute'; - if (enable) { - attributeChange = 'removeAttribute'; - } - if (button) { - if (this.buttons[button]) { - this.buttons[button][attributeChange](DISABLED, DISABLED); - this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; - } - } else { - Y.Array.each(this.buttonNames, function(button) { - this.buttons[button][attributeChange](DISABLED, DISABLED); - this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; - }, this); - } - - this.get('host').checkTabFocus(); - return this; - }, - - /** - * Highlight a button, or buttons in the toolbar. - * - * If no button is specified, all buttons are highlighted. - * - * @method highlightButtons - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @chainable - */ - highlightButtons: function(button) { - return this._changeButtonHighlight(true, button); - }, - - /** - * Un-highlight a button, or buttons in the toolbar. - * - * If no button is specified, all buttons are un-highlighted. - * - * @method unHighlightButtons - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @chainable - */ - unHighlightButtons: function(button) { - return this._changeButtonHighlight(false, button); - }, - - /** - * Highlight a button, or buttons in the toolbar. - * - * @method _changeButtonHighlight - * @param {boolean} highlight true - * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. - * @protected - * @chainable - */ - _changeButtonHighlight: function(highlight, button) { - var method = 'addClass'; - - if (!highlight) { - method = 'removeClass'; - } - if (button) { - if (this.buttons[button]) { - this.buttons[button][method](HIGHLIGHT); - } - } else { - Y.Object.each(this.buttons, function(button) { - button[method](HIGHLIGHT); - }, this); - } - - return this; - }, - - /** - * Get the default meta key to use with keyboard events. - * - * On a Mac, this will be the 'meta' key for Command; otherwise it will - * be the Control key. - * - * @method _getDefaultMetaKey - * @return {string} - * @private - */ - _getDefaultMetaKey: function() { - if (Y.UA.os === 'macintosh') { - return 'meta'; - } else { - return 'ctrl'; - } - }, - - /** - * Get the user-visible description of the meta key to use with keyboard events. - * - * On a Mac, this will be 'Command' ; otherwise it will be 'Control'. - * - * @method _getDefaultMetaKeyDescription - * @return {string} - * @private - */ - _getDefaultMetaKeyDescription: function(keyCode) { - if (Y.UA.os === 'macintosh') { - return M.util.get_string('editor_command_keycode', 'editor_ousupsub', String.fromCharCode(keyCode).toLowerCase()); - } else { - return M.util.get_string('editor_control_keycode', 'editor_ousupsub', String.fromCharCode(keyCode).toLowerCase()); - } - }, - - /** - * Get the standard key event to use for keyboard events. - * - * @method _getKeyEvent - * @return {string} - * @private - */ - _getKeyEvent: function() { - return 'down:'; - }, - - /** - * Apply the given document.execCommand and tidy up the editor dom afterwards. - * - * @method _applyTextCommand - * @private - * @return void - */ - _applyTextCommand: function(e) { - var mode = 0; - - if(e && e.type === 'key') { - // handled by this._getEditor().textareaKeyboardNavigation(e); - return; - } - - this._getEditor()._applyTextCommand(this.exec, mode); - }, - - /** - * Get the editor object. - * - * @method _getEditor - * @private - * @return node. - */ - _getEditor: function(host) { - if (!host) { - host = this.get('host'); - } - - return host; - }, - - /** - * Get the node containing the editor html to be updated. - * - * @method _getEditorNode - * @private - * @return node. - */ - _getEditorNode: function(host) { - return this._getEditor(host).editor._node; - } - -}, { - NAME: 'editorPlugin', - ATTRS: { - /** - * The editor instance that this plugin was instantiated by. - * - * @attribute host - * @type M.editor_ousupsub.Editor - * @writeOnce - */ - host: { - writeOnce: true - }, - - /** - * The toolbar group that this button belongs to. - * - * When setting, the name of the group should be specified. - * - * When retrieving, the Node for the toolbar group is returned. If - * the group doesn't exist yet, then it is created first. - * - * @attribute group - * @type Node - * @writeOnce - */ - group: { - writeOnce: true, - getter: function(groupName) { - var group = this.toolbar.one(GROUPSELECTOR + groupName + GROUP); - if (!group) { - group = Y.Node.create('
'); - this.toolbar.append(group); - } - - return group; - } - } - } -}); - -Y.namespace('M.editor_ousupsub').EditorPlugin = EditorPlugin; -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * The manager for the OUSupSub Editor. - * - * @module moodle-editor_ousupsub-editor - * @submodule manager - * @package editor_ousupsub - * @copyright 2014 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @main moodle-editor_ousupsub-editor - */ - -/** - * @module moodle-editor_ousupsub-editor - */ - -/** - * The manager for the OUSupSub editor. - * - * @class editor_ousupsub - */ - -Y.M.editor_ousupsub = Y.M.editor_ousupsub || {}; - -/** - * List of editor_ousupsub instances. Intentionally placed on window.M, not - * something in the namespace, so we can be sure it is really global. - */ -M = M || {}; -M.editor_ousupsub = M.editor_ousupsub || {}; -M.editor_ousupsub._instances = M.editor_ousupsub._instances || {}; - -/** - * Add a reference to an editor. - * Note: This is an internal method which should only be called by the editor itself. - * - * @method addEditorReference - * @param {String} name The name of the editor instance to add - * @private - */ -Y.M.editor_ousupsub.addEditorReference = function(name, reference) { - if (typeof M.editor_ousupsub._instances[name] === 'undefined') { - M.editor_ousupsub._instances[name] = reference; - } else { - } - - return Y.M.editor_ousupsub; -}; - -/** - * Create a new editor using simple options. - * - * @method createEditor - * @param {String} id of the textarea to turn into an editor. - * @param {String} type 'superscript', 'subscript' or 'both'. - * @return {M.editor_ousupsub.Editor} The newly created editor instance - */ -Y.M.editor_ousupsub.createEditorSimple = function(id, type) { - var plugins = []; - if (type === 'both' || type === 'superscript') { - plugins.push({"name": "superscript", "params": []}); - } - if (type === 'both' || type === 'subscript') { - plugins.push({"name": "subscript", "params": []}); - } - - Y.M.editor_ousupsub.createEditor( - {"elementid" : id, "content_css" : "", "contextid" : 0, "language" : "en", - "directionality" : "ltr", "plugins" : [{"group" : "style1", "plugins" : plugins}],"pageHash" : ""}); -}; - -/** - * Create a new editor using the specified configuration. - * - * @method createEditor - * @param {Object} config See the attributes for {{#crossLink - * "M.editor_ousupsub.Editor"}}{{/crossLink}} for configuration options. The - * elementid provided will be used as the name of this editor within - * the editor Manager. - * @return {M.editor_ousupsub.Editor} The newly created editor instance - */ -Y.M.editor_ousupsub.createEditor = function(config) { - - var instance = new Y.M.editor_ousupsub.Editor(config); - Y.M.editor_ousupsub.fire('editor_ousupsub:created', { - id: instance.get('elementid'), - instance: instance - }); - return instance; -}; - -/** - * Get the requested Editor instance. - * - * @method getEditor - * @param {String} name The name of the editor instance to retrieve - * @return {M.editor_ousupsub.Editor} The requested editor instance - */ -Y.M.editor_ousupsub.getEditor = function(name) { - return M.editor_ousupsub._instances[name]; -}; - -/** - * Remove the reference for an editor. - * - * @method removeEditorReference - * @param {String} name The name of the editor instance to remove - */ -Y.M.editor_ousupsub.removeEditor = function(name) { - var instance = Y.M.editor_ousupsub.getEditor(name); - if (instance) { - instance.destroy(); - this.fire('editor_ousupsub:removed', { - id: name - }); - } - return Y.M.editor_ousupsub; -}; - -/** - * Remove the reference for an editor. - * Note: This is an internal method which should only be called by the editor itself. - * - * @method removeEditorReference - * @param {String} name The name of the editor instance to remove - * @private - */ -Y.M.editor_ousupsub.removeEditorReference = function(name) { - if (Y.M.editor_ousupsub.getEditor(name)) { - delete M.editor_ousupsub._instances[name]; - } -}; - -/** - * Add the supplied function to the manager using the specified name. - * - * @method addMethod - * @param {String} name The name to store the method on within the editor manager. - * @param {Function} fn The function to be added. - * @param {Object} [context] The context to apply the function with. If not specified, the Editor itself is used. - */ -Y.M.editor_ousupsub.addMethod = function(name, fn) { - if (typeof this[name] !== 'undefined') { - } - - Y.M.editor_ousupsub[name] = function() { - var ret = [], - args = arguments; - - Y.Object.each(M.editor_ousupsub._instances, function(editor) { - var result = fn.apply(editor, args); - - if (result !== undefined && result !== editor) { - ret[ret.length] = result; - } - }); - - // If we received a set of results, return them, otherwise make this method chainable. - return ret.length ? ret : this; - }; -}; - -Y.augment(Y.M.editor_ousupsub, Y.EventTarget); - -Y.Array.each(['saveSelection', 'updateFromTextArea', 'updateOriginal', 'cleanEditorHTML', 'destroy'], function(name) { - Y.M.editor_ousupsub.addMethod(name, Y.M.editor_ousupsub.Editor.prototype[name]); -}); - - -}, '@VERSION@', {"requires": ["base", "node", "event", "event-custom", "moodle-editor_ousupsub-rangy"]}); diff --git a/yui/build/moodle-editor_ousupsub-rangy/moodle-editor_ousupsub-rangy-debug.js b/yui/build/moodle-editor_ousupsub-rangy/moodle-editor_ousupsub-rangy-debug.js deleted file mode 100644 index 9667943..0000000 --- a/yui/build/moodle-editor_ousupsub-rangy/moodle-editor_ousupsub-rangy-debug.js +++ /dev/null @@ -1,8087 +0,0 @@ -/** - * Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Copyright 2022, Tim Down - * Licensed under the MIT license. - * Version: 1.3.1 - * Build date: 17 August 2022 - */ - -(function(factory, root) { - // No AMD or CommonJS support so we place Rangy in (probably) the global variable - root.rangy = factory(); -})(function() { - - var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; - - // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START - // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. - var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", - "commonAncestorContainer"]; - - // Minimal set of methods required for DOM Level 2 Range compliance - var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", - "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", - "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; - - var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; - - // Subset of TextRange's full set of methods that we're interested in - var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", - "setEndPoint", "getBoundingClientRect"]; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Trio of functions taken from Peter Michaux's article: - // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting - function isHostMethod(o, p) { - var t = typeof o[p]; - return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; - } - - function isHostObject(o, p) { - return !!(typeof o[p] == OBJECT && o[p]); - } - - function isHostProperty(o, p) { - return typeof o[p] != UNDEFINED; - } - - // Creates a convenience function to save verbose repeated calls to tests functions - function createMultiplePropertyTest(testFunc) { - return function(o, props) { - var i = props.length; - while (i--) { - if (!testFunc(o, props[i])) { - return false; - } - } - return true; - }; - } - - // Next trio of functions are a convenience to save verbose repeated calls to previous two functions - var areHostMethods = createMultiplePropertyTest(isHostMethod); - var areHostObjects = createMultiplePropertyTest(isHostObject); - var areHostProperties = createMultiplePropertyTest(isHostProperty); - - function isTextRange(range) { - return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); - } - - function getBody(doc) { - return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; - } - - var forEach = [].forEach ? - function(arr, func) { - arr.forEach(func); - } : - function(arr, func) { - for (var i = 0, len = arr.length; i < len; ++i) { - func(arr[i], i); - } - }; - - var modules = {}; - - var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); - - var util = { - isHostMethod: isHostMethod, - isHostObject: isHostObject, - isHostProperty: isHostProperty, - areHostMethods: areHostMethods, - areHostObjects: areHostObjects, - areHostProperties: areHostProperties, - isTextRange: isTextRange, - getBody: getBody, - forEach: forEach - }; - - var api = { - version: "1.3.1", - initialized: false, - isBrowser: isBrowser, - supported: true, - util: util, - features: {}, - modules: modules, - config: { - alertOnFail: false, - alertOnWarn: false, - preferTextRange: false, - autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize - } - }; - - function consoleLog(msg) { - if (typeof console != UNDEFINED && isHostMethod(console, "log")) { - console.log(msg); - } - } - - function alertOrLog(msg, shouldAlert) { - if (isBrowser && shouldAlert) { - alert(msg); - } else { - consoleLog(msg); - } - } - - function fail(reason) { - api.initialized = true; - api.supported = false; - alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); - } - - api.fail = fail; - - function warn(msg) { - alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); - } - - api.warn = warn; - - // Add utility extend() method - var extend; - if ({}.hasOwnProperty) { - util.extend = extend = function(obj, props, deep) { - var o, p; - for (var i in props) { - if (props.hasOwnProperty(i)) { - o = obj[i]; - p = props[i]; - if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { - extend(o, p, true); - } - obj[i] = p; - } - } - // Special case for toString, which does not show up in for...in loops in IE <= 8 - if (props.hasOwnProperty("toString")) { - obj.toString = props.toString; - } - return obj; - }; - - util.createOptions = function(optionsParam, defaults) { - var options = {}; - extend(options, defaults); - if (optionsParam) { - extend(options, optionsParam); - } - return options; - }; - } else { - fail("hasOwnProperty not supported"); - } - - // Test whether we're in a browser and bail out if not - if (!isBrowser) { - fail("Rangy can only run in a browser"); - } - - // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not - (function() { - var toArray; - - if (isBrowser) { - var el = document.createElement("div"); - el.appendChild(document.createElement("span")); - var slice = [].slice; - try { - if (slice.call(el.childNodes, 0)[0].nodeType == 1) { - toArray = function(arrayLike) { - return slice.call(arrayLike, 0); - }; - } - } catch (e) {} - } - - if (!toArray) { - toArray = function(arrayLike) { - var arr = []; - for (var i = 0, len = arrayLike.length; i < len; ++i) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - util.toArray = toArray; - })(); - - // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or - // normalization of event properties because we don't need this. - var addListener; - if (isBrowser) { - if (isHostMethod(document, "addEventListener")) { - addListener = function(obj, eventType, listener) { - obj.addEventListener(eventType, listener, false); - }; - } else if (isHostMethod(document, "attachEvent")) { - addListener = function(obj, eventType, listener) { - obj.attachEvent("on" + eventType, listener); - }; - } else { - fail("Document does not have required addEventListener or attachEvent method"); - } - - util.addListener = addListener; - } - - var initListeners = []; - - function getErrorDesc(ex) { - return ex.message || ex.description || String(ex); - } - - // Initialization - function init() { - if (!isBrowser || api.initialized) { - return; - } - var testRange; - var implementsDomRange = false, implementsTextRange = false; - - // First, perform basic feature tests - - if (isHostMethod(document, "createRange")) { - testRange = document.createRange(); - if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { - implementsDomRange = true; - } - } - - var body = getBody(document); - if (!body || body.nodeName.toLowerCase() != "body") { - fail("No body element found"); - return; - } - - if (body && isHostMethod(body, "createTextRange")) { - testRange = body.createTextRange(); - if (isTextRange(testRange)) { - implementsTextRange = true; - } - } - - if (!implementsDomRange && !implementsTextRange) { - fail("Neither Range nor TextRange are available"); - return; - } - - api.initialized = true; - api.features = { - implementsDomRange: implementsDomRange, - implementsTextRange: implementsTextRange - }; - - // Initialize modules - var module, errorMessage; - for (var moduleName in modules) { - if ( (module = modules[moduleName]) instanceof Module ) { - module.init(module, api); - } - } - - // Call init listeners - for (var i = 0, len = initListeners.length; i < len; ++i) { - try { - initListeners[i](api); - } catch (ex) { - errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); - consoleLog(errorMessage); - } - } - } - - function deprecationNotice(deprecated, replacement, module) { - if (module) { - deprecated += " in module " + module.name; - } - api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + - replacement + " instead."); - } - - function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { - owner[deprecated] = function() { - deprecationNotice(deprecated, replacement, module); - return owner[replacement].apply(owner, util.toArray(arguments)); - }; - } - - util.deprecationNotice = deprecationNotice; - util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod; - - // Allow external scripts to initialize this library in case it's loaded after the document has loaded - api.init = init; - - // Execute listener immediately if already initialized - api.addInitListener = function(listener) { - if (api.initialized) { - listener(api); - } else { - initListeners.push(listener); - } - }; - - var shimListeners = []; - - api.addShimListener = function(listener) { - shimListeners.push(listener); - }; - - function shim(win) { - win = win || window; - init(); - - // Notify listeners - for (var i = 0, len = shimListeners.length; i < len; ++i) { - shimListeners[i](win); - } - } - - if (isBrowser) { - api.shim = api.createMissingNativeApi = shim; - createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); - } - - function Module(name, dependencies, initializer) { - this.name = name; - this.dependencies = dependencies; - this.initialized = false; - this.supported = false; - this.initializer = initializer; - } - - Module.prototype = { - init: function() { - var requiredModuleNames = this.dependencies || []; - for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { - moduleName = requiredModuleNames[i]; - - requiredModule = modules[moduleName]; - if (!requiredModule || !(requiredModule instanceof Module)) { - throw new Error("required module '" + moduleName + "' not found"); - } - - requiredModule.init(); - - if (!requiredModule.supported) { - throw new Error("required module '" + moduleName + "' not supported"); - } - } - - // Now run initializer - this.initializer(this); - }, - - fail: function(reason) { - this.initialized = true; - this.supported = false; - throw new Error(reason); - }, - - warn: function(msg) { - api.warn("Module " + this.name + ": " + msg); - }, - - deprecationNotice: function(deprecated, replacement) { - api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " + - replacement + " instead"); - }, - - createError: function(msg) { - return new Error("Error in Rangy " + this.name + " module: " + msg); - } - }; - - function createModule(name, dependencies, initFunc) { - var newModule = new Module(name, dependencies, function(module) { - if (!module.initialized) { - module.initialized = true; - try { - initFunc(api, module); - module.supported = true; - } catch (ex) { - var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); - consoleLog(errorMessage); - if (ex.stack) { - consoleLog(ex.stack); - } - } - } - }); - modules[name] = newModule; - return newModule; - } - - api.createModule = function(name) { - // Allow 2 or 3 arguments (second argument is an optional array of dependencies) - var initFunc, dependencies; - if (arguments.length == 2) { - initFunc = arguments[1]; - dependencies = []; - } else { - initFunc = arguments[2]; - dependencies = arguments[1]; - } - - var module = createModule(name, dependencies, initFunc); - - // Initialize the module immediately if the core is already initialized - if (api.initialized && api.supported) { - module.init(); - } - }; - - api.createCoreModule = function(name, dependencies, initFunc) { - createModule(name, dependencies, initFunc); - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately - - function RangePrototype() {} - api.RangePrototype = RangePrototype; - api.rangePrototype = new RangePrototype(); - - function SelectionPrototype() {} - api.selectionPrototype = new SelectionPrototype(); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // DOM utility methods used by Rangy - api.createCoreModule("DomUtil", [], function(api, module) { - var UNDEF = "undefined"; - var util = api.util; - var getBody = util.getBody; - - // Perform feature tests - if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { - module.fail("document missing a Node creation method"); - } - - if (!util.isHostMethod(document, "getElementsByTagName")) { - module.fail("document missing getElementsByTagName method"); - } - - var el = document.createElement("div"); - if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || - !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { - module.fail("Incomplete Element implementation"); - } - - // innerHTML is required for Range's createContextualFragment method - if (!util.isHostProperty(el, "innerHTML")) { - module.fail("Element is missing innerHTML property"); - } - - var textNode = document.createTextNode("test"); - if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || - !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || - !util.areHostProperties(textNode, ["data"]))) { - module.fail("Incomplete Text Node implementation"); - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been - // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that - // contains just the document as a single element and the value searched for is the document. - var arrayContains = /*Array.prototype.indexOf ? - function(arr, val) { - return arr.indexOf(val) > -1; - }:*/ - - function(arr, val) { - var i = arr.length; - while (i--) { - if (arr[i] === val) { - return true; - } - } - return false; - }; - - // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI - function isHtmlNamespace(node) { - var ns; - return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); - } - - function parentElement(node) { - var parent = node.parentNode; - return (parent.nodeType == 1) ? parent : null; - } - - function getNodeIndex(node) { - var i = 0; - while( (node = node.previousSibling) ) { - ++i; - } - return i; - } - - function getNodeLength(node) { - switch (node.nodeType) { - case 7: - case 10: - return 0; - case 3: - case 8: - return node.length; - default: - return node.childNodes.length; - } - } - - function getCommonAncestor(node1, node2) { - var ancestors = [], n; - for (n = node1; n; n = n.parentNode) { - ancestors.push(n); - } - - for (n = node2; n; n = n.parentNode) { - if (arrayContains(ancestors, n)) { - return n; - } - } - - return null; - } - - function isAncestorOf(ancestor, descendant, selfIsAncestor) { - var n = selfIsAncestor ? descendant : descendant.parentNode; - while (n) { - if (n === ancestor) { - return true; - } else { - n = n.parentNode; - } - } - return false; - } - - function isOrIsAncestorOf(ancestor, descendant) { - return isAncestorOf(ancestor, descendant, true); - } - - function getClosestAncestorIn(node, ancestor, selfIsAncestor) { - var p, n = selfIsAncestor ? node : node.parentNode; - while (n) { - p = n.parentNode; - if (p === ancestor) { - return n; - } - n = p; - } - return null; - } - - function isCharacterDataNode(node) { - var t = node.nodeType; - return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment - } - - function isTextOrCommentNode(node) { - if (!node) { - return false; - } - var t = node.nodeType; - return t == 3 || t == 8 ; // Text or Comment - } - - function insertAfter(node, precedingNode) { - var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; - if (nextNode) { - parent.insertBefore(node, nextNode); - } else { - parent.appendChild(node); - } - return node; - } - - // Note that we cannot use splitText() because it is bugridden in IE 9. - function splitDataNode(node, index, positionsToPreserve) { - var newNode = node.cloneNode(false); - newNode.deleteData(0, index); - node.deleteData(index, node.length - index); - insertAfter(newNode, node); - - // Preserve positions - if (positionsToPreserve) { - for (var i = 0, position; position = positionsToPreserve[i++]; ) { - // Handle case where position was inside the portion of node after the split point - if (position.node == node && position.offset > index) { - position.node = newNode; - position.offset -= index; - } - // Handle the case where the position is a node offset within node's parent - else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { - ++position.offset; - } - } - } - return newNode; - } - - function getDocument(node) { - if (node.nodeType == 9) { - return node; - } else if (typeof node.ownerDocument != UNDEF) { - return node.ownerDocument; - } else if (typeof node.document != UNDEF) { - return node.document; - } else if (node.parentNode) { - return getDocument(node.parentNode); - } else { - throw module.createError("getDocument: no document found for node"); - } - } - - function getWindow(node) { - var doc = getDocument(node); - if (typeof doc.defaultView != UNDEF) { - return doc.defaultView; - } else if (typeof doc.parentWindow != UNDEF) { - return doc.parentWindow; - } else { - throw module.createError("Cannot get a window object for node"); - } - } - - function getIframeDocument(iframeEl) { - if (typeof iframeEl.contentDocument != UNDEF) { - return iframeEl.contentDocument; - } else if (typeof iframeEl.contentWindow != UNDEF) { - return iframeEl.contentWindow.document; - } else { - throw module.createError("getIframeDocument: No Document object found for iframe element"); - } - } - - function getIframeWindow(iframeEl) { - if (typeof iframeEl.contentWindow != UNDEF) { - return iframeEl.contentWindow; - } else if (typeof iframeEl.contentDocument != UNDEF) { - return iframeEl.contentDocument.defaultView; - } else { - throw module.createError("getIframeWindow: No Window object found for iframe element"); - } - } - - // This looks bad. Is it worth it? - function isWindow(obj) { - return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); - } - - function getContentDocument(obj, module, methodName) { - var doc; - - if (!obj) { - doc = document; - } - - // Test if a DOM node has been passed and obtain a document object for it if so - else if (util.isHostProperty(obj, "nodeType")) { - doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? - getIframeDocument(obj) : getDocument(obj); - } - - // Test if the doc parameter appears to be a Window object - else if (isWindow(obj)) { - doc = obj.document; - } - - if (!doc) { - throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); - } - - return doc; - } - - function getRootContainer(node) { - var parent; - while ( (parent = node.parentNode) ) { - node = parent; - } - return node; - } - - function comparePoints(nodeA, offsetA, nodeB, offsetB) { - // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing - var nodeC, root, childA, childB, n; - if (nodeA == nodeB) { - // Case 1: nodes are the same - return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; - } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { - // Case 2: node C (container B or an ancestor) is a child node of A - return offsetA <= getNodeIndex(nodeC) ? -1 : 1; - } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { - // Case 3: node C (container A or an ancestor) is a child node of B - return getNodeIndex(nodeC) < offsetB ? -1 : 1; - } else { - root = getCommonAncestor(nodeA, nodeB); - if (!root) { - throw new Error("comparePoints error: nodes have no common ancestor"); - } - - // Case 4: containers are siblings or descendants of siblings - childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); - childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); - - if (childA === childB) { - // This shouldn't be possible - throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); - } else { - n = root.firstChild; - while (n) { - if (n === childA) { - return -1; - } else if (n === childB) { - return 1; - } - n = n.nextSibling; - } - } - } - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried - var crashyTextNodes = false; - - function isBrokenNode(node) { - var n; - try { - n = node.parentNode; - return false; - } catch (e) { - return true; - } - } - - (function() { - var el = document.createElement("b"); - el.innerHTML = "1"; - var textNode = el.firstChild; - el.innerHTML = "
"; - crashyTextNodes = isBrokenNode(textNode); - - api.features.crashyTextNodes = crashyTextNodes; - })(); - - /*----------------------------------------------------------------------------------------------------------------*/ - - function inspectNode(node) { - if (!node) { - return "[No node]"; - } - if (crashyTextNodes && isBrokenNode(node)) { - return "[Broken node]"; - } - if (isCharacterDataNode(node)) { - return '"' + node.data + '"'; - } - if (node.nodeType == 1) { - var idAttr = node.id ? ' id="' + node.id + '"' : ""; - return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; - } - return node.nodeName; - } - - function fragmentFromNodeChildren(node) { - var fragment = getDocument(node).createDocumentFragment(), child; - while ( (child = node.firstChild) ) { - fragment.appendChild(child); - } - return fragment; - } - - var getComputedStyleProperty; - if (typeof window.getComputedStyle != UNDEF) { - getComputedStyleProperty = function(el, propName) { - return getWindow(el).getComputedStyle(el, null)[propName]; - }; - } else if (typeof document.documentElement.currentStyle != UNDEF) { - getComputedStyleProperty = function(el, propName) { - return el.currentStyle ? el.currentStyle[propName] : ""; - }; - } else { - module.fail("No means of obtaining computed style properties found"); - } - - function createTestElement(doc, html, contentEditable) { - var body = getBody(doc); - var el = doc.createElement("div"); - el.contentEditable = "" + !!contentEditable; - if (html) { - el.innerHTML = html; - } - - // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) - var bodyFirstChild = body.firstChild; - if (bodyFirstChild) { - body.insertBefore(el, bodyFirstChild); - } else { - body.appendChild(el); - } - - return el; - } - - function removeNode(node) { - return node.parentNode.removeChild(node); - } - - function NodeIterator(root) { - this.root = root; - this._next = root; - } - - NodeIterator.prototype = { - _current: null, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - var n = this._current = this._next; - var child, next; - if (this._current) { - child = n.firstChild; - if (child) { - this._next = child; - } else { - next = null; - while ((n !== this.root) && !(next = n.nextSibling)) { - n = n.parentNode; - } - this._next = next; - } - } - return this._current; - }, - - detach: function() { - this._current = this._next = this.root = null; - } - }; - - function createIterator(root) { - return new NodeIterator(root); - } - - function DomPosition(node, offset) { - this.node = node; - this.offset = offset; - } - - DomPosition.prototype = { - equals: function(pos) { - return !!pos && this.node === pos.node && this.offset == pos.offset; - }, - - inspect: function() { - return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; - }, - - toString: function() { - return this.inspect(); - } - }; - - function DOMException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "DOMException: " + this.codeName; - } - - DOMException.prototype = { - INDEX_SIZE_ERR: 1, - HIERARCHY_REQUEST_ERR: 3, - WRONG_DOCUMENT_ERR: 4, - NO_MODIFICATION_ALLOWED_ERR: 7, - NOT_FOUND_ERR: 8, - NOT_SUPPORTED_ERR: 9, - INVALID_STATE_ERR: 11, - INVALID_NODE_TYPE_ERR: 24 - }; - - DOMException.prototype.toString = function() { - return this.message; - }; - - api.dom = { - arrayContains: arrayContains, - isHtmlNamespace: isHtmlNamespace, - parentElement: parentElement, - getNodeIndex: getNodeIndex, - getNodeLength: getNodeLength, - getCommonAncestor: getCommonAncestor, - isAncestorOf: isAncestorOf, - isOrIsAncestorOf: isOrIsAncestorOf, - getClosestAncestorIn: getClosestAncestorIn, - isCharacterDataNode: isCharacterDataNode, - isTextOrCommentNode: isTextOrCommentNode, - insertAfter: insertAfter, - splitDataNode: splitDataNode, - getDocument: getDocument, - getWindow: getWindow, - getIframeWindow: getIframeWindow, - getIframeDocument: getIframeDocument, - getBody: getBody, - isWindow: isWindow, - getContentDocument: getContentDocument, - getRootContainer: getRootContainer, - comparePoints: comparePoints, - isBrokenNode: isBrokenNode, - inspectNode: inspectNode, - getComputedStyleProperty: getComputedStyleProperty, - createTestElement: createTestElement, - removeNode: removeNode, - fragmentFromNodeChildren: fragmentFromNodeChildren, - createIterator: createIterator, - DomPosition: DomPosition - }; - - api.DOMException = DOMException; - }); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Pure JavaScript implementation of DOM Range - api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { - var dom = api.dom; - var util = api.util; - var DomPosition = dom.DomPosition; - var DOMException = api.DOMException; - - var isCharacterDataNode = dom.isCharacterDataNode; - var getNodeIndex = dom.getNodeIndex; - var isOrIsAncestorOf = dom.isOrIsAncestorOf; - var getDocument = dom.getDocument; - var comparePoints = dom.comparePoints; - var splitDataNode = dom.splitDataNode; - var getClosestAncestorIn = dom.getClosestAncestorIn; - var getNodeLength = dom.getNodeLength; - var arrayContains = dom.arrayContains; - var getRootContainer = dom.getRootContainer; - var crashyTextNodes = api.features.crashyTextNodes; - - var removeNode = dom.removeNode; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Utility functions - - function isNonTextPartiallySelected(node, range) { - return (node.nodeType != 3) && - (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); - } - - function getRangeDocument(range) { - return range.document || getDocument(range.startContainer); - } - - function getRangeRoot(range) { - return getRootContainer(range.startContainer); - } - - function getBoundaryBeforeNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node)); - } - - function getBoundaryAfterNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node) + 1); - } - - function insertNodeAtPosition(node, n, o) { - var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; - if (isCharacterDataNode(n)) { - if (o == n.length) { - dom.insertAfter(node, n); - } else { - n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); - } - } else if (o >= n.childNodes.length) { - n.appendChild(node); - } else { - n.insertBefore(node, n.childNodes[o]); - } - return firstNodeInserted; - } - - function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { - assertRangeValid(rangeA); - assertRangeValid(rangeB); - - if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); - } - - var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), - endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); - - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - } - - function cloneSubtree(iterator) { - var partiallySelected; - for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - partiallySelected = iterator.isPartiallySelectedSubtree(); - node = node.cloneNode(!partiallySelected); - if (partiallySelected) { - subIterator = iterator.getSubtreeIterator(); - node.appendChild(cloneSubtree(subIterator)); - subIterator.detach(); - } - - if (node.nodeType == 10) { // DocumentType - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - frag.appendChild(node); - } - return frag; - } - - function iterateSubtree(rangeIterator, func, iteratorState) { - var it, n; - iteratorState = iteratorState || { stop: false }; - for (var node, subRangeIterator; node = rangeIterator.next(); ) { - if (rangeIterator.isPartiallySelectedSubtree()) { - if (func(node) === false) { - iteratorState.stop = true; - return; - } else { - // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of - // the node selected by the Range. - subRangeIterator = rangeIterator.getSubtreeIterator(); - iterateSubtree(subRangeIterator, func, iteratorState); - subRangeIterator.detach(); - if (iteratorState.stop) { - return; - } - } - } else { - // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its - // descendants - it = dom.createIterator(node); - while ( (n = it.next()) ) { - if (func(n) === false) { - iteratorState.stop = true; - return; - } - } - } - } - } - - function deleteSubtree(iterator) { - var subIterator; - while (iterator.next()) { - if (iterator.isPartiallySelectedSubtree()) { - subIterator = iterator.getSubtreeIterator(); - deleteSubtree(subIterator); - subIterator.detach(); - } else { - iterator.remove(); - } - } - } - - function extractSubtree(iterator) { - for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - - if (iterator.isPartiallySelectedSubtree()) { - node = node.cloneNode(false); - subIterator = iterator.getSubtreeIterator(); - node.appendChild(extractSubtree(subIterator)); - subIterator.detach(); - } else { - iterator.remove(); - } - if (node.nodeType == 10) { // DocumentType - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - frag.appendChild(node); - } - return frag; - } - - function getNodesInRange(range, nodeTypes, filter) { - var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; - var filterExists = !!filter; - if (filterNodeTypes) { - regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); - } - - var nodes = []; - iterateSubtree(new RangeIterator(range, false), function(node) { - if (filterNodeTypes && !regex.test(node.nodeType)) { - return; - } - if (filterExists && !filter(node)) { - return; - } - // Don't include a boundary container if it is a character data node and the range does not contain any - // of its character data. See issue 190. - var sc = range.startContainer; - if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { - return; - } - - var ec = range.endContainer; - if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { - return; - } - - nodes.push(node); - }); - return nodes; - } - - function inspect(range) { - var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); - return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + - dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) - - function RangeIterator(range, clonePartiallySelectedTextNodes) { - this.range = range; - this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; - - - if (!range.collapsed) { - this.sc = range.startContainer; - this.so = range.startOffset; - this.ec = range.endContainer; - this.eo = range.endOffset; - var root = range.commonAncestorContainer; - - if (this.sc === this.ec && isCharacterDataNode(this.sc)) { - this.isSingleCharacterDataNode = true; - this._first = this._last = this._next = this.sc; - } else { - this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? - this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); - this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? - this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); - } - } - } - - RangeIterator.prototype = { - _current: null, - _next: null, - _first: null, - _last: null, - isSingleCharacterDataNode: false, - - reset: function() { - this._current = null; - this._next = this._first; - }, - - hasNext: function() { - return !!this._next; - }, - - next: function() { - // Move to next node - var current = this._current = this._next; - if (current) { - this._next = (current !== this._last) ? current.nextSibling : null; - - // Check for partially selected text nodes - if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { - if (current === this.ec) { - (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); - } - if (this._current === this.sc) { - (current = current.cloneNode(true)).deleteData(0, this.so); - } - } - } - - return current; - }, - - remove: function() { - var current = this._current, start, end; - - if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { - start = (current === this.sc) ? this.so : 0; - end = (current === this.ec) ? this.eo : current.length; - if (start != end) { - current.deleteData(start, end - start); - } - } else { - if (current.parentNode) { - removeNode(current); - } else { - } - } - }, - - // Checks if the current node is partially selected - isPartiallySelectedSubtree: function() { - var current = this._current; - return isNonTextPartiallySelected(current, this.range); - }, - - getSubtreeIterator: function() { - var subRange; - if (this.isSingleCharacterDataNode) { - subRange = this.range.cloneRange(); - subRange.collapse(false); - } else { - subRange = new Range(getRangeDocument(this.range)); - var current = this._current; - var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); - - if (isOrIsAncestorOf(current, this.sc)) { - startContainer = this.sc; - startOffset = this.so; - } - if (isOrIsAncestorOf(current, this.ec)) { - endContainer = this.ec; - endOffset = this.eo; - } - - updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); - } - return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); - }, - - detach: function() { - this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; - var rootContainerNodeTypes = [2, 9, 11]; - var readonlyNodeTypes = [5, 6, 10, 12]; - var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; - var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; - - function createAncestorFinder(nodeTypes) { - return function(node, selfIsAncestor) { - var t, n = selfIsAncestor ? node : node.parentNode; - while (n) { - t = n.nodeType; - if (arrayContains(nodeTypes, t)) { - return n; - } - n = n.parentNode; - } - return null; - }; - } - - var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); - var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); - var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); - var getElementAncestor = createAncestorFinder( [1] ); - - function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { - if (getDocTypeNotationEntityAncestor(node, allowSelf)) { - throw new DOMException("INVALID_NODE_TYPE_ERR"); - } - } - - function assertValidNodeType(node, invalidTypes) { - if (!arrayContains(invalidTypes, node.nodeType)) { - throw new DOMException("INVALID_NODE_TYPE_ERR"); - } - } - - function assertValidOffset(node, offset) { - if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { - throw new DOMException("INDEX_SIZE_ERR"); - } - } - - function assertSameDocumentOrFragment(node1, node2) { - if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); - } - } - - function assertNodeNotReadOnly(node) { - if (getReadonlyAncestor(node, true)) { - throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); - } - } - - function assertNode(node, codeName) { - if (!node) { - throw new DOMException(codeName); - } - } - - function isValidOffset(node, offset) { - return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); - } - - function isRangeValid(range) { - return (!!range.startContainer && !!range.endContainer && - !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) && - getRootContainer(range.startContainer) == getRootContainer(range.endContainer) && - isValidOffset(range.startContainer, range.startOffset) && - isValidOffset(range.endContainer, range.endOffset)); - } - - function assertRangeValid(range) { - if (!isRangeValid(range)) { - throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); - } - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Test the browser's innerHTML support to decide how to implement createContextualFragment - var styleEl = document.createElement("style"); - var htmlParsingConforms = false; - try { - styleEl.innerHTML = "x"; - htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Pre-Blink Opera incorrectly creates an element node - } catch (e) { - // IE 6 and 7 throw - } - - api.features.htmlParsingConforms = htmlParsingConforms; - - var createContextualFragment = htmlParsingConforms ? - - // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See - // discussion and base code for this implementation at issue 67. - // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface - // Thanks to Aleks Williams. - function(fragmentStr) { - // "Let node the context object's start's node." - var node = this.startContainer; - var doc = getDocument(node); - - // "If the context object's start's node is null, raise an INVALID_STATE_ERR - // exception and abort these steps." - if (!node) { - throw new DOMException("INVALID_STATE_ERR"); - } - - // "Let element be as follows, depending on node's interface:" - // Document, Document Fragment: null - var el = null; - - // "Element: node" - if (node.nodeType == 1) { - el = node; - - // "Text, Comment: node's parentElement" - } else if (isCharacterDataNode(node)) { - el = dom.parentElement(node); - } - - // "If either element is null or element's ownerDocument is an HTML document - // and element's local name is "html" and element's namespace is the HTML - // namespace" - if (el === null || ( - el.nodeName == "HTML" && - dom.isHtmlNamespace(getDocument(el).documentElement) && - dom.isHtmlNamespace(el) - )) { - - // "let element be a new Element with "body" as its local name and the HTML - // namespace as its namespace."" - el = doc.createElement("body"); - } else { - el = el.cloneNode(false); - } - - // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." - // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." - // "In either case, the algorithm must be invoked with fragment as the input - // and element as the context element." - el.innerHTML = fragmentStr; - - // "If this raises an exception, then abort these steps. Otherwise, let new - // children be the nodes returned." - - // "Let fragment be a new DocumentFragment." - // "Append all new children to fragment." - // "Return fragment." - return dom.fragmentFromNodeChildren(el); - } : - - // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that - // previous versions of Rangy used (with the exception of using a body element rather than a div) - function(fragmentStr) { - var doc = getRangeDocument(this); - var el = doc.createElement("body"); - el.innerHTML = fragmentStr; - - return dom.fragmentFromNodeChildren(el); - }; - - function splitRangeBoundaries(range, positionsToPreserve) { - assertRangeValid(range); - - var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; - var startEndSame = (sc === ec); - - if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { - splitDataNode(ec, eo, positionsToPreserve); - } - - if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { - sc = splitDataNode(sc, so, positionsToPreserve); - if (startEndSame) { - eo -= so; - ec = sc; - } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { - eo++; - } - so = 0; - } - range.setStartAndEnd(sc, so, ec, eo); - } - - function rangeToHtml(range) { - assertRangeValid(range); - var container = range.commonAncestorContainer.parentNode.cloneNode(false); - container.appendChild( range.cloneContents() ); - return container.innerHTML; - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", - "commonAncestorContainer"]; - - var s2s = 0, s2e = 1, e2e = 2, e2s = 3; - var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; - - util.extend(api.rangePrototype, { - compareBoundaryPoints: function(how, range) { - assertRangeValid(this); - assertSameDocumentOrFragment(this.startContainer, range.startContainer); - - var nodeA, offsetA, nodeB, offsetB; - var prefixA = (how == e2s || how == s2s) ? "start" : "end"; - var prefixB = (how == s2e || how == s2s) ? "start" : "end"; - nodeA = this[prefixA + "Container"]; - offsetA = this[prefixA + "Offset"]; - nodeB = range[prefixB + "Container"]; - offsetB = range[prefixB + "Offset"]; - return comparePoints(nodeA, offsetA, nodeB, offsetB); - }, - - insertNode: function(node) { - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); - - if (isOrIsAncestorOf(node, this.startContainer)) { - throw new DOMException("HIERARCHY_REQUEST_ERR"); - } - - // No check for whether the container of the start of the Range is of a type that does not allow - // children of the type of node: the browser's DOM implementation should do this for us when we attempt - // to add the node - - var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); - this.setStartBefore(firstNodeInserted); - }, - - cloneContents: function() { - assertRangeValid(this); - - var clone, frag; - if (this.collapsed) { - return getRangeDocument(this).createDocumentFragment(); - } else { - if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { - clone = this.startContainer.cloneNode(true); - clone.data = clone.data.slice(this.startOffset, this.endOffset); - frag = getRangeDocument(this).createDocumentFragment(); - frag.appendChild(clone); - return frag; - } else { - var iterator = new RangeIterator(this, true); - clone = cloneSubtree(iterator); - iterator.detach(); - } - return clone; - } - }, - - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); - - // Check if the contents can be surrounded. Specifically, this means whether the range partially selects - // no non-text nodes. - var iterator = new RangeIterator(this, true); - var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || - (iterator._last && isNonTextPartiallySelected(iterator._last, this))); - iterator.detach(); - return !boundariesInvalid; - }, - - surroundContents: function(node) { - assertValidNodeType(node, surroundNodeTypes); - - if (!this.canSurroundContents()) { - throw new DOMException("INVALID_STATE_ERR"); - } - - // Extract the contents - var content = this.extractContents(); - - // Clear the children of the node - if (node.hasChildNodes()) { - while (node.lastChild) { - node.removeChild(node.lastChild); - } - } - - // Insert the new node and add the extracted contents - insertNodeAtPosition(node, this.startContainer, this.startOffset); - node.appendChild(content); - - this.selectNode(node); - }, - - cloneRange: function() { - assertRangeValid(this); - var range = new Range(getRangeDocument(this)); - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = this[prop]; - } - return range; - }, - - toString: function() { - assertRangeValid(this); - var sc = this.startContainer; - if (sc === this.endContainer && isCharacterDataNode(sc)) { - return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; - } else { - var textParts = [], iterator = new RangeIterator(this, true); - iterateSubtree(iterator, function(node) { - // Accept only text or CDATA nodes, not comments - if (node.nodeType == 3 || node.nodeType == 4) { - textParts.push(node.data); - } - }); - iterator.detach(); - return textParts.join(""); - } - }, - - // The methods below are all non-standard. The following batch were introduced by Mozilla but have since - // been removed from Mozilla. - - compareNode: function(node) { - assertRangeValid(this); - - var parent = node.parentNode; - var nodeIndex = getNodeIndex(node); - - if (!parent) { - throw new DOMException("NOT_FOUND_ERR"); - } - - var startComparison = this.comparePoint(parent, nodeIndex), - endComparison = this.comparePoint(parent, nodeIndex + 1); - - if (startComparison < 0) { // Node starts before - return (endComparison > 0) ? n_b_a : n_b; - } else { - return (endComparison > 0) ? n_a : n_i; - } - }, - - comparePoint: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); - - if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { - return -1; - } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { - return 1; - } - return 0; - }, - - createContextualFragment: createContextualFragment, - - toHtml: function() { - return rangeToHtml(this); - }, - - // touchingIsIntersecting determines whether this method considers a node that borders a range intersects - // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) - intersectsNode: function(node, touchingIsIntersecting) { - assertRangeValid(this); - if (getRootContainer(node) != getRangeRoot(this)) { - return false; - } - - var parent = node.parentNode, offset = getNodeIndex(node); - if (!parent) { - return true; - } - - var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), - endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - - return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; - }, - - isPointInRange: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); - - return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); - }, - - // The methods below are non-standard and invented by me. - - // Sharing a boundary start-to-end or end-to-start does not count as intersection. - intersectsRange: function(range) { - return rangesIntersect(this, range, false); - }, - - // Sharing a boundary start-to-end or end-to-start does count as intersection. - intersectsOrTouchesRange: function(range) { - return rangesIntersect(this, range, true); - }, - - intersection: function(range) { - if (this.intersectsRange(range)) { - var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), - endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); - - var intersectionRange = this.cloneRange(); - if (startComparison == -1) { - intersectionRange.setStart(range.startContainer, range.startOffset); - } - if (endComparison == 1) { - intersectionRange.setEnd(range.endContainer, range.endOffset); - } - return intersectionRange; - } - return null; - }, - - union: function(range) { - if (this.intersectsOrTouchesRange(range)) { - var unionRange = this.cloneRange(); - if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { - unionRange.setStart(range.startContainer, range.startOffset); - } - if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { - unionRange.setEnd(range.endContainer, range.endOffset); - } - return unionRange; - } else { - throw new DOMException("Ranges do not intersect"); - } - }, - - containsNode: function(node, allowPartial) { - if (allowPartial) { - return this.intersectsNode(node, false); - } else { - return this.compareNode(node) == n_i; - } - }, - - containsNodeContents: function(node) { - return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; - }, - - containsRange: function(range) { - var intersection = this.intersection(range); - return intersection !== null && range.equals(intersection); - }, - - containsNodeText: function(node) { - var nodeRange = this.cloneRange(); - nodeRange.selectNode(node); - var textNodes = nodeRange.getNodes([3]); - if (textNodes.length > 0) { - nodeRange.setStart(textNodes[0], 0); - var lastTextNode = textNodes.pop(); - nodeRange.setEnd(lastTextNode, lastTextNode.length); - return this.containsRange(nodeRange); - } else { - return this.containsNodeContents(node); - } - }, - - getNodes: function(nodeTypes, filter) { - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, - - getDocument: function() { - return getRangeDocument(this); - }, - - collapseBefore: function(node) { - this.setEndBefore(node); - this.collapse(false); - }, - - collapseAfter: function(node) { - this.setStartAfter(node); - this.collapse(true); - }, - - getBookmark: function(containerNode) { - var doc = getRangeDocument(this); - var preSelectionRange = api.createRange(doc); - containerNode = containerNode || dom.getBody(doc); - preSelectionRange.selectNodeContents(containerNode); - var range = this.intersection(preSelectionRange); - var start = 0, end = 0; - if (range) { - preSelectionRange.setEnd(range.startContainer, range.startOffset); - start = preSelectionRange.toString().length; - end = start + range.toString().length; - } - - return { - start: start, - end: end, - containerNode: containerNode - }; - }, - - moveToBookmark: function(bookmark) { - var containerNode = bookmark.containerNode; - var charIndex = 0; - this.setStart(containerNode, 0); - this.collapse(true); - var nodeStack = [containerNode], node, foundStart = false, stop = false; - var nextCharIndex, i, childNodes; - - while (!stop && (node = nodeStack.pop())) { - if (node.nodeType == 3) { - nextCharIndex = charIndex + node.length; - if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { - this.setStart(node, bookmark.start - charIndex); - foundStart = true; - } - if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { - this.setEnd(node, bookmark.end - charIndex); - stop = true; - } - charIndex = nextCharIndex; - } else { - childNodes = node.childNodes; - i = childNodes.length; - while (i--) { - nodeStack.push(childNodes[i]); - } - } - } - }, - - getName: function() { - return "DomRange"; - }, - - equals: function(range) { - return Range.rangesEqual(this, range); - }, - - isValid: function() { - return isRangeValid(this); - }, - - inspect: function() { - return inspect(this); - }, - - detach: function() { - // In DOM4, detach() is now a no-op. - } - }); - - function copyComparisonConstantsToObject(obj) { - obj.START_TO_START = s2s; - obj.START_TO_END = s2e; - obj.END_TO_END = e2e; - obj.END_TO_START = e2s; - - obj.NODE_BEFORE = n_b; - obj.NODE_AFTER = n_a; - obj.NODE_BEFORE_AND_AFTER = n_b_a; - obj.NODE_INSIDE = n_i; - } - - function copyComparisonConstants(constructor) { - copyComparisonConstantsToObject(constructor); - copyComparisonConstantsToObject(constructor.prototype); - } - - function createRangeContentRemover(remover, boundaryUpdater) { - return function() { - assertRangeValid(this); - - var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; - - var iterator = new RangeIterator(this, true); - - // Work out where to position the range after content removal - var node, boundary; - if (sc !== root) { - node = getClosestAncestorIn(sc, root, true); - boundary = getBoundaryAfterNode(node); - sc = boundary.node; - so = boundary.offset; - } - - // Check none of the range is read-only - iterateSubtree(iterator, assertNodeNotReadOnly); - - iterator.reset(); - - // Remove the content - var returnValue = remover(iterator); - iterator.detach(); - - // Move to the new position - boundaryUpdater(this, sc, so, sc, so); - - return returnValue; - }; - } - - function createPrototypeRange(constructor, boundaryUpdater) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); - - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); - }; - } - - function setRangeStart(range, node, offset) { - var ec = range.endContainer, eo = range.endOffset; - if (node !== range.startContainer || offset !== range.startOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { - ec = node; - eo = offset; - } - boundaryUpdater(range, node, offset, ec, eo); - } - } - - function setRangeEnd(range, node, offset) { - var sc = range.startContainer, so = range.startOffset; - if (node !== range.endContainer || offset !== range.endOffset) { - // Check the root containers of the range and the new boundary, and also check whether the new boundary - // is after the current end. In either case, collapse the range to the new position - if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { - sc = node; - so = offset; - } - boundaryUpdater(range, sc, so, node, offset); - } - } - - // Set up inheritance - var F = function() {}; - F.prototype = api.rangePrototype; - constructor.prototype = new F(); - - util.extend(constructor.prototype, { - setStart: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeStart(this, node, offset); - }, - - setEnd: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - - setRangeEnd(this, node, offset); - }, - - /** - * Convenience method to set a range's start and end boundaries. Overloaded as follows: - * - Two parameters (node, offset) creates a collapsed range at that position - * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at - * startOffset and ending at endOffset - * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in - * startNode and ending at endOffset in endNode - */ - setStartAndEnd: function() { - var args = arguments; - var sc = args[0], so = args[1], ec = sc, eo = so; - - switch (args.length) { - case 3: - eo = args[2]; - break; - case 4: - ec = args[2]; - eo = args[3]; - break; - } - - assertNoDocTypeNotationEntityAncestor(sc, true); - assertValidOffset(sc, so); - - assertNoDocTypeNotationEntityAncestor(ec, true); - assertValidOffset(ec, eo); - - boundaryUpdater(this, sc, so, ec, eo); - }, - - setBoundary: function(node, offset, isStart) { - this["set" + (isStart ? "Start" : "End")](node, offset); - }, - - setStartBefore: createBeforeAfterNodeSetter(true, true), - setStartAfter: createBeforeAfterNodeSetter(false, true), - setEndBefore: createBeforeAfterNodeSetter(true, false), - setEndAfter: createBeforeAfterNodeSetter(false, false), - - collapse: function(isStart) { - assertRangeValid(this); - if (isStart) { - boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); - } else { - boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); - } - }, - - selectNodeContents: function(node) { - assertNoDocTypeNotationEntityAncestor(node, true); - - boundaryUpdater(this, node, 0, node, getNodeLength(node)); - }, - - selectNode: function(node) { - assertNoDocTypeNotationEntityAncestor(node, false); - assertValidNodeType(node, beforeAfterNodeTypes); - - var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); - boundaryUpdater(this, start.node, start.offset, end.node, end.offset); - }, - - extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), - - deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), - - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); - - // Check if the contents can be surrounded. Specifically, this means whether the range partially selects - // no non-text nodes. - var iterator = new RangeIterator(this, true); - var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || - (iterator._last && isNonTextPartiallySelected(iterator._last, this))); - iterator.detach(); - return !boundariesInvalid; - }, - - splitBoundaries: function() { - splitRangeBoundaries(this); - }, - - splitBoundariesPreservingPositions: function(positionsToPreserve) { - splitRangeBoundaries(this, positionsToPreserve); - }, - - normalizeBoundaries: function() { - assertRangeValid(this); - - var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; - - var mergeForward = function(node) { - var sibling = node.nextSibling; - if (sibling && sibling.nodeType == node.nodeType) { - ec = node; - eo = node.length; - node.appendData(sibling.data); - removeNode(sibling); - } - }; - - var mergeBackward = function(node) { - var sibling = node.previousSibling; - if (sibling && sibling.nodeType == node.nodeType) { - sc = node; - var nodeLength = node.length; - so = sibling.length; - node.insertData(0, sibling.data); - removeNode(sibling); - if (sc == ec) { - eo += so; - ec = sc; - } else if (ec == node.parentNode) { - var nodeIndex = getNodeIndex(node); - if (eo == nodeIndex) { - ec = node; - eo = nodeLength; - } else if (eo > nodeIndex) { - eo--; - } - } - } - }; - - var normalizeStart = true; - var sibling; - - if (isCharacterDataNode(ec)) { - if (eo == ec.length) { - mergeForward(ec); - } else if (eo == 0) { - sibling = ec.previousSibling; - if (sibling && sibling.nodeType == ec.nodeType) { - eo = sibling.length; - if (sc == ec) { - normalizeStart = false; - } - sibling.appendData(ec.data); - removeNode(ec); - ec = sibling; - } - } - } else { - if (eo > 0) { - var endNode = ec.childNodes[eo - 1]; - if (endNode && isCharacterDataNode(endNode)) { - mergeForward(endNode); - } - } - normalizeStart = !this.collapsed; - } - - if (normalizeStart) { - if (isCharacterDataNode(sc)) { - if (so == 0) { - mergeBackward(sc); - } else if (so == sc.length) { - sibling = sc.nextSibling; - if (sibling && sibling.nodeType == sc.nodeType) { - if (ec == sibling) { - ec = sc; - eo += sc.length; - } - sc.appendData(sibling.data); - removeNode(sibling); - } - } - } else { - if (so < sc.childNodes.length) { - var startNode = sc.childNodes[so]; - if (startNode && isCharacterDataNode(startNode)) { - mergeBackward(startNode); - } - } - } - } else { - sc = ec; - so = eo; - } - - boundaryUpdater(this, sc, so, ec, eo); - }, - - collapseToPoint: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - this.setStartAndEnd(node, offset); - }, - - parentElement: function() { - assertRangeValid(this); - var parentNode = this.commonAncestorContainer; - return parentNode ? getElementAncestor(this.commonAncestorContainer, true) : null; - } - }); - - copyComparisonConstants(constructor); - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Updates commonAncestorContainer and collapsed after boundary change - function updateCollapsedAndCommonAncestor(range) { - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); - range.commonAncestorContainer = range.collapsed ? - range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); - } - - function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { - range.startContainer = startContainer; - range.startOffset = startOffset; - range.endContainer = endContainer; - range.endOffset = endOffset; - range.document = dom.getDocument(startContainer); - updateCollapsedAndCommonAncestor(range); - } - - function Range(doc) { - updateBoundaries(this, doc, 0, doc, 0); - } - - createPrototypeRange(Range, updateBoundaries); - - util.extend(Range, { - rangeProperties: rangeProperties, - RangeIterator: RangeIterator, - copyComparisonConstants: copyComparisonConstants, - createPrototypeRange: createPrototypeRange, - inspect: inspect, - toHtml: rangeToHtml, - getRangeDocument: getRangeDocument, - rangesEqual: function(r1, r2) { - return r1.startContainer === r2.startContainer && - r1.startOffset === r2.startOffset && - r1.endContainer === r2.endContainer && - r1.endOffset === r2.endOffset; - } - }); - - api.DomRange = Range; - }); - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Wrappers for the browser's native DOM Range and/or TextRange implementation - api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { - var WrappedRange, WrappedTextRange; - var dom = api.dom; - var util = api.util; - var DomPosition = dom.DomPosition; - var DomRange = api.DomRange; - var getBody = dom.getBody; - var getContentDocument = dom.getContentDocument; - var isCharacterDataNode = dom.isCharacterDataNode; - - - /*----------------------------------------------------------------------------------------------------------------*/ - - if (api.features.implementsDomRange) { - // This is a wrapper around the browser's native DOM Range. It has two aims: - // - Provide workarounds for specific browser bugs - // - provide convenient extensions, which are inherited from Rangy's DomRange - - (function() { - var rangeProto; - var rangeProperties = DomRange.rangeProperties; - - function updateRangeProperties(range) { - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = range.nativeRange[prop]; - } - // Fix for broken collapsed property in IE 9. - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); - } - - function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { - var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); - var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); - var nativeRangeDifferent = !range.equals(range.nativeRange); - - // Always set both boundaries for the benefit of IE9 (see issue 35) - if (startMoved || endMoved || nativeRangeDifferent) { - range.setEnd(endContainer, endOffset); - range.setStart(startContainer, startOffset); - } - } - - var createBeforeAfterNodeSetter; - - WrappedRange = function(range) { - if (!range) { - throw module.createError("WrappedRange: Range must be specified"); - } - this.nativeRange = range; - updateRangeProperties(this); - }; - - DomRange.createPrototypeRange(WrappedRange, updateNativeRange); - - rangeProto = WrappedRange.prototype; - - rangeProto.selectNode = function(node) { - this.nativeRange.selectNode(node); - updateRangeProperties(this); - }; - - rangeProto.cloneContents = function() { - return this.nativeRange.cloneContents(); - }; - - // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, - // insertNode() is never delegated to the native range. - - rangeProto.surroundContents = function(node) { - this.nativeRange.surroundContents(node); - updateRangeProperties(this); - }; - - rangeProto.collapse = function(isStart) { - this.nativeRange.collapse(isStart); - updateRangeProperties(this); - }; - - rangeProto.cloneRange = function() { - return new WrappedRange(this.nativeRange.cloneRange()); - }; - - rangeProto.refresh = function() { - updateRangeProperties(this); - }; - - rangeProto.toString = function() { - return this.nativeRange.toString(); - }; - - // Create test range and node for feature detection - - var testTextNode = document.createTextNode("test"); - getBody(document).appendChild(testTextNode); - var range = document.createRange(); - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and - // correct for it - - range.setStart(testTextNode, 0); - range.setEnd(testTextNode, 0); - - try { - range.setStart(testTextNode, 1); - - rangeProto.setStart = function(node, offset) { - this.nativeRange.setStart(node, offset); - updateRangeProperties(this); - }; - - rangeProto.setEnd = function(node, offset) { - this.nativeRange.setEnd(node, offset); - updateRangeProperties(this); - }; - - createBeforeAfterNodeSetter = function(name) { - return function(node) { - this.nativeRange[name](node); - updateRangeProperties(this); - }; - }; - - } catch(ex) { - - rangeProto.setStart = function(node, offset) { - try { - this.nativeRange.setStart(node, offset); - } catch (ex) { - this.nativeRange.setEnd(node, offset); - this.nativeRange.setStart(node, offset); - } - updateRangeProperties(this); - }; - - rangeProto.setEnd = function(node, offset) { - try { - this.nativeRange.setEnd(node, offset); - } catch (ex) { - this.nativeRange.setStart(node, offset); - this.nativeRange.setEnd(node, offset); - } - updateRangeProperties(this); - }; - - createBeforeAfterNodeSetter = function(name, oppositeName) { - return function(node) { - try { - this.nativeRange[name](node); - } catch (ex) { - this.nativeRange[oppositeName](node); - this.nativeRange[name](node); - } - updateRangeProperties(this); - }; - }; - } - - rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); - rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); - rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); - rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); - - /*--------------------------------------------------------------------------------------------------------*/ - - // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing - // whether the native implementation can be trusted - rangeProto.selectNodeContents = function(node) { - this.setStartAndEnd(node, 0, dom.getNodeLength(node)); - }; - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for - // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 - - range.selectNodeContents(testTextNode); - range.setEnd(testTextNode, 3); - - var range2 = document.createRange(); - range2.selectNodeContents(testTextNode); - range2.setEnd(testTextNode, 4); - range2.setStart(testTextNode, 2); - - if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && - range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { - // This is the wrong way round, so correct for it - - rangeProto.compareBoundaryPoints = function(type, range) { - range = range.nativeRange || range; - if (type == range.START_TO_END) { - type = range.END_TO_START; - } else if (type == range.END_TO_START) { - type = range.START_TO_END; - } - return this.nativeRange.compareBoundaryPoints(type, range); - }; - } else { - rangeProto.compareBoundaryPoints = function(type, range) { - return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); - }; - } - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. - - var el = document.createElement("div"); - el.innerHTML = "123"; - var textNode = el.firstChild; - var body = getBody(document); - body.appendChild(el); - - range.setStart(textNode, 1); - range.setEnd(textNode, 2); - range.deleteContents(); - - if (textNode.data == "13") { - // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and - // extractContents() - rangeProto.deleteContents = function() { - this.nativeRange.deleteContents(); - updateRangeProperties(this); - }; - - rangeProto.extractContents = function() { - var frag = this.nativeRange.extractContents(); - updateRangeProperties(this); - return frag; - }; - } else { - } - - body.removeChild(el); - body = null; - - /*--------------------------------------------------------------------------------------------------------*/ - - // Test for existence of createContextualFragment and delegate to it if it exists - if (util.isHostMethod(range, "createContextualFragment")) { - rangeProto.createContextualFragment = function(fragmentStr) { - return this.nativeRange.createContextualFragment(fragmentStr); - }; - } - - /*--------------------------------------------------------------------------------------------------------*/ - - // Clean up - getBody(document).removeChild(testTextNode); - - rangeProto.getName = function() { - return "WrappedRange"; - }; - - api.WrappedRange = WrappedRange; - - api.createNativeRange = function(doc) { - doc = getContentDocument(doc, module, "createNativeRange"); - return doc.createRange(); - }; - })(); - } - - if (api.features.implementsTextRange) { - /* - This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() - method. For example, in the following (where pipes denote the selection boundaries): - -
  • | a
  • b |
- - var range = document.selection.createRange(); - alert(range.parentElement().id); // Should alert "ul" but alerts "b" - - This method returns the common ancestor node of the following: - - the parentElement() of the textRange - - the parentElement() of the textRange after calling collapse(true) - - the parentElement() of the textRange after calling collapse(false) - */ - var getTextRangeContainerElement = function(textRange) { - var parentEl = textRange.parentElement(); - var range = textRange.duplicate(); - range.collapse(true); - var startEl = range.parentElement(); - range = textRange.duplicate(); - range.collapse(false); - var endEl = range.parentElement(); - var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); - - return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); - }; - - var textRangeIsCollapsed = function(textRange) { - return textRange.compareEndPoints("StartToEnd", textRange) == 0; - }; - - // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started - // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) - // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange - // bugs, handling for inputs and images, plus optimizations. - var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { - var workingRange = textRange.duplicate(); - workingRange.collapse(isStart); - var containerElement = workingRange.parentElement(); - - // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so - // check for that - if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { - containerElement = wholeRangeContainerElement; - } - - - // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and - // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx - if (!containerElement.canHaveHTML) { - var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); - return { - boundaryPosition: pos, - nodeInfo: { - nodeIndex: pos.offset, - containerElement: pos.node - } - }; - } - - var workingNode = dom.getDocument(containerElement).createElement("span"); - - // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 - // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 - if (workingNode.parentNode) { - dom.removeNode(workingNode); - } - - var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; - var previousNode, nextNode, boundaryPosition, boundaryNode; - var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; - var childNodeCount = containerElement.childNodes.length; - var end = childNodeCount; - - // Check end first. Code within the loop assumes that the endth child node of the container is definitely - // after the range boundary. - var nodeIndex = end; - - while (true) { - if (nodeIndex == childNodeCount) { - containerElement.appendChild(workingNode); - } else { - containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); - } - workingRange.moveToElementText(workingNode); - comparison = workingRange.compareEndPoints(workingComparisonType, textRange); - if (comparison == 0 || start == end) { - break; - } else if (comparison == -1) { - if (end == start + 1) { - // We know the endth child node is after the range boundary, so we must be done. - break; - } else { - start = nodeIndex; - } - } else { - end = (end == start + 1) ? start : nodeIndex; - } - nodeIndex = Math.floor((start + end) / 2); - containerElement.removeChild(workingNode); - } - - - // We've now reached or gone past the boundary of the text range we're interested in - // so have identified the node we want - boundaryNode = workingNode.nextSibling; - - if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { - // This is a character data node (text, comment, cdata). The working range is collapsed at the start of - // the node containing the text range's boundary, so we move the end of the working range to the - // boundary point and measure the length of its text to get the boundary's offset within the node. - workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); - - var offset; - - if (/[\r\n]/.test(boundaryNode.data)) { - /* - For the particular case of a boundary within a text node containing rendered line breaks (within a -
 element, for example), we need a slightly complicated approach to get the boundary's offset in
-                        IE. The facts:
-
-                        - Each line break is represented as \r in the text node's data/nodeValue properties
-                        - Each line break is represented as \r\n in the TextRange's 'text' property
-                        - The 'text' property of the TextRange does not contain trailing line breaks
-
-                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
-                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
-                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
-                        to use this to store the characters moved when moving both the start and end of the range to the
-                        start of the document body and subtracting the start offset from the end offset (the
-                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
-                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
-                        the end of the document) has the same problem.
-
-                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
-                        end boundary one character at a time and incrementing a counter with the value returned by the
-                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
-                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
-                        by the location of the range within the document).
-
-                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
-                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
-                        be longer than the text of the TextRange, so the start of the range is moved that length initially
-                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
-                        property. This has good performance in most situations compared to the previous two methods.
-                        */
-                        var tempRange = workingRange.duplicate();
-                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
-
-                        offset = tempRange.moveStart("character", rangeLength);
-                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
-                            offset++;
-                            tempRange.moveStart("character", 1);
-                        }
-                    } else {
-                        offset = workingRange.text.length;
-                    }
-                    boundaryPosition = new DomPosition(boundaryNode, offset);
-                } else {
-
-                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
-                    // a position within that, and likewise for a start boundary preceding a character data node
-                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
-                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
-                    if (nextNode && isCharacterDataNode(nextNode)) {
-                        boundaryPosition = new DomPosition(nextNode, 0);
-                    } else if (previousNode && isCharacterDataNode(previousNode)) {
-                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
-                    } else {
-                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
-                    }
-                }
-
-                // Clean up
-                dom.removeNode(workingNode);
-
-                return {
-                    boundaryPosition: boundaryPosition,
-                    nodeInfo: {
-                        nodeIndex: nodeIndex,
-                        containerElement: containerElement
-                    }
-                };
-            };
-
-            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
-            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
-            // (http://code.google.com/p/ierange/)
-            var createBoundaryTextRange = function(boundaryPosition, isStart) {
-                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
-                var doc = dom.getDocument(boundaryPosition.node);
-                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
-                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
-
-                if (nodeIsDataNode) {
-                    boundaryNode = boundaryPosition.node;
-                    boundaryParent = boundaryNode.parentNode;
-                } else {
-                    childNodes = boundaryPosition.node.childNodes;
-                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
-                    boundaryParent = boundaryPosition.node;
-                }
-
-                // Position the range immediately before the node containing the boundary
-                workingNode = doc.createElement("span");
-
-                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
-                // the element rather than immediately before or after it
-                workingNode.innerHTML = "&#feff;";
-
-                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
-                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
-                if (boundaryNode) {
-                    boundaryParent.insertBefore(workingNode, boundaryNode);
-                } else {
-                    boundaryParent.appendChild(workingNode);
-                }
-
-                workingRange.moveToElementText(workingNode);
-                workingRange.collapse(!isStart);
-
-                // Clean up
-                boundaryParent.removeChild(workingNode);
-
-                // Move the working range to the text offset, if required
-                if (nodeIsDataNode) {
-                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
-                }
-
-                return workingRange;
-            };
-
-            /*------------------------------------------------------------------------------------------------------------*/
-
-            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
-            // prototype
-
-            WrappedTextRange = function(textRange) {
-                this.textRange = textRange;
-                this.refresh();
-            };
-
-            WrappedTextRange.prototype = new DomRange(document);
-
-            WrappedTextRange.prototype.refresh = function() {
-                var start, end, startBoundary;
-
-                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
-                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
-
-                if (textRangeIsCollapsed(this.textRange)) {
-                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
-                        true).boundaryPosition;
-                } else {
-                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
-                    start = startBoundary.boundaryPosition;
-
-                    // An optimization used here is that if the start and end boundaries have the same parent element, the
-                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
-                    // the start boundary
-                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
-                        startBoundary.nodeInfo).boundaryPosition;
-                }
-
-                this.setStart(start.node, start.offset);
-                this.setEnd(end.node, end.offset);
-            };
-
-            WrappedTextRange.prototype.getName = function() {
-                return "WrappedTextRange";
-            };
-
-            DomRange.copyComparisonConstants(WrappedTextRange);
-
-            var rangeToTextRange = function(range) {
-                if (range.collapsed) {
-                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
-                } else {
-                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
-                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
-                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
-                    textRange.setEndPoint("StartToStart", startRange);
-                    textRange.setEndPoint("EndToEnd", endRange);
-                    return textRange;
-                }
-            };
-
-            WrappedTextRange.rangeToTextRange = rangeToTextRange;
-
-            WrappedTextRange.prototype.toTextRange = function() {
-                return rangeToTextRange(this);
-            };
-
-            api.WrappedTextRange = WrappedTextRange;
-
-            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
-            // implementation to use by default.
-            if (!api.features.implementsDomRange || api.config.preferTextRange) {
-                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
-                var globalObj = (function(f) { return f("return this;")(); })(Function);
-                if (typeof globalObj.Range == "undefined") {
-                    globalObj.Range = WrappedTextRange;
-                }
-
-                api.createNativeRange = function(doc) {
-                    doc = getContentDocument(doc, module, "createNativeRange");
-                    return getBody(doc).createTextRange();
-                };
-
-                api.WrappedRange = WrappedTextRange;
-            }
-        }
-
-        api.createRange = function(doc) {
-            doc = getContentDocument(doc, module, "createRange");
-            return new api.WrappedRange(api.createNativeRange(doc));
-        };
-
-        api.createRangyRange = function(doc) {
-            doc = getContentDocument(doc, module, "createRangyRange");
-            return new DomRange(doc);
-        };
-
-        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
-        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
-
-        api.addShimListener(function(win) {
-            var doc = win.document;
-            if (typeof doc.createRange == "undefined") {
-                doc.createRange = function() {
-                    return api.createRange(doc);
-                };
-            }
-            doc = win = null;
-        });
-    });
-
-    /*----------------------------------------------------------------------------------------------------------------*/
-
-    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
-    // in the W3C Selection API spec (https://www.w3.org/TR/selection-api)
-    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
-        api.config.checkSelectionRanges = true;
-
-        var BOOLEAN = "boolean";
-        var NUMBER = "number";
-        var dom = api.dom;
-        var util = api.util;
-        var isHostMethod = util.isHostMethod;
-        var DomRange = api.DomRange;
-        var WrappedRange = api.WrappedRange;
-        var DOMException = api.DOMException;
-        var DomPosition = dom.DomPosition;
-        var getNativeSelection;
-        var selectionIsCollapsed;
-        var features = api.features;
-        var CONTROL = "Control";
-        var getDocument = dom.getDocument;
-        var getBody = dom.getBody;
-        var rangesEqual = DomRange.rangesEqual;
-
-
-        // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
-        // "forward" or "forwards") or a Boolean (true for backwards).
-        function isDirectionBackward(dir) {
-            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
-        }
-
-        function getWindow(win, methodName) {
-            if (!win) {
-                return window;
-            } else if (dom.isWindow(win)) {
-                return win;
-            } else if (win instanceof WrappedSelection) {
-                return win.win;
-            } else {
-                var doc = dom.getContentDocument(win, module, methodName);
-                return dom.getWindow(doc);
-            }
-        }
-
-        function getWinSelection(winParam) {
-            return getWindow(winParam, "getWinSelection").getSelection();
-        }
-
-        function getDocSelection(winParam) {
-            return getWindow(winParam, "getDocSelection").document.selection;
-        }
-
-        function winSelectionIsBackward(sel) {
-            var backward = false;
-            if (sel.anchorNode) {
-                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
-            }
-            return backward;
-        }
-
-        // Test for the Range/TextRange and Selection features required
-        // Test for ability to retrieve selection
-        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
-            implementsDocSelection = util.isHostObject(document, "selection");
-
-        features.implementsWinGetSelection = implementsWinGetSelection;
-        features.implementsDocSelection = implementsDocSelection;
-
-        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
-
-        if (useDocumentSelection) {
-            getNativeSelection = getDocSelection;
-            api.isSelectionValid = function(winParam) {
-                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
-
-                // Check whether the selection TextRange is actually contained within the correct document
-                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
-            };
-        } else if (implementsWinGetSelection) {
-            getNativeSelection = getWinSelection;
-            api.isSelectionValid = function() {
-                return true;
-            };
-        } else {
-            module.fail("Neither document.selection or window.getSelection() detected.");
-            return false;
-        }
-
-        api.getNativeSelection = getNativeSelection;
-
-        var testSelection = getNativeSelection();
-
-        // In Firefox, the selection is null in an iframe with display: none. See issue #138.
-        if (!testSelection) {
-            module.fail("Native selection was null (possibly issue 138?)");
-            return false;
-        }
-
-        var testRange = api.createNativeRange(document);
-        var body = getBody(document);
-
-        // Obtaining a range from a selection
-        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
-            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
-
-        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
-
-        // Test for existence of native selection extend() method
-        var selectionHasExtend = isHostMethod(testSelection, "extend");
-        features.selectionHasExtend = selectionHasExtend;
-
-        // Test for existence of native selection setBaseAndExtent() method
-        var selectionHasSetBaseAndExtent = isHostMethod(testSelection, "setBaseAndExtent");
-        features.selectionHasSetBaseAndExtent = selectionHasSetBaseAndExtent;
-
-        // Test if rangeCount exists
-        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
-        features.selectionHasRangeCount = selectionHasRangeCount;
-
-        var selectionSupportsMultipleRanges = false;
-        var collapsedNonEditableSelectionsSupported = true;
-
-        var addRangeBackwardToNative = selectionHasExtend ?
-            function(nativeSelection, range) {
-                var doc = DomRange.getRangeDocument(range);
-                var endRange = api.createRange(doc);
-                endRange.collapseToPoint(range.endContainer, range.endOffset);
-                nativeSelection.addRange(getNativeRange(endRange));
-                nativeSelection.extend(range.startContainer, range.startOffset);
-            } : null;
-
-        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
-                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
-
-            (function() {
-                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
-                // performed on the current document's selection. See issue 109.
-
-                // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
-                // will result in the selection direction being reversed if the original selection was backwards and the
-                // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
-                var sel = window.getSelection();
-                if (sel) {
-                    // Store the current selection
-                    var originalSelectionRangeCount = sel.rangeCount;
-                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
-                    var originalSelectionRanges = [];
-                    var originalSelectionBackward = winSelectionIsBackward(sel);
-                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
-                        originalSelectionRanges[i] = sel.getRangeAt(i);
-                    }
-
-                    // Create some test elements
-                    var testEl = dom.createTestElement(document, "", false);
-                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
-
-                    // Test whether the native selection will allow a collapsed selection within a non-editable element
-                    var r1 = document.createRange();
-
-                    r1.setStart(textNode, 1);
-                    r1.collapse(true);
-                    sel.removeAllRanges();
-                    sel.addRange(r1);
-                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
-                    sel.removeAllRanges();
-
-                    // Test whether the native selection is capable of supporting multiple ranges.
-                    if (!selectionHasMultipleRanges) {
-                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
-                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
-                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
-                        // sniff. I'm not happy about it. See
-                        // https://code.google.com/p/chromium/issues/detail?id=399791
-                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
-                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
-                            selectionSupportsMultipleRanges = false;
-                        } else {
-                            var r2 = r1.cloneRange();
-                            r1.setStart(textNode, 0);
-                            r2.setEnd(textNode, 3);
-                            r2.setStart(textNode, 2);
-                            sel.addRange(r1);
-                            sel.addRange(r2);
-                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
-                        }
-                    }
-
-                    // Clean up
-                    dom.removeNode(testEl);
-                    sel.removeAllRanges();
-
-                    for (i = 0; i < originalSelectionRangeCount; ++i) {
-                        if (i == 0 && originalSelectionBackward) {
-                            if (addRangeBackwardToNative) {
-                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
-                            } else {
-                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
-                                sel.addRange(originalSelectionRanges[i]);
-                            }
-                        } else {
-                            sel.addRange(originalSelectionRanges[i]);
-                        }
-                    }
-                }
-            })();
-        }
-
-        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
-        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
-
-        // ControlRanges
-        var implementsControlRange = false, testControlRange;
-
-        if (body && isHostMethod(body, "createControlRange")) {
-            testControlRange = body.createControlRange();
-            if (util.areHostProperties(testControlRange, ["item", "add"])) {
-                implementsControlRange = true;
-            }
-        }
-        features.implementsControlRange = implementsControlRange;
-
-        // Selection collapsedness
-        if (selectionHasAnchorAndFocus) {
-            selectionIsCollapsed = function(sel) {
-                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
-            };
-        } else {
-            selectionIsCollapsed = function(sel) {
-                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
-            };
-        }
-
-        function updateAnchorAndFocusFromRange(sel, range, backward) {
-            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
-            sel.anchorNode = range[anchorPrefix + "Container"];
-            sel.anchorOffset = range[anchorPrefix + "Offset"];
-            sel.focusNode = range[focusPrefix + "Container"];
-            sel.focusOffset = range[focusPrefix + "Offset"];
-        }
-
-        function updateAnchorAndFocusFromNativeSelection(sel) {
-            var nativeSel = sel.nativeSelection;
-            sel.anchorNode = nativeSel.anchorNode;
-            sel.anchorOffset = nativeSel.anchorOffset;
-            sel.focusNode = nativeSel.focusNode;
-            sel.focusOffset = nativeSel.focusOffset;
-        }
-
-        function updateEmptySelection(sel) {
-            sel.anchorNode = sel.focusNode = null;
-            sel.anchorOffset = sel.focusOffset = 0;
-            sel.rangeCount = 0;
-            sel.isCollapsed = true;
-            sel._ranges.length = 0;
-            updateType(sel);
-        }
-
-        function updateType(sel) {
-            sel.type = (sel.rangeCount == 0) ? "None" : (selectionIsCollapsed(sel) ? "Caret" : "Range");
-        }
-
-        function getNativeRange(range) {
-            var nativeRange;
-            if (range instanceof DomRange) {
-                nativeRange = api.createNativeRange(range.getDocument());
-                nativeRange.setEnd(range.endContainer, range.endOffset);
-                nativeRange.setStart(range.startContainer, range.startOffset);
-            } else if (range instanceof WrappedRange) {
-                nativeRange = range.nativeRange;
-            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
-                nativeRange = range;
-            }
-            return nativeRange;
-        }
-
-        function rangeContainsSingleElement(rangeNodes) {
-            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
-                return false;
-            }
-            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
-                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        function getSingleElementFromRange(range) {
-            var nodes = range.getNodes();
-            if (!rangeContainsSingleElement(nodes)) {
-                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
-            }
-            return nodes[0];
-        }
-
-        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
-        function isTextRange(range) {
-            return !!range && typeof range.text != "undefined";
-        }
-
-        function updateFromTextRange(sel, range) {
-            // Create a Range from the selected TextRange
-            var wrappedRange = new WrappedRange(range);
-            sel._ranges = [wrappedRange];
-
-            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
-            sel.rangeCount = 1;
-            sel.isCollapsed = wrappedRange.collapsed;
-            updateType(sel);
-        }
-
-        function updateControlSelection(sel) {
-            // Update the wrapped selection based on what's now in the native selection
-            sel._ranges.length = 0;
-            if (sel.docSelection.type == "None") {
-                updateEmptySelection(sel);
-            } else {
-                var controlRange = sel.docSelection.createRange();
-                if (isTextRange(controlRange)) {
-                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
-                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
-                    // ControlRange have been removed from the ControlRange and removed from the document.
-                    updateFromTextRange(sel, controlRange);
-                } else {
-                    sel.rangeCount = controlRange.length;
-                    var range, doc = getDocument(controlRange.item(0));
-                    for (var i = 0; i < sel.rangeCount; ++i) {
-                        range = api.createRange(doc);
-                        range.selectNode(controlRange.item(i));
-                        sel._ranges.push(range);
-                    }
-                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
-                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
-                    updateType(sel);
-                }
-            }
-        }
-
-        function addRangeToControlSelection(sel, range) {
-            var controlRange = sel.docSelection.createRange();
-            var rangeElement = getSingleElementFromRange(range);
-
-            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
-            // contained by the supplied range
-            var doc = getDocument(controlRange.item(0));
-            var newControlRange = getBody(doc).createControlRange();
-            for (var i = 0, len = controlRange.length; i < len; ++i) {
-                newControlRange.add(controlRange.item(i));
-            }
-            try {
-                newControlRange.add(rangeElement);
-            } catch (ex) {
-                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
-            }
-            newControlRange.select();
-
-            // Update the wrapped selection based on what's now in the native selection
-            updateControlSelection(sel);
-        }
-
-        var getSelectionRangeAt;
-
-        if (isHostMethod(testSelection, "getRangeAt")) {
-            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
-            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
-            // lesson to us all, especially me.
-            getSelectionRangeAt = function(sel, index) {
-                try {
-                    return sel.getRangeAt(index);
-                } catch (ex) {
-                    return null;
-                }
-            };
-        } else if (selectionHasAnchorAndFocus) {
-            getSelectionRangeAt = function(sel) {
-                var doc = getDocument(sel.anchorNode);
-                var range = api.createRange(doc);
-                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
-
-                // Handle the case when the selection was selected backwards (from the end to the start in the
-                // document)
-                if (range.collapsed !== this.isCollapsed) {
-                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
-                }
-
-                return range;
-            };
-        }
-
-        function WrappedSelection(selection, docSelection, win) {
-            this.nativeSelection = selection;
-            this.docSelection = docSelection;
-            this._ranges = [];
-            this.win = win;
-            this.refresh();
-        }
-
-        WrappedSelection.prototype = api.selectionPrototype;
-
-        function deleteProperties(sel) {
-            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
-            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
-            sel.detached = true;
-            updateType(sel);
-        }
-
-        var cachedRangySelections = [];
-
-        function actOnCachedSelection(win, action) {
-            var i = cachedRangySelections.length, cached, sel;
-            while (i--) {
-                cached = cachedRangySelections[i];
-                sel = cached.selection;
-                if (action == "deleteAll") {
-                    deleteProperties(sel);
-                } else if (cached.win == win) {
-                    if (action == "delete") {
-                        cachedRangySelections.splice(i, 1);
-                        return true;
-                    } else {
-                        return sel;
-                    }
-                }
-            }
-            if (action == "deleteAll") {
-                cachedRangySelections.length = 0;
-            }
-            return null;
-        }
-
-        var getSelection = function(win) {
-            // Check if the parameter is a Rangy Selection object
-            if (win && win instanceof WrappedSelection) {
-                win.refresh();
-                return win;
-            }
-
-            win = getWindow(win, "getNativeSelection");
-
-            var sel = actOnCachedSelection(win);
-            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
-            if (sel) {
-                sel.nativeSelection = nativeSel;
-                sel.docSelection = docSel;
-                sel.refresh();
-            } else {
-                sel = new WrappedSelection(nativeSel, docSel, win);
-                cachedRangySelections.push( { win: win, selection: sel } );
-            }
-            return sel;
-        };
-
-        api.getSelection = getSelection;
-
-        util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
-
-        var selProto = WrappedSelection.prototype;
-
-        function createControlSelection(sel, ranges) {
-            // Ensure that the selection becomes of type "Control"
-            var doc = getDocument(ranges[0].startContainer);
-            var controlRange = getBody(doc).createControlRange();
-            for (var i = 0, el, len = ranges.length; i < len; ++i) {
-                el = getSingleElementFromRange(ranges[i]);
-                try {
-                    controlRange.add(el);
-                } catch (ex) {
-                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
-                }
-            }
-            controlRange.select();
-
-            // Update the wrapped selection based on what's now in the native selection
-            updateControlSelection(sel);
-        }
-
-        // Selecting a range
-        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
-            selProto.removeAllRanges = function() {
-                this.nativeSelection.removeAllRanges();
-                updateEmptySelection(this);
-            };
-
-            var addRangeBackward = function(sel, range) {
-                addRangeBackwardToNative(sel.nativeSelection, range);
-                sel.refresh();
-            };
-
-            if (selectionHasRangeCount) {
-                selProto.addRange = function(range, direction) {
-                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
-                        addRangeToControlSelection(this, range);
-                    } else {
-                        if (isDirectionBackward(direction) && selectionHasExtend) {
-                            addRangeBackward(this, range);
-                        } else {
-                            var previousRangeCount;
-                            if (selectionSupportsMultipleRanges) {
-                                previousRangeCount = this.rangeCount;
-                            } else {
-                                this.removeAllRanges();
-                                previousRangeCount = 0;
-                            }
-                            // Clone the native range so that changing the selected range does not affect the selection.
-                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
-                            // issue 80.
-                            var clonedNativeRange = getNativeRange(range).cloneRange();
-                            try {
-                                this.nativeSelection.addRange(clonedNativeRange);
-                            } catch (ex) {
-                            }
-
-                            // Check whether adding the range was successful
-                            this.rangeCount = this.nativeSelection.rangeCount;
-
-                            if (this.rangeCount == previousRangeCount + 1) {
-                                // The range was added successfully
-
-                                // Check whether the range that we added to the selection is reflected in the last range extracted from
-                                // the selection
-                                if (api.config.checkSelectionRanges) {
-                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
-                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
-                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
-                                        range = new WrappedRange(nativeRange);
-                                    }
-                                }
-                                this._ranges[this.rangeCount - 1] = range;
-                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
-                                this.isCollapsed = selectionIsCollapsed(this);
-                                updateType(this);
-                            } else {
-                                // The range was not added successfully. The simplest thing is to refresh
-                                this.refresh();
-                            }
-                        }
-                    }
-                };
-            } else {
-                selProto.addRange = function(range, direction) {
-                    if (isDirectionBackward(direction) && selectionHasExtend) {
-                        addRangeBackward(this, range);
-                    } else {
-                        this.nativeSelection.addRange(getNativeRange(range));
-                        this.refresh();
-                    }
-                };
-            }
-
-            selProto.setRanges = function(ranges) {
-                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
-                    createControlSelection(this, ranges);
-                } else {
-                    this.removeAllRanges();
-                    for (var i = 0, len = ranges.length; i < len; ++i) {
-                        this.addRange(ranges[i]);
-                    }
-                }
-            };
-        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
-                   implementsControlRange && useDocumentSelection) {
-
-            selProto.removeAllRanges = function() {
-                // Added try/catch as fix for issue #21
-                try {
-                    this.docSelection.empty();
-
-                    // Check for empty() not working (issue #24)
-                    if (this.docSelection.type != "None") {
-                        // Work around failure to empty a control selection by instead selecting a TextRange and then
-                        // calling empty()
-                        var doc;
-                        if (this.anchorNode) {
-                            doc = getDocument(this.anchorNode);
-                        } else if (this.docSelection.type == CONTROL) {
-                            var controlRange = this.docSelection.createRange();
-                            if (controlRange.length) {
-                                doc = getDocument( controlRange.item(0) );
-                            }
-                        }
-                        if (doc) {
-                            var textRange = getBody(doc).createTextRange();
-                            textRange.select();
-                            this.docSelection.empty();
-                        }
-                    }
-                } catch(ex) {}
-                updateEmptySelection(this);
-            };
-
-            selProto.addRange = function(range) {
-                if (this.docSelection.type == CONTROL) {
-                    addRangeToControlSelection(this, range);
-                } else {
-                    api.WrappedTextRange.rangeToTextRange(range).select();
-                    this._ranges[0] = range;
-                    this.rangeCount = 1;
-                    this.isCollapsed = this._ranges[0].collapsed;
-                    updateAnchorAndFocusFromRange(this, range, false);
-                    updateType(this);
-                }
-            };
-
-            selProto.setRanges = function(ranges) {
-                this.removeAllRanges();
-                var rangeCount = ranges.length;
-                if (rangeCount > 1) {
-                    createControlSelection(this, ranges);
-                } else if (rangeCount) {
-                    this.addRange(ranges[0]);
-                }
-            };
-        } else {
-            module.fail("No means of selecting a Range or TextRange was found");
-            return false;
-        }
-
-        selProto.getRangeAt = function(index) {
-            if (index < 0 || index >= this.rangeCount) {
-                throw new DOMException("INDEX_SIZE_ERR");
-            } else {
-                // Clone the range to preserve selection-range independence. See issue 80.
-                return this._ranges[index].cloneRange();
-            }
-        };
-
-        var refreshSelection;
-
-        if (useDocumentSelection) {
-            refreshSelection = function(sel) {
-                var range;
-                if (api.isSelectionValid(sel.win)) {
-                    range = sel.docSelection.createRange();
-                } else {
-                    range = getBody(sel.win.document).createTextRange();
-                    range.collapse(true);
-                }
-
-                if (sel.docSelection.type == CONTROL) {
-                    updateControlSelection(sel);
-                } else if (isTextRange(range)) {
-                    updateFromTextRange(sel, range);
-                } else {
-                    updateEmptySelection(sel);
-                }
-            };
-        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
-            refreshSelection = function(sel) {
-                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
-                    updateControlSelection(sel);
-                } else {
-                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
-                    if (sel.rangeCount) {
-                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
-                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
-                        }
-                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
-                        sel.isCollapsed = selectionIsCollapsed(sel);
-                        updateType(sel);
-                    } else {
-                        updateEmptySelection(sel);
-                    }
-                }
-            };
-        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
-            refreshSelection = function(sel) {
-                var range, nativeSel = sel.nativeSelection;
-                if (nativeSel.anchorNode) {
-                    range = getSelectionRangeAt(nativeSel, 0);
-                    sel._ranges = [range];
-                    sel.rangeCount = 1;
-                    updateAnchorAndFocusFromNativeSelection(sel);
-                    sel.isCollapsed = selectionIsCollapsed(sel);
-                    updateType(sel);
-                } else {
-                    updateEmptySelection(sel);
-                }
-            };
-        } else {
-            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
-            return false;
-        }
-
-        selProto.refresh = function(checkForChanges) {
-            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
-            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
-
-            refreshSelection(this);
-            if (checkForChanges) {
-                // Check the range count first
-                var i = oldRanges.length;
-                if (i != this._ranges.length) {
-                    return true;
-                }
-
-                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
-                // ranges after this
-                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
-                    return true;
-                }
-
-                // Finally, compare each range in turn
-                while (i--) {
-                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-        };
-
-        // Removal of a single range
-        var removeRangeManually = function(sel, range) {
-            var ranges = sel.getAllRanges();
-            sel.removeAllRanges();
-            for (var i = 0, len = ranges.length; i < len; ++i) {
-                if (!rangesEqual(range, ranges[i])) {
-                    sel.addRange(ranges[i]);
-                }
-            }
-            if (!sel.rangeCount) {
-                updateEmptySelection(sel);
-            }
-        };
-
-        if (implementsControlRange && implementsDocSelection) {
-            selProto.removeRange = function(range) {
-                if (this.docSelection.type == CONTROL) {
-                    var controlRange = this.docSelection.createRange();
-                    var rangeElement = getSingleElementFromRange(range);
-
-                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
-                    // element contained by the supplied range
-                    var doc = getDocument(controlRange.item(0));
-                    var newControlRange = getBody(doc).createControlRange();
-                    var el, removed = false;
-                    for (var i = 0, len = controlRange.length; i < len; ++i) {
-                        el = controlRange.item(i);
-                        if (el !== rangeElement || removed) {
-                            newControlRange.add(controlRange.item(i));
-                        } else {
-                            removed = true;
-                        }
-                    }
-                    newControlRange.select();
-
-                    // Update the wrapped selection based on what's now in the native selection
-                    updateControlSelection(this);
-                } else {
-                    removeRangeManually(this, range);
-                }
-            };
-        } else {
-            selProto.removeRange = function(range) {
-                removeRangeManually(this, range);
-            };
-        }
-
-        // Detecting if a selection is backward
-        var selectionIsBackward;
-        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
-            selectionIsBackward = winSelectionIsBackward;
-
-            selProto.isBackward = function() {
-                return selectionIsBackward(this);
-            };
-        } else {
-            selectionIsBackward = selProto.isBackward = function() {
-                return false;
-            };
-        }
-
-        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
-        selProto.isBackwards = selProto.isBackward;
-
-        // Selection stringifier
-        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
-        // The current spec does not yet define this method.
-        selProto.toString = function() {
-            var rangeTexts = [];
-            for (var i = 0, len = this.rangeCount; i < len; ++i) {
-                rangeTexts[i] = "" + this._ranges[i];
-            }
-            return rangeTexts.join("");
-        };
-
-        function assertNodeInSameDocument(sel, node) {
-            if (sel.win.document != getDocument(node)) {
-                throw new DOMException("WRONG_DOCUMENT_ERR");
-            }
-        }
-
-        function assertValidOffset(node, offset) {
-            if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
-                throw new DOMException("INDEX_SIZE_ERR");
-            }
-        }
-
-        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
-        selProto.collapse = function(node, offset) {
-            assertNodeInSameDocument(this, node);
-            var range = api.createRange(node);
-            range.collapseToPoint(node, offset);
-            this.setSingleRange(range);
-            this.isCollapsed = true;
-        };
-
-        selProto.collapseToStart = function() {
-            if (this.rangeCount) {
-                var range = this._ranges[0];
-                this.collapse(range.startContainer, range.startOffset);
-            } else {
-                throw new DOMException("INVALID_STATE_ERR");
-            }
-        };
-
-        selProto.collapseToEnd = function() {
-            if (this.rangeCount) {
-                var range = this._ranges[this.rangeCount - 1];
-                this.collapse(range.endContainer, range.endOffset);
-            } else {
-                throw new DOMException("INVALID_STATE_ERR");
-            }
-        };
-
-        // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
-        // specified so the native implementation is never used by Rangy.
-        selProto.selectAllChildren = function(node) {
-            assertNodeInSameDocument(this, node);
-            var range = api.createRange(node);
-            range.selectNodeContents(node);
-            this.setSingleRange(range);
-        };
-
-        if (selectionHasSetBaseAndExtent) {
-            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
-                this.nativeSelection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
-                this.refresh();
-            };
-        } else if (selectionHasExtend) {
-            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
-                assertValidOffset(anchorNode, anchorOffset);
-                assertValidOffset(focusNode, focusOffset);
-                assertNodeInSameDocument(this, anchorNode);
-                assertNodeInSameDocument(this, focusNode);
-                var range = api.createRange(node);
-                var isBackwards = (dom.comparePoints(anchorNode, anchorOffset, focusNode, focusOffset) == -1);
-                if (isBackwards) {
-                    range.setStartAndEnd(focusNode, focusOffset, anchorNode, anchorOffset);
-                } else {
-                    range.setStartAndEnd(anchorNode, anchorOffset, focusNode, focusOffset);
-                }
-                this.setSingleRange(range, isBackwards);
-            };
-        }
-
-        selProto.deleteFromDocument = function() {
-            // Sepcial behaviour required for IE's control selections
-            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
-                var controlRange = this.docSelection.createRange();
-                var element;
-                while (controlRange.length) {
-                    element = controlRange.item(0);
-                    controlRange.remove(element);
-                    dom.removeNode(element);
-                }
-                this.refresh();
-            } else if (this.rangeCount) {
-                var ranges = this.getAllRanges();
-                if (ranges.length) {
-                    this.removeAllRanges();
-                    for (var i = 0, len = ranges.length; i < len; ++i) {
-                        ranges[i].deleteContents();
-                    }
-                    // The spec says nothing about what the selection should contain after calling deleteContents on each
-                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
-                    this.addRange(ranges[len - 1]);
-                }
-            }
-        };
-
-        // The following are non-standard extensions
-        selProto.eachRange = function(func, returnValue) {
-            for (var i = 0, len = this._ranges.length; i < len; ++i) {
-                if ( func( this.getRangeAt(i) ) ) {
-                    return returnValue;
-                }
-            }
-        };
-
-        selProto.getAllRanges = function() {
-            var ranges = [];
-            this.eachRange(function(range) {
-                ranges.push(range);
-            });
-            return ranges;
-        };
-
-        selProto.setSingleRange = function(range, direction) {
-            this.removeAllRanges();
-            this.addRange(range, direction);
-        };
-
-        selProto.callMethodOnEachRange = function(methodName, params) {
-            var results = [];
-            this.eachRange( function(range) {
-                results.push( range[methodName].apply(range, params || []) );
-            } );
-            return results;
-        };
-
-        function createStartOrEndSetter(isStart) {
-            return function(node, offset) {
-                var range;
-                if (this.rangeCount) {
-                    range = this.getRangeAt(0);
-                    range["set" + (isStart ? "Start" : "End")](node, offset);
-                } else {
-                    range = api.createRange(this.win.document);
-                    range.setStartAndEnd(node, offset);
-                }
-                this.setSingleRange(range, this.isBackward());
-            };
-        }
-
-        selProto.setStart = createStartOrEndSetter(true);
-        selProto.setEnd = createStartOrEndSetter(false);
-
-        // Add select() method to Range prototype. Any existing selection will be removed.
-        api.rangePrototype.select = function(direction) {
-            getSelection( this.getDocument() ).setSingleRange(this, direction);
-        };
-
-        selProto.changeEachRange = function(func) {
-            var ranges = [];
-            var backward = this.isBackward();
-
-            this.eachRange(function(range) {
-                func(range);
-                ranges.push(range);
-            });
-
-            this.removeAllRanges();
-            if (backward && ranges.length == 1) {
-                this.addRange(ranges[0], "backward");
-            } else {
-                this.setRanges(ranges);
-            }
-        };
-
-        selProto.containsNode = function(node, allowPartial) {
-            return this.eachRange( function(range) {
-                return range.containsNode(node, allowPartial);
-            }, true ) || false;
-        };
-
-        selProto.getBookmark = function(containerNode) {
-            return {
-                backward: this.isBackward(),
-                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
-            };
-        };
-
-        selProto.moveToBookmark = function(bookmark) {
-            var selRanges = [];
-            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
-                range = api.createRange(this.win);
-                range.moveToBookmark(rangeBookmark);
-                selRanges.push(range);
-            }
-            if (bookmark.backward) {
-                this.setSingleRange(selRanges[0], "backward");
-            } else {
-                this.setRanges(selRanges);
-            }
-        };
-
-        selProto.saveRanges = function() {
-            return {
-                backward: this.isBackward(),
-                ranges: this.callMethodOnEachRange("cloneRange")
-            };
-        };
-
-        selProto.restoreRanges = function(selRanges) {
-            this.removeAllRanges();
-            for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
-                this.addRange(range, (selRanges.backward && i == 0));
-            }
-        };
-
-        selProto.toHtml = function() {
-            var rangeHtmls = [];
-            this.eachRange(function(range) {
-                rangeHtmls.push( DomRange.toHtml(range) );
-            });
-            return rangeHtmls.join("");
-        };
-
-        if (features.implementsTextRange) {
-            selProto.getNativeTextRange = function() {
-                var sel, textRange;
-                if ( (sel = this.docSelection) ) {
-                    var range = sel.createRange();
-                    if (isTextRange(range)) {
-                        return range;
-                    } else {
-                        throw module.createError("getNativeTextRange: selection is a control selection");
-                    }
-                } else if (this.rangeCount > 0) {
-                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
-                } else {
-                    throw module.createError("getNativeTextRange: selection contains no range");
-                }
-            };
-        }
-
-        function inspect(sel) {
-            var rangeInspects = [];
-            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
-            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
-            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
-
-            if (typeof sel.rangeCount != "undefined") {
-                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
-                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
-                }
-            }
-            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
-                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
-        }
-
-        selProto.getName = function() {
-            return "WrappedSelection";
-        };
-
-        selProto.inspect = function() {
-            return inspect(this);
-        };
-
-        selProto.detach = function() {
-            actOnCachedSelection(this.win, "delete");
-            deleteProperties(this);
-        };
-
-        WrappedSelection.detachAll = function() {
-            actOnCachedSelection(null, "deleteAll");
-        };
-
-        WrappedSelection.inspect = inspect;
-        WrappedSelection.isDirectionBackward = isDirectionBackward;
-
-        api.Selection = WrappedSelection;
-
-        api.selectionPrototype = selProto;
-
-        api.addShimListener(function(win) {
-            if (typeof win.getSelection == "undefined") {
-                win.getSelection = function() {
-                    return getSelection(win);
-                };
-            }
-            win = null;
-        });
-    });
-
-
-    /*----------------------------------------------------------------------------------------------------------------*/
-
-    // Wait for document to load before initializing
-    var docReady = false;
-
-    var loadHandler = function(e) {
-        if (!docReady) {
-            docReady = true;
-            if (!api.initialized && api.config.autoInitialize) {
-                init();
-            }
-        }
-    };
-
-    if (isBrowser) {
-        // Test whether the document has already been loaded and initialize immediately if so
-        if (document.readyState == "complete") {
-            loadHandler();
-        } else {
-            if (isHostMethod(document, "addEventListener")) {
-                document.addEventListener("DOMContentLoaded", loadHandler, false);
-            }
-
-            // Add a fallback in case the DOMContentLoaded event isn't supported
-            addListener(window, "load", loadHandler);
-        }
-    }
-
-    return api;
-}, this);
-/**
- * Selection save and restore module for Rangy.
- * Saves and restores user selections using marker invisible elements in the DOM.
- *
- * Part of Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Depends on Rangy core.
- *
- * Copyright 2022, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.1
- * Build date: 17 August 2022
- */
-(function(factory, root) {
-    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
-    factory(root.rangy);
-})(function(rangy) {
-    rangy.createModule("SaveRestore", ["WrappedSelection"], function(api, module) {
-        var dom = api.dom;
-        var removeNode = dom.removeNode;
-        var isDirectionBackward = api.Selection.isDirectionBackward;
-        var markerTextChar = "\ufeff";
-
-        function gEBI(id, doc) {
-            return (doc || document).getElementById(id);
-        }
-
-        function insertRangeBoundaryMarker(range, atStart) {
-            var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
-            var markerEl;
-            var doc = dom.getDocument(range.startContainer);
-
-            // Clone the Range and collapse to the appropriate boundary point
-            var boundaryRange = range.cloneRange();
-            boundaryRange.collapse(atStart);
-
-            // Create the marker element containing a single invisible character using DOM methods and insert it
-            markerEl = doc.createElement("span");
-            markerEl.id = markerId;
-            markerEl.style.lineHeight = "0";
-            markerEl.style.display = "none";
-            markerEl.className = "rangySelectionBoundary";
-            markerEl.appendChild(doc.createTextNode(markerTextChar));
-
-            boundaryRange.insertNode(markerEl);
-            return markerEl;
-        }
-
-        function setRangeBoundary(doc, range, markerId, atStart) {
-            var markerEl = gEBI(markerId, doc);
-            if (markerEl) {
-                range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
-                removeNode(markerEl);
-            } else {
-                module.warn("Marker element has been removed. Cannot restore selection.");
-            }
-        }
-
-        function compareRanges(r1, r2) {
-            return r2.compareBoundaryPoints(r1.START_TO_START, r1);
-        }
-
-        function saveRange(range, direction) {
-            var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
-            var backward = isDirectionBackward(direction);
-
-            if (range.collapsed) {
-                endEl = insertRangeBoundaryMarker(range, false);
-                return {
-                    document: doc,
-                    markerId: endEl.id,
-                    collapsed: true
-                };
-            } else {
-                endEl = insertRangeBoundaryMarker(range, false);
-                startEl = insertRangeBoundaryMarker(range, true);
-
-                return {
-                    document: doc,
-                    startMarkerId: startEl.id,
-                    endMarkerId: endEl.id,
-                    collapsed: false,
-                    backward: backward,
-                    toString: function() {
-                        return "original text: '" + text + "', new text: '" + range.toString() + "'";
-                    }
-                };
-            }
-        }
-
-        function restoreRange(rangeInfo, normalize) {
-            var doc = rangeInfo.document;
-            if (typeof normalize == "undefined") {
-                normalize = true;
-            }
-            var range = api.createRange(doc);
-            if (rangeInfo.collapsed) {
-                var markerEl = gEBI(rangeInfo.markerId, doc);
-                if (markerEl) {
-                    markerEl.style.display = "inline";
-                    var previousNode = markerEl.previousSibling;
-
-                    // Workaround for issue 17
-                    if (previousNode && previousNode.nodeType == 3) {
-                        removeNode(markerEl);
-                        range.collapseToPoint(previousNode, previousNode.length);
-                    } else {
-                        range.collapseBefore(markerEl);
-                        removeNode(markerEl);
-                    }
-                } else {
-                    module.warn("Marker element has been removed. Cannot restore selection.");
-                }
-            } else {
-                setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
-                setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
-            }
-
-            if (normalize) {
-                range.normalizeBoundaries();
-            }
-
-            return range;
-        }
-
-        function saveRanges(ranges, direction) {
-            var rangeInfos = [], range, doc;
-            var backward = isDirectionBackward(direction);
-
-            // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
-            ranges = ranges.slice(0);
-            ranges.sort(compareRanges);
-
-            for (var i = 0, len = ranges.length; i < len; ++i) {
-                rangeInfos[i] = saveRange(ranges[i], backward);
-            }
-
-            // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
-            // between its markers
-            for (i = len - 1; i >= 0; --i) {
-                range = ranges[i];
-                doc = api.DomRange.getRangeDocument(range);
-                if (range.collapsed) {
-                    range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
-                } else {
-                    range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
-                    range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
-                }
-            }
-
-            return rangeInfos;
-        }
-
-        function saveSelection(win) {
-            if (!api.isSelectionValid(win)) {
-                module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
-                return null;
-            }
-            var sel = api.getSelection(win);
-            var ranges = sel.getAllRanges();
-            var backward = (ranges.length == 1 && sel.isBackward());
-
-            var rangeInfos = saveRanges(ranges, backward);
-
-            // Ensure current selection is unaffected
-            if (backward) {
-                sel.setSingleRange(ranges[0], backward);
-            } else {
-                sel.setRanges(ranges);
-            }
-
-            return {
-                win: win,
-                rangeInfos: rangeInfos,
-                restored: false
-            };
-        }
-
-        function restoreRanges(rangeInfos) {
-            var ranges = [];
-
-            // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
-            // normalization affecting previously restored ranges.
-            var rangeCount = rangeInfos.length;
-
-            for (var i = rangeCount - 1; i >= 0; i--) {
-                ranges[i] = restoreRange(rangeInfos[i], true);
-            }
-
-            return ranges;
-        }
-
-        function restoreSelection(savedSelection, preserveDirection) {
-            if (!savedSelection.restored) {
-                var rangeInfos = savedSelection.rangeInfos;
-                var sel = api.getSelection(savedSelection.win);
-                var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
-
-                if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
-                    sel.removeAllRanges();
-                    sel.addRange(ranges[0], true);
-                } else {
-                    sel.setRanges(ranges);
-                }
-
-                savedSelection.restored = true;
-            }
-        }
-
-        function removeMarkerElement(doc, markerId) {
-            var markerEl = gEBI(markerId, doc);
-            if (markerEl) {
-                removeNode(markerEl);
-            }
-        }
-
-        function removeMarkers(savedSelection) {
-            var rangeInfos = savedSelection.rangeInfos;
-            for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
-                rangeInfo = rangeInfos[i];
-                if (rangeInfo.collapsed) {
-                    removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
-                } else {
-                    removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
-                    removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
-                }
-            }
-        }
-
-        api.util.extend(api, {
-            saveRange: saveRange,
-            restoreRange: restoreRange,
-            saveRanges: saveRanges,
-            restoreRanges: restoreRanges,
-            saveSelection: saveSelection,
-            restoreSelection: restoreSelection,
-            removeMarkerElement: removeMarkerElement,
-            removeMarkers: removeMarkers
-        });
-    });
-
-    return rangy;
-}, this);
-/**
- * Serializer module for Rangy.
- * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
- * cookie or local storage and restore it on the user's next visit to the same page.
- *
- * Part of Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Depends on Rangy core.
- *
- * Copyright 2022, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.1
- * Build date: 17 August 2022
- */
-(function(factory, root) {
-    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
-    factory(root.rangy);
-})(function(rangy) {
-    rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
-        var UNDEF = "undefined";
-        var util = api.util;
-
-        // encodeURIComponent and decodeURIComponent are required for cookie handling
-        if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
-            module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
-        }
-
-        // Checksum for checking whether range can be serialized
-        var crc32 = (function() {
-            function utf8encode(str) {
-                var utf8CharCodes = [];
-
-                for (var i = 0, len = str.length, c; i < len; ++i) {
-                    c = str.charCodeAt(i);
-                    if (c < 128) {
-                        utf8CharCodes.push(c);
-                    } else if (c < 2048) {
-                        utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
-                    } else {
-                        utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
-                    }
-                }
-                return utf8CharCodes;
-            }
-
-            var cachedCrcTable = null;
-
-            function buildCRCTable() {
-                var table = [];
-                for (var i = 0, j, crc; i < 256; ++i) {
-                    crc = i;
-                    j = 8;
-                    while (j--) {
-                        if ((crc & 1) == 1) {
-                            crc = (crc >>> 1) ^ 0xEDB88320;
-                        } else {
-                            crc >>>= 1;
-                        }
-                    }
-                    table[i] = crc >>> 0;
-                }
-                return table;
-            }
-
-            function getCrcTable() {
-                if (!cachedCrcTable) {
-                    cachedCrcTable = buildCRCTable();
-                }
-                return cachedCrcTable;
-            }
-
-            return function(str) {
-                var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
-                for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
-                    y = (crc ^ utf8CharCodes[i]) & 0xFF;
-                    crc = (crc >>> 8) ^ crcTable[y];
-                }
-                return (crc ^ -1) >>> 0;
-            };
-        })();
-
-        var dom = api.dom;
-
-        function escapeTextForHtml(str) {
-            return str.replace(//g, ">");
-        }
-
-        function nodeToInfoString(node, infoParts) {
-            infoParts = infoParts || [];
-            var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
-            var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
-            var start = "", end = "";
-            switch (nodeType) {
-                case 3: // Text node
-                    start = escapeTextForHtml(node.nodeValue);
-                    break;
-                case 8: // Comment
-                    start = "";
-                    break;
-                default:
-                    start = "<" + nodeInfo + ">";
-                    end = "";
-                    break;
-            }
-            if (start) {
-                infoParts.push(start);
-            }
-            for (var i = 0; i < childCount; ++i) {
-                nodeToInfoString(children[i], infoParts);
-            }
-            if (end) {
-                infoParts.push(end);
-            }
-            return infoParts;
-        }
-
-        // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
-        // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
-        // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
-        // innerHTML whenever the user changes an input within the element.
-        function getElementChecksum(el) {
-            var info = nodeToInfoString(el).join("");
-            return crc32(info).toString(16);
-        }
-
-        function serializePosition(node, offset, rootNode) {
-            var pathParts = [], n = node;
-            rootNode = rootNode || dom.getDocument(node).documentElement;
-            while (n && n != rootNode) {
-                pathParts.push(dom.getNodeIndex(n, true));
-                n = n.parentNode;
-            }
-            return pathParts.join("/") + ":" + offset;
-        }
-
-        function deserializePosition(serialized, rootNode, doc) {
-            if (!rootNode) {
-                rootNode = (doc || document).documentElement;
-            }
-            var parts = serialized.split(":");
-            var node = rootNode;
-            var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
-
-            while (i--) {
-                nodeIndex = parseInt(nodeIndices[i], 10);
-                if (nodeIndex < node.childNodes.length) {
-                    node = node.childNodes[nodeIndex];
-                } else {
-                    throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
-                            " has no child with index " + nodeIndex + ", " + i);
-                }
-            }
-
-            return new dom.DomPosition(node, parseInt(parts[1], 10));
-        }
-
-        function serializeRange(range, omitChecksum, rootNode) {
-            rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
-            if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
-                throw module.createError("serializeRange(): range " + range.inspect() +
-                    " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
-            }
-            var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
-                serializePosition(range.endContainer, range.endOffset, rootNode);
-            if (!omitChecksum) {
-                serialized += "{" + getElementChecksum(rootNode) + "}";
-            }
-            return serialized;
-        }
-
-        var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
-
-        function deserializeRange(serialized, rootNode, doc) {
-            if (rootNode) {
-                doc = doc || dom.getDocument(rootNode);
-            } else {
-                doc = doc || document;
-                rootNode = doc.documentElement;
-            }
-            var result = deserializeRegex.exec(serialized);
-            var checksum = result[4];
-            if (checksum) {
-                var rootNodeChecksum = getElementChecksum(rootNode);
-                if (checksum !== rootNodeChecksum) {
-                    throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
-                        ") and target root node (" + rootNodeChecksum + ") do not match");
-                }
-            }
-            var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
-            var range = api.createRange(doc);
-            range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
-            return range;
-        }
-
-        function canDeserializeRange(serialized, rootNode, doc) {
-            if (!rootNode) {
-                rootNode = (doc || document).documentElement;
-            }
-            var result = deserializeRegex.exec(serialized);
-            var checksum = result[3];
-            return !checksum || checksum === getElementChecksum(rootNode);
-        }
-
-        function serializeSelection(selection, omitChecksum, rootNode) {
-            selection = api.getSelection(selection);
-            var ranges = selection.getAllRanges(), serializedRanges = [];
-            for (var i = 0, len = ranges.length; i < len; ++i) {
-                serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
-            }
-            return serializedRanges.join("|");
-        }
-
-        function deserializeSelection(serialized, rootNode, win) {
-            if (rootNode) {
-                win = win || dom.getWindow(rootNode);
-            } else {
-                win = win || window;
-                rootNode = win.document.documentElement;
-            }
-            var serializedRanges = serialized.split("|");
-            var sel = api.getSelection(win);
-            var ranges = [];
-
-            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
-                ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
-            }
-            sel.setRanges(ranges);
-
-            return sel;
-        }
-
-        function canDeserializeSelection(serialized, rootNode, win) {
-            var doc;
-            if (rootNode) {
-                doc = win ? win.document : dom.getDocument(rootNode);
-            } else {
-                win = win || window;
-                rootNode = win.document.documentElement;
-            }
-            var serializedRanges = serialized.split("|");
-
-            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
-                if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        var cookieName = "rangySerializedSelection";
-
-        function getSerializedSelectionFromCookie(cookie) {
-            var parts = cookie.split(/[;,]/);
-            for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
-                nameVal = parts[i].split("=");
-                if (nameVal[0].replace(/^\s+/, "") == cookieName) {
-                    val = nameVal[1];
-                    if (val) {
-                        return decodeURIComponent(val.replace(/\s+$/, ""));
-                    }
-                }
-            }
-            return null;
-        }
-
-        function restoreSelectionFromCookie(win) {
-            win = win || window;
-            var serialized = getSerializedSelectionFromCookie(win.document.cookie);
-            if (serialized) {
-                deserializeSelection(serialized, win.doc);
-            }
-        }
-
-        function saveSelectionCookie(win, props) {
-            win = win || window;
-            props = (typeof props == "object") ? props : {};
-            var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
-            var path = props.path ? ";path=" + props.path : "";
-            var domain = props.domain ? ";domain=" + props.domain : "";
-            var secure = props.secure ? ";secure" : "";
-            var serialized = serializeSelection(api.getSelection(win));
-            win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
-        }
-
-        util.extend(api, {
-            serializePosition: serializePosition,
-            deserializePosition: deserializePosition,
-            serializeRange: serializeRange,
-            deserializeRange: deserializeRange,
-            canDeserializeRange: canDeserializeRange,
-            serializeSelection: serializeSelection,
-            deserializeSelection: deserializeSelection,
-            canDeserializeSelection: canDeserializeSelection,
-            restoreSelectionFromCookie: restoreSelectionFromCookie,
-            saveSelectionCookie: saveSelectionCookie,
-            getElementChecksum: getElementChecksum,
-            nodeToInfoString: nodeToInfoString
-        });
-
-        util.crc32 = crc32;
-    });
-
-    return rangy;
-}, this);
-/**
- * Class Applier module for Rangy.
- * Adds, removes and toggles classes on Ranges and Selections
- *
- * Part of Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Depends on Rangy core.
- *
- * Copyright 2022, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.1
- * Build date: 17 August 2022
- */
-(function(factory, root) {
-    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
-    factory(root.rangy);
-})(function(rangy) {
-    rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
-        var dom = api.dom;
-        var DomPosition = dom.DomPosition;
-        var contains = dom.arrayContains;
-        var util = api.util;
-        var forEach = util.forEach;
-
-
-        var defaultTagName = "span";
-        var createElementNSSupported = util.isHostMethod(document, "createElementNS");
-
-        function each(obj, func) {
-            for (var i in obj) {
-                if (obj.hasOwnProperty(i)) {
-                    if (func(i, obj[i]) === false) {
-                        return false;
-                    }
-                }
-            }
-            return true;
-        }
-
-        function trim(str) {
-            return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
-        }
-
-        function classNameContainsClass(fullClassName, className) {
-            return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
-        }
-
-        // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
-        function hasClass(el, className) {
-            if (typeof el.classList == "object") {
-                return el.classList.contains(className);
-            } else {
-                var classNameSupported = (typeof el.className == "string");
-                var elClass = classNameSupported ? el.className : el.getAttribute("class");
-                return classNameContainsClass(elClass, className);
-            }
-        }
-
-        function addClass(el, className) {
-            if (typeof el.classList == "object") {
-                el.classList.add(className);
-            } else {
-                var classNameSupported = (typeof el.className == "string");
-                var elClass = classNameSupported ? el.className : el.getAttribute("class");
-                if (elClass) {
-                    if (!classNameContainsClass(elClass, className)) {
-                        elClass += " " + className;
-                    }
-                } else {
-                    elClass = className;
-                }
-                if (classNameSupported) {
-                    el.className = elClass;
-                } else {
-                    el.setAttribute("class", elClass);
-                }
-            }
-        }
-
-        var removeClass = (function() {
-            function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
-                return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
-            }
-
-            return function(el, className) {
-                if (typeof el.classList == "object") {
-                    el.classList.remove(className);
-                } else {
-                    var classNameSupported = (typeof el.className == "string");
-                    var elClass = classNameSupported ? el.className : el.getAttribute("class");
-                    elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
-                    if (classNameSupported) {
-                        el.className = elClass;
-                    } else {
-                        el.setAttribute("class", elClass);
-                    }
-                }
-            };
-        })();
-
-        function getClass(el) {
-            var classNameSupported = (typeof el.className == "string");
-            return classNameSupported ? el.className : el.getAttribute("class");
-        }
-
-        function sortClassName(className) {
-            return className && className.split(/\s+/).sort().join(" ");
-        }
-
-        function getSortedClassName(el) {
-            return sortClassName( getClass(el) );
-        }
-
-        function haveSameClasses(el1, el2) {
-            return getSortedClassName(el1) == getSortedClassName(el2);
-        }
-
-        function hasAllClasses(el, className) {
-            var classes = className.split(/\s+/);
-            for (var i = 0, len = classes.length; i < len; ++i) {
-                if (!hasClass(el, trim(classes[i]))) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        function canTextBeStyled(textNode) {
-            var parent = textNode.parentNode;
-            return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
-        }
-
-        function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
-            var posNode = position.node, posOffset = position.offset;
-            var newNode = posNode, newOffset = posOffset;
-
-            if (posNode == newParent && posOffset > newIndex) {
-                ++newOffset;
-            }
-
-            if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
-                newNode = newParent;
-                newOffset += newIndex - oldIndex;
-            }
-
-            if (posNode == oldParent && posOffset > oldIndex + 1) {
-                --newOffset;
-            }
-
-            position.node = newNode;
-            position.offset = newOffset;
-        }
-
-        function movePositionWhenRemovingNode(position, parentNode, index) {
-            if (position.node == parentNode && position.offset > index) {
-                --position.offset;
-            }
-        }
-
-        function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
-            // For convenience, allow newIndex to be -1 to mean "insert at the end".
-            if (newIndex == -1) {
-                newIndex = newParent.childNodes.length;
-            }
-
-            var oldParent = node.parentNode;
-            var oldIndex = dom.getNodeIndex(node);
-
-            forEach(positionsToPreserve, function(position) {
-                movePosition(position, oldParent, oldIndex, newParent, newIndex);
-            });
-
-            // Now actually move the node.
-            if (newParent.childNodes.length == newIndex) {
-                newParent.appendChild(node);
-            } else {
-                newParent.insertBefore(node, newParent.childNodes[newIndex]);
-            }
-        }
-
-        function removePreservingPositions(node, positionsToPreserve) {
-
-            var oldParent = node.parentNode;
-            var oldIndex = dom.getNodeIndex(node);
-
-            forEach(positionsToPreserve, function(position) {
-                movePositionWhenRemovingNode(position, oldParent, oldIndex);
-            });
-
-            dom.removeNode(node);
-        }
-
-        function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
-            var child, children = [];
-            while ( (child = node.firstChild) ) {
-                movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
-                children.push(child);
-            }
-            if (removeNode) {
-                removePreservingPositions(node, positionsToPreserve);
-            }
-            return children;
-        }
-
-        function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
-            return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
-        }
-
-        function rangeSelectsAnyText(range, textNode) {
-            var textNodeRange = range.cloneRange();
-            textNodeRange.selectNodeContents(textNode);
-
-            var intersectionRange = textNodeRange.intersection(range);
-            var text = intersectionRange ? intersectionRange.toString() : "";
-
-            return text != "";
-        }
-
-        function getEffectiveTextNodes(range) {
-            var nodes = range.getNodes([3]);
-
-            // Optimization as per issue 145
-
-            // Remove non-intersecting text nodes from the start of the range
-            var start = 0, node;
-            while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
-                ++start;
-            }
-
-            // Remove non-intersecting text nodes from the start of the range
-            var end = nodes.length - 1;
-            while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
-                --end;
-            }
-
-            return nodes.slice(start, end + 1);
-        }
-
-        function elementsHaveSameNonClassAttributes(el1, el2) {
-            if (el1.attributes.length != el2.attributes.length) return false;
-            for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
-                attr1 = el1.attributes[i];
-                name = attr1.name;
-                if (name != "class") {
-                    attr2 = el2.attributes.getNamedItem(name);
-                    if ( (attr1 === null) != (attr2 === null) ) return false;
-                    if (attr1.specified != attr2.specified) return false;
-                    if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
-                }
-            }
-            return true;
-        }
-
-        function elementHasNonClassAttributes(el, exceptions) {
-            for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
-                attrName = el.attributes[i].name;
-                if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        var getComputedStyleProperty = dom.getComputedStyleProperty;
-        var isEditableElement = (function() {
-            var testEl = document.createElement("div");
-            return typeof testEl.isContentEditable == "boolean" ?
-                function (node) {
-                    return node && node.nodeType == 1 && node.isContentEditable;
-                } :
-                function (node) {
-                    if (!node || node.nodeType != 1 || node.contentEditable == "false") {
-                        return false;
-                    }
-                    return node.contentEditable == "true" || isEditableElement(node.parentNode);
-                };
-        })();
-
-        function isEditingHost(node) {
-            var parent;
-            return node && node.nodeType == 1 &&
-                (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
-                (isEditableElement(node) && !isEditableElement(node.parentNode)));
-        }
-
-        function isEditable(node) {
-            return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
-        }
-
-        var inlineDisplayRegex = /^inline(-block|-table)?$/i;
-
-        function isNonInlineElement(node) {
-            return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
-        }
-
-        // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
-        var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
-
-        function isUnrenderedWhiteSpaceNode(node) {
-            if (node.data.length == 0) {
-                return true;
-            }
-            if (htmlNonWhiteSpaceRegex.test(node.data)) {
-                return false;
-            }
-            var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
-            switch (cssWhiteSpace) {
-                case "pre":
-                case "pre-wrap":
-                case "-moz-pre-wrap":
-                    return false;
-                case "pre-line":
-                    if (/[\r\n]/.test(node.data)) {
-                        return false;
-                    }
-            }
-
-            // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
-            // non-inline element, it will not be rendered. This seems to be a good enough definition.
-            return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
-        }
-
-        function getRangeBoundaries(ranges) {
-            var positions = [], i, range;
-            for (i = 0; range = ranges[i++]; ) {
-                positions.push(
-                    new DomPosition(range.startContainer, range.startOffset),
-                    new DomPosition(range.endContainer, range.endOffset)
-                );
-            }
-            return positions;
-        }
-
-        function updateRangesFromBoundaries(ranges, positions) {
-            for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
-                range = ranges[i];
-                start = positions[i * 2];
-                end = positions[i * 2 + 1];
-                range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
-            }
-        }
-
-        function isSplitPoint(node, offset) {
-            if (dom.isCharacterDataNode(node)) {
-                if (offset == 0) {
-                    return !!node.previousSibling;
-                } else if (offset == node.length) {
-                    return !!node.nextSibling;
-                } else {
-                    return true;
-                }
-            }
-
-            return offset > 0 && offset < node.childNodes.length;
-        }
-
-        function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
-            var newNode, parentNode;
-            var splitAtStart = (descendantOffset == 0);
-
-            if (dom.isAncestorOf(descendantNode, node)) {
-                return node;
-            }
-
-            if (dom.isCharacterDataNode(descendantNode)) {
-                var descendantIndex = dom.getNodeIndex(descendantNode);
-                if (descendantOffset == 0) {
-                    descendantOffset = descendantIndex;
-                } else if (descendantOffset == descendantNode.length) {
-                    descendantOffset = descendantIndex + 1;
-                } else {
-                    throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
-                        descendantOffset + " in " + descendantNode.data);
-                }
-                descendantNode = descendantNode.parentNode;
-            }
-
-            if (isSplitPoint(descendantNode, descendantOffset)) {
-                // descendantNode is now guaranteed not to be a text or other character node
-                newNode = descendantNode.cloneNode(false);
-                parentNode = descendantNode.parentNode;
-                if (newNode.id) {
-                    newNode.removeAttribute("id");
-                }
-                var child, newChildIndex = 0;
-
-                while ( (child = descendantNode.childNodes[descendantOffset]) ) {
-                    movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
-                }
-                movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
-                return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
-            } else if (node != descendantNode) {
-                newNode = descendantNode.parentNode;
-
-                // Work out a new split point in the parent node
-                var newNodeIndex = dom.getNodeIndex(descendantNode);
-
-                if (!splitAtStart) {
-                    newNodeIndex++;
-                }
-                return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
-            }
-            return node;
-        }
-
-        function areElementsMergeable(el1, el2) {
-            return el1.namespaceURI == el2.namespaceURI &&
-                el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
-                haveSameClasses(el1, el2) &&
-                elementsHaveSameNonClassAttributes(el1, el2) &&
-                getComputedStyleProperty(el1, "display") == "inline" &&
-                getComputedStyleProperty(el2, "display") == "inline";
-        }
-
-        function createAdjacentMergeableTextNodeGetter(forward) {
-            var siblingPropName = forward ? "nextSibling" : "previousSibling";
-
-            return function(textNode, checkParentElement) {
-                var el = textNode.parentNode;
-                var adjacentNode = textNode[siblingPropName];
-                if (adjacentNode) {
-                    // Can merge if the node's previous/next sibling is a text node
-                    if (adjacentNode && adjacentNode.nodeType == 3) {
-                        return adjacentNode;
-                    }
-                } else if (checkParentElement) {
-                    // Compare text node parent element with its sibling
-                    adjacentNode = el[siblingPropName];
-                    if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
-                        var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
-                        if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
-                            return adjacentNodeChild;
-                        }
-                    }
-                }
-                return null;
-            };
-        }
-
-        var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
-            getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
-
-
-        function Merge(firstNode) {
-            this.isElementMerge = (firstNode.nodeType == 1);
-            this.textNodes = [];
-            var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
-            if (firstTextNode) {
-                this.textNodes[0] = firstTextNode;
-            }
-        }
-
-        Merge.prototype = {
-            doMerge: function(positionsToPreserve) {
-                var textNodes = this.textNodes;
-                var firstTextNode = textNodes[0];
-                if (textNodes.length > 1) {
-                    var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
-                    var textParts = [], combinedTextLength = 0, textNode, parent;
-                    forEach(textNodes, function(textNode, i) {
-                        parent = textNode.parentNode;
-                        if (i > 0) {
-                            parent.removeChild(textNode);
-                            if (!parent.hasChildNodes()) {
-                                dom.removeNode(parent);
-                            }
-                            if (positionsToPreserve) {
-                                forEach(positionsToPreserve, function(position) {
-                                    // Handle case where position is inside the text node being merged into a preceding node
-                                    if (position.node == textNode) {
-                                        position.node = firstTextNode;
-                                        position.offset += combinedTextLength;
-                                    }
-                                    // Handle case where both text nodes precede the position within the same parent node
-                                    if (position.node == parent && position.offset > firstTextNodeIndex) {
-                                        --position.offset;
-                                        if (position.offset == firstTextNodeIndex + 1 && i < textNodes.length - 1) {
-                                            position.node = firstTextNode;
-                                            position.offset = combinedTextLength;
-                                        }
-                                    }
-                                });
-                            }
-                        }
-                        textParts[i] = textNode.data;
-                        combinedTextLength += textNode.data.length;
-                    });
-                    firstTextNode.data = textParts.join("");
-                }
-                return firstTextNode.data;
-            },
-
-            getLength: function() {
-                var i = this.textNodes.length, len = 0;
-                while (i--) {
-                    len += this.textNodes[i].length;
-                }
-                return len;
-            },
-
-            toString: function() {
-                var textParts = [];
-                forEach(this.textNodes, function(textNode, i) {
-                    textParts[i] = "'" + textNode.data + "'";
-                });
-                return "[Merge(" + textParts.join(",") + ")]";
-            }
-        };
-
-        var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
-            "removeEmptyElements", "onElementCreate"];
-
-        // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
-        var attrNamesForProperties = {};
-
-        function ClassApplier(className, options, tagNames) {
-            var normalize, i, len, propName, applier = this;
-            applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
-
-            var elementPropertiesFromOptions = null, elementAttributes = {};
-
-            // Initialize from options object
-            if (typeof options == "object" && options !== null) {
-                if (typeof options.elementTagName !== "undefined") {
-                    options.elementTagName = options.elementTagName.toLowerCase();
-                }
-                tagNames = options.tagNames;
-                elementPropertiesFromOptions = options.elementProperties;
-                elementAttributes = options.elementAttributes;
-
-                for (i = 0; propName = optionProperties[i++]; ) {
-                    if (options.hasOwnProperty(propName)) {
-                        applier[propName] = options[propName];
-                    }
-                }
-                normalize = options.normalize;
-            } else {
-                normalize = options;
-            }
-
-            // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
-            applier.normalize = (typeof normalize == "undefined") ? true : normalize;
-
-            // Initialize element properties and attribute exceptions
-            applier.attrExceptions = [];
-            var el = document.createElement(applier.elementTagName);
-            applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
-            each(elementAttributes, function(attrName, attrValue) {
-                applier.attrExceptions.push(attrName);
-                // Ensure each attribute value is a string
-                elementAttributes[attrName] = "" + attrValue;
-            });
-            applier.elementAttributes = elementAttributes;
-
-            applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
-                sortClassName(applier.elementProperties.className + " " + className) : className;
-
-            // Initialize tag names
-            applier.applyToAnyTagName = false;
-            var type = typeof tagNames;
-            if (type == "string") {
-                if (tagNames == "*") {
-                    applier.applyToAnyTagName = true;
-                } else {
-                    applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
-                }
-            } else if (type == "object" && typeof tagNames.length == "number") {
-                applier.tagNames = [];
-                for (i = 0, len = tagNames.length; i < len; ++i) {
-                    if (tagNames[i] == "*") {
-                        applier.applyToAnyTagName = true;
-                    } else {
-                        applier.tagNames.push(tagNames[i].toLowerCase());
-                    }
-                }
-            } else {
-                applier.tagNames = [applier.elementTagName];
-            }
-        }
-
-        ClassApplier.prototype = {
-            elementTagName: defaultTagName,
-            elementProperties: {},
-            elementAttributes: {},
-            ignoreWhiteSpace: true,
-            applyToEditableOnly: false,
-            useExistingElements: true,
-            removeEmptyElements: true,
-            onElementCreate: null,
-
-            copyPropertiesToElement: function(props, el, createCopy) {
-                var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
-
-                for (var p in props) {
-                    if (props.hasOwnProperty(p)) {
-                        propValue = props[p];
-                        elPropValue = el[p];
-
-                        // Special case for class. The copied properties object has the applier's class as well as its own
-                        // to simplify checks when removing styling elements
-                        if (p == "className") {
-                            addClass(el, propValue);
-                            addClass(el, this.className);
-                            el[p] = sortClassName(el[p]);
-                            if (createCopy) {
-                                elProps[p] = propValue;
-                            }
-                        }
-
-                        // Special case for style
-                        else if (p == "style") {
-                            elStyle = elPropValue;
-                            if (createCopy) {
-                                elProps[p] = elPropsStyle = {};
-                            }
-                            for (s in props[p]) {
-                                if (props[p].hasOwnProperty(s)) {
-                                    elStyle[s] = propValue[s];
-                                    if (createCopy) {
-                                        elPropsStyle[s] = elStyle[s];
-                                    }
-                                }
-                            }
-                            this.attrExceptions.push(p);
-                        } else {
-                            el[p] = propValue;
-                            // Copy the property back from the dummy element so that later comparisons to check whether
-                            // elements may be removed are checking against the right value. For example, the href property
-                            // of an element returns a fully qualified URL even if it was previously assigned a relative
-                            // URL.
-                            if (createCopy) {
-                                elProps[p] = el[p];
-
-                                // Not all properties map to identically-named attributes
-                                attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
-                                this.attrExceptions.push(attrName);
-                            }
-                        }
-                    }
-                }
-
-                return createCopy ? elProps : "";
-            },
-
-            copyAttributesToElement: function(attrs, el) {
-                for (var attrName in attrs) {
-                    if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
-                        el.setAttribute(attrName, attrs[attrName]);
-                    }
-                }
-            },
-
-            appliesToElement: function(el) {
-                return contains(this.tagNames, el.tagName.toLowerCase());
-            },
-
-            getEmptyElements: function(range) {
-                var applier = this;
-                return range.getNodes([1], function(el) {
-                    return applier.appliesToElement(el) && !el.hasChildNodes();
-                });
-            },
-
-            hasClass: function(node) {
-                return node.nodeType == 1 &&
-                    (this.applyToAnyTagName || this.appliesToElement(node)) &&
-                    hasClass(node, this.className);
-            },
-
-            getSelfOrAncestorWithClass: function(node) {
-                while (node) {
-                    if (this.hasClass(node)) {
-                        return node;
-                    }
-                    node = node.parentNode;
-                }
-                return null;
-            },
-
-            isModifiable: function(node) {
-                return !this.applyToEditableOnly || isEditable(node);
-            },
-
-            // White space adjacent to an unwrappable node can be ignored for wrapping
-            isIgnorableWhiteSpaceNode: function(node) {
-                return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
-            },
-
-            // Normalizes nodes after applying a class to a Range.
-            postApply: function(textNodes, range, positionsToPreserve, isUndo) {
-                var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
-                var merges = [], currentMerge;
-                var rangeStartNode = firstNode, rangeEndNode = lastNode;
-                var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
-                var precedingTextNode;
-
-                // Check for every required merge and create a Merge object for each
-                forEach(textNodes, function(textNode) {
-                    precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
-                    if (precedingTextNode) {
-                        if (!currentMerge) {
-                            currentMerge = new Merge(precedingTextNode);
-                            merges.push(currentMerge);
-                        }
-                        currentMerge.textNodes.push(textNode);
-                        if (textNode === firstNode) {
-                            rangeStartNode = currentMerge.textNodes[0];
-                            rangeStartOffset = rangeStartNode.length;
-                        }
-                        if (textNode === lastNode) {
-                            rangeEndNode = currentMerge.textNodes[0];
-                            rangeEndOffset = currentMerge.getLength();
-                        }
-                    } else {
-                        currentMerge = null;
-                    }
-                });
-
-                // Test whether the first node after the range needs merging
-                var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
-
-                if (nextTextNode) {
-                    if (!currentMerge) {
-                        currentMerge = new Merge(lastNode);
-                        merges.push(currentMerge);
-                    }
-                    currentMerge.textNodes.push(nextTextNode);
-                }
-
-                // Apply the merges
-                if (merges.length) {
-                    for (var i = 0, len = merges.length; i < len; ++i) {
-                        merges[i].doMerge(positionsToPreserve);
-                    }
-
-                    // Set the range boundaries
-                    range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
-                }
-            },
-
-            createContainer: function(parentNode) {
-                var doc = dom.getDocument(parentNode);
-                var namespace;
-                var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
-                    doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
-                    doc.createElement(this.elementTagName);
-
-                this.copyPropertiesToElement(this.elementProperties, el, false);
-                this.copyAttributesToElement(this.elementAttributes, el);
-                addClass(el, this.className);
-                if (this.onElementCreate) {
-                    this.onElementCreate(el, this);
-                }
-                return el;
-            },
-
-            elementHasProperties: function(el, props) {
-                var applier = this;
-                return each(props, function(p, propValue) {
-                    if (p == "className") {
-                        // For checking whether we should reuse an existing element, we just want to check that the element
-                        // has all the classes specified in the className property. When deciding whether the element is
-                        // removable when unapplying a class, there is separate special handling to check whether the
-                        // element has extra classes so the same simple check will do.
-                        return hasAllClasses(el, propValue);
-                    } else if (typeof propValue == "object") {
-                        if (!applier.elementHasProperties(el[p], propValue)) {
-                            return false;
-                        }
-                    } else if (el[p] !== propValue) {
-                        return false;
-                    }
-                });
-            },
-
-            elementHasAttributes: function(el, attrs) {
-                return each(attrs, function(name, value) {
-                    if (el.getAttribute(name) !== value) {
-                        return false;
-                    }
-                });
-            },
-
-            applyToTextNode: function(textNode, positionsToPreserve) {
-
-                // Check whether the text node can be styled. Text within a