diff --git a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
index 4fee53bca80..c0c14aede45 100644
--- a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
+++ b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
@@ -81,7 +81,7 @@ M.atto_equation = M.atto_equation || {
M.editor_atto.focus(elementid);
}
M.atto_equation.selection = M.editor_atto.get_selection();
- if (M.atto_equation.selection !== false && (!M.atto_equation.selection.collapsed)) {
+ if (M.atto_equation.selection) {
var dialogue;
if (!M.atto_equation.dialogue) {
dialogue = new M.core.dialogue({
diff --git a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
index 6c40ed061fa..0f416d5eab3 100644
--- a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
+++ b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
@@ -1 +1 @@
-YUI.add("moodle-atto_equation-button",function(e,t){M.atto_equation=M.atto_equation||{dialogue:null,selection:null,contextids:{},library:{},lastcursor:0,display_chooser:function(t,n){t.preventDefault(),M.editor_atto.is_active(n)||M.editor_atto.focus(n),M.atto_equation.selection=M.editor_atto.get_selection();if(M.atto_equation.selection!==!1&&!M.atto_equation.selection.collapsed){var r;M.atto_equation.dialogue?r=M.atto_equation.dialogue:r=new M.core.dialogue({visible:!1,modal:!0,close:!0,draggable:!0,width:"800px"}),r.render(),r.set("bodyContent",M.atto_equation.get_form_content(n)),r.set("headerContent",M.util.get_string("pluginname","atto_equation"));var i=new e.TabView({srcNode:"#atto_equation_library"});i.render(),r.show();var s=M.atto_equation.resolve_equation();s&&e.one("#atto_equation_equation").set("text",s),M.atto_equation.update_preview(!1,n),M.atto_equation.dialogue=r}},init:function(e){var t=M.util.image_url("e/math","core");e.texfilteractive&&(this.contextids[e.elementid]=e.contextid,this.library=e.library,M.editor_atto.add_toolbar_button(e.elementid,"equation",t,e.group,this.display_chooser),M.editor_atto.on("atto:selectionchanged",function(e){M.atto_equation.resolve_equation()?M.editor_atto.add_widget_highlight(e.elementid,"equation"):M.editor_atto.remove_widget_highlight(e.elementid,"equation")}))},resolve_equation:function(){var t=M.editor_atto.get_selection_parent_node(),n,r;return t?(n=e.one(t).get("text"),pattern=/\$\$[\S\s]*\$\$/,r=pattern.exec(n),r&&r.length?(r=r.pop(),r=r.substring(2,r.length-2),r):!1):!1},set_equation:function(t,n){var r,i,s,o,u,a;t.preventDefault(),M.atto_equation.dialogue.hide(),M.editor_atto.set_selection(M.atto_equation.selection),r=t.currentTarget.ancestor(".atto_form").one("textarea"),a=r.get("value"),a!==""&&(a="$$ "+a.trim()+" $$",i=e.one(M.editor_atto.get_selection_parent_node()),s=i.get("text"),o=/\$\$[\S\s]*\$\$/,u=o.exec(s),u&&u.length?(u=u.pop(),s=s.replace(u,"$$"+a+"$$"),i.set("text",s)):M.editor_atto.insert_html_at_focus_point(a),M.editor_atto.text_updated(n))},update_preview:function(t,n){var r=e.one("#atto_equation_equation"),i=r.get("value"),s,o,u="",a="\\square ",f=r.get("selectionStart");f||(f=0);while(i.charAt(f)==="\\"&&f>0)f-=1;var l=/[\w\{\}]/;while(l.test(i.charAt(f))&&f "+M.util.get_string("editequation_desc","atto_equation")+" "+M.util.get_string("editequation_desc","atto_equation")+"
'+"
"+'"+"";for(i=1;i<5;i++)n+='
",n+="
'+"
"+'"+"";for(i=1;i<5;i++)n+='
",n+=" 0) {
+ if (images) {
+ images = images.filter('img');
+ }
+
+ if (images && images.size()) {
image = images.item(0);
- for (i in images) {
- if (images[i].test('img')) {
- image = images[i];
- break;
- }
- }
+ this.lastselectedimage = image;
+
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
diff --git a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
index 074c97fd6eb..5aec326d218 100644
--- a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
+++ b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
@@ -1,2 +1,2 @@
-YUI.add("moodle-atto_image-button",function(e,t){var n={TAGS:"img"},r={INPUTALIGNMENT:"atto_image_alignment",INPUTALT:"atto_image_altentry",INPUTHEIGHT:"atto_image_heightentry",INPUTSUMBIT:"atto_image_urlentrysubmit",INPUTURL:"atto_image_urlentry",INPUTWIDTH:"atto_image_widthentry",IMAGEALTWARNING:"atto_image_altwarning",IMAGEBROWSER:"openimagebrowser",IMAGEPRESENTATION:"atto_image_presentation",IMAGEPREVIEW:"atto_image_preview"},i,s;s=function(e,t){this.value=e,this.style=t,this.regex=new RegExp(this._regex_escape(t)+" *: *"+this._regex_escape(e))},s.prototype={value:null,style:null,regex:null,test:function(e){return this.regex.test(e)},_regex_escape:function(e){return e.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g,"\\$1")},apply:function(e){var t=e.getAttribute("style");t!==""&&t.substr(t.length-1,1)!==";"&&(t+=";"),t+=this.style+": "+this.value+";"},to_select_option:function(){var e=M.util.get_string("alignment_"+this.value.replace("-",""),"atto_image");return value=this.style+": "+this.value,'"}},i=[new s("baseline","vertical-align"),new s("sub","vertical-align"),new s("super","vertical-align"),new s("top","vertical-align"),new s("text-top","vertical-align"),new s("middle","vertical-align"),new s("bottom","vertical-align"),new s("text-bottom","vertical-align"),new s("left","float"),new s("right","float")],M.atto_image=M.atto_image||{dialogue:null,selection:null,currentlyselected:{},init:function(e){var t=function(e,t){e.preventDefault(),M.editor_atto.is_active(t)||M.editor_atto.focus(t),M.atto_image.selection=M.editor_atto.get_selection();if(M.atto_image.selection!==!1){var n;M.atto_image.dialogue?n=M.atto_image.dialogue:n=new M.core.dialogue({visible:!1,modal:!0,close:!0,draggable:!0}),n.set("bodyContent",M.atto_image.get_form_content(t)),n.set("headerContent",M.util.get_string("createimage","atto_image")),n.render(),n.centerDialogue(),M.atto_image.dialogue=n,n.show()}},r=M.util.image_url("e/insert_edit_image","core");M.editor_atto.add_toolbar_button(e.elementid,"image",r,e.group,t),M.editor_atto.currentlyselected=M.editor_atto.currentlyselected||{},M.editor_atto.currentlyselected[e.elementid]=null,M.editor_atto.on("atto:selectionchanged",function(e){M.editor_atto.selection_filter_matches(e.elementid,n.TAGS,e.selectedNodes)?(M.editor_atto.add_widget_highlight(e.elementid,"image"),M.editor_atto.currentlyselected[e.elementid]=e.selectedNodes):(M.editor_atto.remove_widget_highlight(e.elementid,"image"),M.editor_atto.currentlyselected[e.elementid]=null)})},open_filepicker:function(e){var t=this.getAttribute("data-editor");e.preventDefault(),M.editor_atto.show_filepicker(t,"image",M.atto_image.filepicker_callback)},filepicker_callback:function(t){if(t.url!==""){var n=e.one("#"+r.INPUTURL);n.set("value",t.url);var i=new Image;i.onload=function(){e.one("#"+r.INPUTWIDTH).set("value",this.width),e.one("#"+r.INPUTHEIGHT).set("value",this.height),e.one("#"+r.IMAGEPREVIEW).set("src",this.src),e.one("#"+r.IMAGEPREVIEW).setStyle("display","inline")},i.src=t.url}},url_changed:function(){var t=e.one("#"+r.INPUTURL);if(t.get("value")!==""){var n=new Image;n.onload=function(){var t;t=e.one("#"+r.INPUTWIDTH),t.get("value")===""&&t.set("value",this.width),t=e.one("#"+r.INPUTHEIGHT),t.get("value")===""&&t.set("value",this.height),t=e.one("#"+r.IMAGEPREVIEW),t.set("src",this.src),t.setStyle("display","inline")},n.src=t.get("value")}},set_image:function(t,n){var i=t.currentTarget.ancestor(".atto_form"),s=i.one("#"+r.INPUTURL).get("value"),o=i.one("#"+r.INPUTALT).get("value"),u=i.one("#"+r.INPUTWIDTH).get("value"),a=i.one("#"+r.INPUTHEIGHT).get("value"),f=i.one("#"+r.INPUTALIGNMENT).get("value"),l=i.one("#"+r.IMAGEPRESENTATION).get("checked"),c;t.preventDefault();if(o===""&&!l){i.one("#"+r.IMAGEALTWARNING).setStyle("display","block"),i.one("#"+r.INPUTALT).setAttribute("aria-invalid",!0),i.one("#"+r.IMAGEPRESENTATION).setAttribute("aria-invalid",!0);return}i.one("#"+r.IMAGEALTWARNING).setStyle("display","none"),i.one("#"+r.INPUTALT).setAttribute("aria-invalid",!1),i.one("#"+r.IMAGEPRESENTATION).setAttribute("aria-invalid",!1),M.atto_image.dialogue.hide(),s!==""&&(M.editor_atto.set_selection(M.atto_image.selection),c='
",M.editor_atto.insert_html_at_focus_point(c),M.editor_atto.text_updated(n))},_get_selected_image_properties:function(e){var t={src:null,alt:null,width:null,height:null,align:null,display:"inline",presentation:!1},n=M.editor_atto.currentlyselected[e],r,s,o,u,a;if(n!==null&&n.size()>0){s=n.item(0);for(r in n)if(n[r].test("img")){s=n[r];break}a=s.getAttribute("style"),o=parseInt(s.getAttribute("width"),10),u=parseInt(s.getAttribute("height"),10),o>0&&(t.width=o),u>0&&(t.height=u);for(r in i)if(i[r].test(a)){t.align=i[r];break}return t.src=s.getAttribute("src"),t.alt=s.getAttribute("alt")||"",t.presentation=s.get("role")==="presentation",t}return!1},get_form_content:function(t){var n={alignment:M.util.get_string("alignment","atto_image"),alt:M.util.get_string("enteralt","atto_image"),browse:M.util.get_string("browserepositories","atto_image"),create:M.util.get_string("createimage","atto_image"),height:M.util.get_string("height","atto_image"),presentation:M.util.get_string("presentation","atto_image"),presentationrequired:M.util.get_string("presentationoraltrequired","atto_image"),preview:M.util.get_string("preview","atto_image"),width:M.util.get_string("width","atto_image"),url:M.util.get_string("enterurl","atto_image")},s,o;s='";var u=e.Node.create(s);return this._apply_image_properties(u,t),u.one("#"+r.INPUTURL).on("blur",M.atto_image.url_changed,this),u.one("#"+r.INPUTSUMBIT).on("click",M.atto_image.set_image,this,t),M.editor_atto.can_show_filepicker(t,"image")&&u.one("#"+r.IMAGEBROWSER).on("click",M.atto_image.open_filepicker),u},_apply_image_properties:function(e,t){var n=this._get_selected_image_properties(t),i=e.one("#"+r.IMAGEPREVIEW);if(n===!1){i.setStyle("display","none");return}n.align&&n.align.apply(i),n.display&&i.setStyle("display",n.display),n.width&&e.one("#"+r.INPUTWIDTH).set("value",n.width),n.height&&e.one("#"+r.INPUTHEIGHT).set("value",n.height),n.alt&&e.one("#"+r.INPUTALT).set("value",n.alt),n.src&&(e.one("#"+r.INPUTURL).set("value",n.src),i.setAttribute("src",n.src)),n.presentation&&e.one("#"+r.IMAGEPRESENTATION).set("checked","checked")}}},"@VERSION@",{requires:["node","escape"]});
+YUI.add("moodle-atto_image-button",function(e,t){var n={TAGS:"img"},r={INPUTALIGNMENT:"atto_image_alignment",INPUTALT:"atto_image_altentry",INPUTHEIGHT:"atto_image_heightentry",INPUTSUMBIT:"atto_image_urlentrysubmit",INPUTURL:"atto_image_urlentry",INPUTWIDTH:"atto_image_widthentry",IMAGEALTWARNING:"atto_image_altwarning",IMAGEBROWSER:"openimagebrowser",IMAGEPRESENTATION:"atto_image_presentation",IMAGEPREVIEW:"atto_image_preview"},i,s;s=function(e,t){this.value=e,this.style=t,this.regex=new RegExp(this._regex_escape(t)+" *: *"+this._regex_escape(e))},s.prototype={value:null,style:null,regex:null,test:function(e){return this.regex.test(e)},_regex_escape:function(e){return e.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g,"\\$1")},apply:function(e){var t=e.getAttribute("style");t!==""&&t.substr(t.length-1,1)!==";"&&(t+=";"),t+=this.style+": "+this.value+";"},to_select_option:function(){var e=M.util.get_string("alignment_"+this.value.replace("-",""),"atto_image");return value=this.style+": "+this.value,'"}},i=[new s("baseline","vertical-align"),new s("sub","vertical-align"),new s("super","vertical-align"),new s("top","vertical-align"),new s("text-top","vertical-align"),new s("middle","vertical-align"),new s("bottom","vertical-align"),new s("text-bottom","vertical-align"),new s("left","float"),new s("right","float")],M.atto_image=M.atto_image||{dialogue:null,selection:null,currentlyselected:{},lastselectedimage:null,init:function(e){var t=function(e,t){e.preventDefault(),M.editor_atto.is_active(t)||M.editor_atto.focus(t),M.atto_image.selection=M.editor_atto.get_selection();if(M.atto_image.selection!==!1){var n;M.atto_image.dialogue?n=M.atto_image.dialogue:n=new M.core.dialogue({visible:!1,modal:!0,close:!0,draggable:!0}),n.set("bodyContent",M.atto_image.get_form_content(t)),n.set("headerContent",M.util.get_string("createimage","atto_image")),n.render(),n.centerDialogue(),M.atto_image.dialogue=n,n.show()}},r=M.util.image_url("e/insert_edit_image","core");M.editor_atto.add_toolbar_button(e.elementid,"image",r,e.group,t),M.editor_atto.currentlyselected=M.editor_atto.currentlyselected||{},M.editor_atto.currentlyselected[e.elementid]=null,M.editor_atto.on("atto:selectionchanged",function(e){M.editor_atto.selection_filter_matches(e.elementid,n.TAGS,e.selectedNodes,!1)?(M.editor_atto.add_widget_highlight(e.elementid,"image"),M.editor_atto.currentlyselected[e.elementid]=e.selectedNodes):(M.editor_atto.remove_widget_highlight(e.elementid,"image"),M.editor_atto.currentlyselected[e.elementid]=null)})},open_filepicker:function(e){var t=this.getAttribute("data-editor");e.preventDefault(),M.editor_atto.show_filepicker(t,"image",M.atto_image.filepicker_callback)},filepicker_callback:function(t){if(t.url!==""){var n=e.one("#"+r.INPUTURL);n.set("value",t.url);var i=new Image;i.onload=function(){e.one("#"+r.INPUTWIDTH).set("value",this.width),e.one("#"+r.INPUTHEIGHT).set("value",this.height),e.one("#"+r.IMAGEPREVIEW).set("src",this.src),e.one("#"+r.IMAGEPREVIEW).setStyle("display","inline")},i.src=t.url}},url_changed:function(){var t=e.one("#"+r.INPUTURL);if(t.get("value")!==""){var n=new Image;n.onload=function(){var t;t=e.one("#"+r.INPUTWIDTH),t.get("value")===""&&t.set("value",this.width),t=e.one("#"+r.INPUTHEIGHT),t.get("value")===""&&t.set("value",this.height),t=e.one("#"+r.IMAGEPREVIEW),t.set("src",this.src),t.setStyle("display","inline")},n.src=t.get("value")}},set_image:function(t,n){var i=t.currentTarget.ancestor(".atto_form"),s=i.one("#"+r.INPUTURL).get("value"),o=i.one("#"+r.INPUTALT).get("value"),u=i.one("#"+r.INPUTWIDTH).get("value"),a=i.one("#"+r.INPUTHEIGHT).get("value"),f=i.one("#"+r.INPUTALIGNMENT).get("value"),l=i.one("#"+r.IMAGEPRESENTATION).get("checked"),c;t.preventDefault();if(o===""&&!l){i.one("#"+r.IMAGEALTWARNING).setStyle("display","block"),i.one("#"+r.INPUTALT).setAttribute("aria-invalid",!0),i.one("#"+r.IMAGEPRESENTATION).setAttribute("aria-invalid",!0);return}i.one("#"+r.IMAGEALTWARNING).setStyle("display","none"),i.one("#"+r.INPUTALT).setAttribute("aria-invalid",!1),i.one("#"+r.IMAGEPRESENTATION).setAttribute("aria-invalid",!1),M.atto_image.dialogue.hide(),M.editor_atto.focus(n),s!==""&&(this.lastselectedimage?M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage)):M.editor_atto.set_selection(M.atto_image.selection),c='
",M.editor_atto.insert_html_at_focus_point(c),M.editor_atto.text_updated(n))},_get_selected_image_properties:function(e){var t={src:null,alt:null,width:null,height:null,align:null,display:"inline",presentation:!1},n=M.editor_atto.currentlyselected[e],r,s,o,u,a;n&&(n=n.filter("img"));if(n&&n.size()){s=n.item(0),this.lastselectedimage=s,a=s.getAttribute("style"),o=parseInt(s.getAttribute("width"),10),u=parseInt(s.getAttribute("height"),10),o>0&&(t.width=o),u>0&&(t.height=u);for(r in i)if(i[r].test(a)){t.align=i[r];break}return t.src=s.getAttribute("src"),t.alt=s.getAttribute("alt")||"",t.presentation=s.get("role")==="presentation",t}return!1},get_form_content:function(t){var n={alignment:M.util.get_string("alignment","atto_image"),alt:M.util.get_string("enteralt","atto_image"),browse:M.util.get_string("browserepositories","atto_image"),create:M.util.get_string("createimage","atto_image"),height:M.util.get_string("height","atto_image"),presentation:M.util.get_string("presentation","atto_image"),presentationrequired:M.util.get_string("presentationoraltrequired","atto_image"),preview:M.util.get_string("preview","atto_image"),width:M.util.get_string("width","atto_image"),url:M.util.get_string("enterurl","atto_image")},s,o;s='";var u=e.Node.create(s);return this._apply_image_properties(u,t),u.one("#"+r.INPUTURL).on("blur",M.atto_image.url_changed,this),u.one("#"+r.INPUTSUMBIT).on("click",M.atto_image.set_image,this,t),M.editor_atto.can_show_filepicker(t,"image")&&u.one("#"+r.IMAGEBROWSER).on("click",M.atto_image.open_filepicker),u},_apply_image_properties:function(e,t){var n=this._get_selected_image_properties(t),i=e.one("#"+r.IMAGEPREVIEW);if(n===!1){i.setStyle("display","none");return}n.align&&n.align.apply(i),n.display&&i.setStyle("display",n.display),n.width&&e.one("#"+r.INPUTWIDTH).set("value",n.width),n.height&&e.one("#"+r.INPUTHEIGHT).set("value",n.height),n.alt&&e.one("#"+r.INPUTALT).set("value",n.alt),n.src&&(e.one("#"+r.INPUTURL).set("value",n.src),i.setAttribute("src",n.src)),n.presentation&&e.one("#"+r.IMAGEPRESENTATION).set("checked","checked")}}},"@VERSION@",{requires:["node","escape"]});
diff --git a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
index 566b652527d..0dd4d4aee30 100644
--- a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
+++ b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
@@ -145,6 +145,7 @@ M.atto_image = M.atto_image || {
dialogue: null,
selection: null,
currentlyselected : {},
+ lastselectedimage : null,
init: function(params) {
var display_chooser = function(e, elementid) {
e.preventDefault();
@@ -183,7 +184,7 @@ M.atto_image = M.atto_image || {
// This includes cursor changes, we check if the button should be active or not, based
// on the text selection.
M.editor_atto.on('atto:selectionchanged', function(e) {
- if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes)) {
+ if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes, false)) {
M.editor_atto.add_widget_highlight(e.elementid, 'image');
M.editor_atto.currentlyselected[e.elementid] = e.selectedNodes;
} else {
@@ -263,8 +264,13 @@ M.atto_image = M.atto_image || {
M.atto_image.dialogue.hide();
+ M.editor_atto.focus(elementid);
if (url !== '') {
- M.editor_atto.set_selection(M.atto_image.selection);
+ if (this.lastselectedimage) {
+ M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage));
+ } else {
+ M.editor_atto.set_selection(M.atto_image.selection);
+ }
imagehtml = '
0) {
+ if (images) {
+ images = images.filter('img');
+ }
+
+ if (images && images.size()) {
image = images.item(0);
- for (i in images) {
- if (images[i].test('img')) {
- image = images[i];
- break;
- }
- }
+ this.lastselectedimage = image;
+
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
diff --git a/lib/editor/atto/plugins/image/yui/src/button/js/button.js b/lib/editor/atto/plugins/image/yui/src/button/js/button.js
index 2b77cc803ce..f0b0e9fb8cd 100644
--- a/lib/editor/atto/plugins/image/yui/src/button/js/button.js
+++ b/lib/editor/atto/plugins/image/yui/src/button/js/button.js
@@ -143,6 +143,7 @@ M.atto_image = M.atto_image || {
dialogue: null,
selection: null,
currentlyselected : {},
+ lastselectedimage : null,
init: function(params) {
var display_chooser = function(e, elementid) {
e.preventDefault();
@@ -181,7 +182,7 @@ M.atto_image = M.atto_image || {
// This includes cursor changes, we check if the button should be active or not, based
// on the text selection.
M.editor_atto.on('atto:selectionchanged', function(e) {
- if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes)) {
+ if (M.editor_atto.selection_filter_matches(e.elementid, SELECTORS.TAGS, e.selectedNodes, false)) {
M.editor_atto.add_widget_highlight(e.elementid, 'image');
M.editor_atto.currentlyselected[e.elementid] = e.selectedNodes;
} else {
@@ -261,8 +262,13 @@ M.atto_image = M.atto_image || {
M.atto_image.dialogue.hide();
+ M.editor_atto.focus(elementid);
if (url !== '') {
- M.editor_atto.set_selection(M.atto_image.selection);
+ if (this.lastselectedimage) {
+ M.editor_atto.set_selection(M.editor_atto.get_selection_from_node(this.lastselectedimage));
+ } else {
+ M.editor_atto.set_selection(M.atto_image.selection);
+ }
imagehtml = '
0) {
+ if (images) {
+ images = images.filter('img');
+ }
+
+ if (images && images.size()) {
image = images.item(0);
- for (i in images) {
- if (images[i].test('img')) {
- image = images[i];
- break;
- }
- }
+ this.lastselectedimage = image;
+
style = image.getAttribute('style');
width = parseInt(image.getAttribute('width'), 10);
height = parseInt(image.getAttribute('height'), 10);
diff --git a/lib/editor/atto/thirdpartylibs.xml b/lib/editor/atto/thirdpartylibs.xml
new file mode 100644
index 00000000000..06b6014f949
--- /dev/null
+++ b/lib/editor/atto/thirdpartylibs.xml
@@ -0,0 +1,10 @@
+
+
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 method 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 && dom.isCharacterDataNode(nextNode)) {
+ boundaryPosition = new DomPosition(nextNode, 0);
+ } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
+ boundaryPosition = new DomPosition(previousNode, previousNode.length);
+ } else {
+ boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+ }
+ }
+
+ // Clean up
+ workingNode.parentNode.removeChild(workingNode);
+
+ return boundaryPosition;
+ }
+
+ // 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/)
+ function createBoundaryTextRange(boundaryPosition, isStart) {
+ var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+ var doc = dom.getDocument(boundaryPosition.node);
+ var workingNode, childNodes, workingRange = doc.body.createTextRange();
+ var nodeIsDataNode = dom.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, which is what we want
+ 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;
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------*/
+
+ if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
+ // 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;
+ var canSetRangeStartAfterEnd;
+
+ function updateRangeProperties(range) {
+ var i = rangeProperties.length, prop;
+ while (i--) {
+ prop = rangeProperties[i];
+ range[prop] = range.nativeRange[prop];
+ }
+ }
+
+ function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
+ var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+ var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
+
+ // Always set both boundaries for the benefit of IE9 (see issue 35)
+ if (startMoved || endMoved) {
+ range.setEnd(endContainer, endOffset);
+ range.setStart(startContainer, startOffset);
+ }
+ }
+
+ function detach(range) {
+ range.nativeRange.detach();
+ range.detached = true;
+ var i = rangeProperties.length, prop;
+ while (i--) {
+ prop = rangeProperties[i];
+ range[prop] = null;
+ }
+ }
+
+ var createBeforeAfterNodeSetter;
+
+ WrappedRange = function(range) {
+ if (!range) {
+ throw new Error("Range must be specified");
+ }
+ this.nativeRange = range;
+ updateRangeProperties(this);
+ };
+
+ DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
+
+ rangeProto = WrappedRange.prototype;
+
+ rangeProto.selectNode = function(node) {
+ this.nativeRange.selectNode(node);
+ updateRangeProperties(this);
+ };
+
+ rangeProto.deleteContents = function() {
+ this.nativeRange.deleteContents();
+ updateRangeProperties(this);
+ };
+
+ rangeProto.extractContents = function() {
+ var frag = this.nativeRange.extractContents();
+ updateRangeProperties(this);
+ return frag;
+ };
+
+ rangeProto.cloneContents = function() {
+ return this.nativeRange.cloneContents();
+ };
+
+ // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
+ // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
+ // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
+ // insertNode, which works but is almost certainly slower than the native implementation.
+/*
+ rangeProto.insertNode = function(node) {
+ this.nativeRange.insertNode(node);
+ updateRangeProperties(this);
+ };
+*/
+
+ 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");
+ dom.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);
+ canSetRangeStartAfterEnd = true;
+
+ 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) {
+
+
+ canSetRangeStartAfterEnd = false;
+
+ 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");
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
+ // the 0th character of the text node
+ range.selectNodeContents(testTextNode);
+ if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
+ range.startOffset == 0 && range.endOffset == testTextNode.length) {
+ rangeProto.selectNodeContents = function(node) {
+ this.nativeRange.selectNodeContents(node);
+ updateRangeProperties(this);
+ };
+ } else {
+ rangeProto.selectNodeContents = function(node) {
+ this.setStart(node, 0);
+ this.setEnd(node, DomRange.getEndOffset(node));
+ };
+ }
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Test for WebKit bug that has the beahviour 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 existence of createContextualFragment and delegate to it if it exists
+ if (api.util.isHostMethod(range, "createContextualFragment")) {
+ rangeProto.createContextualFragment = function(fragmentStr) {
+ return this.nativeRange.createContextualFragment(fragmentStr);
+ };
+ }
+
+ /*--------------------------------------------------------------------------------------------------------*/
+
+ // Clean up
+ dom.getBody(document).removeChild(testTextNode);
+ range.detach();
+ range2.detach();
+ })();
+
+ api.createNativeRange = function(doc) {
+ doc = doc || document;
+ return doc.createRange();
+ };
+ } else if (api.features.implementsTextRange) {
+ // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+ // prototype
+
+ WrappedRange = function(textRange) {
+ this.textRange = textRange;
+ this.refresh();
+ };
+
+ WrappedRange.prototype = new DomRange(document);
+
+ WrappedRange.prototype.refresh = function() {
+ var start, end;
+
+ // 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);
+ } else {
+
+ start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+ end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
+ }
+
+ this.setStart(start.node, start.offset);
+ this.setEnd(end.node, end.offset);
+ };
+
+ DomRange.copyComparisonConstants(WrappedRange);
+
+ // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+ var globalObj = (function() { return this; })();
+ if (typeof globalObj.Range == "undefined") {
+ globalObj.Range = WrappedRange;
+ }
+
+ api.createNativeRange = function(doc) {
+ doc = doc || document;
+ return doc.body.createTextRange();
+ };
+ }
+
+ if (api.features.implementsTextRange) {
+ WrappedRange.rangeToTextRange = function(range) {
+ if (range.collapsed) {
+ var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+
+
+
+ return tr;
+
+ //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 = dom.getDocument(range.startContainer).body.createTextRange();
+ textRange.setEndPoint("StartToStart", startRange);
+ textRange.setEndPoint("EndToEnd", endRange);
+ return textRange;
+ }
+ };
+ }
+
+ WrappedRange.prototype.getName = function() {
+ return "WrappedRange";
+ };
+
+ api.WrappedRange = WrappedRange;
+
+ api.createRange = function(doc) {
+ doc = doc || document;
+ return new WrappedRange(api.createNativeRange(doc));
+ };
+
+ api.createRangyRange = function(doc) {
+ doc = doc || document;
+ return new DomRange(doc);
+ };
+
+ api.createIframeRange = function(iframeEl) {
+ return api.createRange(dom.getIframeDocument(iframeEl));
+ };
+
+ api.createIframeRangyRange = function(iframeEl) {
+ return api.createRangyRange(dom.getIframeDocument(iframeEl));
+ };
+
+ api.addCreateMissingNativeApiListener(function(win) {
+ var doc = win.document;
+ if (typeof doc.createRange == "undefined") {
+ doc.createRange = function() {
+ return api.createRange(this);
+ };
+ }
+ doc = win = null;
+ });
+});rangy.createModule("WrappedSelection", function(api, module) {
+ // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
+ // spec (http://html5.org/specs/dom-range.html)
+
+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+ api.config.checkSelectionRanges = true;
+
+ var BOOLEAN = "boolean",
+ windowPropertyName = "_rangySelection",
+ dom = api.dom,
+ util = api.util,
+ DomRange = api.DomRange,
+ WrappedRange = api.WrappedRange,
+ DOMException = api.DOMException,
+ DomPosition = dom.DomPosition,
+ getSelection,
+ selectionIsCollapsed,
+ CONTROL = "Control";
+
+
+
+ function getWinSelection(winParam) {
+ return (winParam || window).getSelection();
+ }
+
+ function getDocSelection(winParam) {
+ return (winParam || window).document.selection;
+ }
+
+ // Test for the Range/TextRange and Selection features required
+ // Test for ability to retrieve selection
+ var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
+ implementsDocSelection = api.util.isHostObject(document, "selection");
+
+ var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+ if (useDocumentSelection) {
+ getSelection = getDocSelection;
+ api.isSelectionValid = function(winParam) {
+ var doc = (winParam || window).document, nativeSel = doc.selection;
+
+ // Check whether the selection TextRange is actually contained within the correct document
+ return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
+ };
+ } else if (implementsWinGetSelection) {
+ getSelection = getWinSelection;
+ api.isSelectionValid = function() {
+ return true;
+ };
+ } else {
+ module.fail("Neither document.selection or window.getSelection() detected.");
+ }
+
+ api.getNativeSelection = getSelection;
+
+ var testSelection = getSelection();
+ var testRange = api.createNativeRange(document);
+ var body = dom.getBody(document);
+
+ // Obtaining a range from a selection
+ var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
+ util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
+ api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+ // Test for existence of native selection extend() method
+ var selectionHasExtend = util.isHostMethod(testSelection, "extend");
+ api.features.selectionHasExtend = selectionHasExtend;
+
+ // Test if rangeCount exists
+ var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
+ api.features.selectionHasRangeCount = selectionHasRangeCount;
+
+ var selectionSupportsMultipleRanges = false;
+ var collapsedNonEditableSelectionsSupported = true;
+
+ if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+ typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
+
+ (function() {
+ var iframe = document.createElement("iframe");
+ iframe.frameBorder = 0;
+ iframe.style.position = "absolute";
+ iframe.style.left = "-10000px";
+ body.appendChild(iframe);
+
+ var iframeDoc = dom.getIframeDocument(iframe);
+ iframeDoc.open();
+ iframeDoc.write("12");
+ iframeDoc.close();
+
+ var sel = dom.getIframeWindow(iframe).getSelection();
+ var docEl = iframeDoc.documentElement;
+ var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
+
+ // Test whether the native selection will allow a collapsed selection within a non-editable element
+ var r1 = iframeDoc.createRange();
+ r1.setStart(textNode, 1);
+ r1.collapse(true);
+ sel.addRange(r1);
+ collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+ sel.removeAllRanges();
+
+ // Test whether the native selection is capable of supporting multiple ranges
+ var r2 = r1.cloneRange();
+ r1.setStart(textNode, 0);
+ r2.setEnd(textNode, 2);
+ sel.addRange(r1);
+ sel.addRange(r2);
+
+ selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+
+ // Clean up
+ r1.detach();
+ r2.detach();
+
+ body.removeChild(iframe);
+ })();
+ }
+
+ api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+ api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+ // ControlRanges
+ var implementsControlRange = false, testControlRange;
+
+ if (body && util.isHostMethod(body, "createControlRange")) {
+ testControlRange = body.createControlRange();
+ if (util.areHostProperties(testControlRange, ["item", "add"])) {
+ implementsControlRange = true;
+ }
+ }
+ api.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, backwards) {
+ var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "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;
+ }
+
+ function getNativeRange(range) {
+ var nativeRange;
+ if (range instanceof DomRange) {
+ nativeRange = range._selectionNativeRange;
+ if (!nativeRange) {
+ nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
+ nativeRange.setEnd(range.endContainer, range.endOffset);
+ nativeRange.setStart(range.startContainer, range.startOffset);
+ range._selectionNativeRange = nativeRange;
+ range.attachListener("detach", function() {
+
+ this._selectionNativeRange = null;
+ });
+ }
+ } else if (range instanceof WrappedRange) {
+ nativeRange = range.nativeRange;
+ } else if (api.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 new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+ }
+ return nodes[0];
+ }
+
+ 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;
+ }
+
+ 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 = dom.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);
+ }
+ }
+ }
+
+ 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 = dom.getDocument(controlRange.item(0));
+ var newControlRange = dom.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 new Error("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 (util.isHostMethod(testSelection, "getRangeAt")) {
+ getSelectionRangeAt = function(sel, index) {
+ try {
+ return sel.getRangeAt(index);
+ } catch(ex) {
+ return null;
+ }
+ };
+ } else if (selectionHasAnchorAndFocus) {
+ getSelectionRangeAt = function(sel) {
+ var doc = dom.getDocument(sel.anchorNode);
+ var range = api.createRange(doc);
+ range.setStart(sel.anchorNode, sel.anchorOffset);
+ range.setEnd(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.setStart(sel.focusNode, sel.focusOffset);
+ range.setEnd(sel.anchorNode, sel.anchorOffset);
+ }
+
+ return range;
+ };
+ }
+
+ /**
+ * @constructor
+ */
+ function WrappedSelection(selection, docSelection, win) {
+ this.nativeSelection = selection;
+ this.docSelection = docSelection;
+ this._ranges = [];
+ this.win = win;
+ this.refresh();
+ }
+
+ api.getSelection = function(win) {
+ win = win || window;
+ var sel = win[windowPropertyName];
+ var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+ if (sel) {
+ sel.nativeSelection = nativeSel;
+ sel.docSelection = docSel;
+ sel.refresh(win);
+ } else {
+ sel = new WrappedSelection(nativeSel, docSel, win);
+ win[windowPropertyName] = sel;
+ }
+ return sel;
+ };
+
+ api.getIframeSelection = function(iframeEl) {
+ return api.getSelection(dom.getIframeWindow(iframeEl));
+ };
+
+ var selProto = WrappedSelection.prototype;
+
+ function createControlSelection(sel, ranges) {
+ // Ensure that the selection becomes of type "Control"
+ var doc = dom.getDocument(ranges[0].startContainer);
+ var controlRange = dom.getBody(doc).createControlRange();
+ for (var i = 0, el; i < rangeCount; ++i) {
+ el = getSingleElementFromRange(ranges[i]);
+ try {
+ controlRange.add(el);
+ } catch (ex) {
+ throw new Error("setRanges(): Element within the 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 addRangeBackwards = function(sel, range) {
+ var doc = DomRange.getRangeDocument(range);
+ var endRange = api.createRange(doc);
+ endRange.collapseToPoint(range.endContainer, range.endOffset);
+ sel.nativeSelection.addRange(getNativeRange(endRange));
+ sel.nativeSelection.extend(range.startContainer, range.startOffset);
+ sel.refresh();
+ };
+
+ if (selectionHasRangeCount) {
+ selProto.addRange = function(range, backwards) {
+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+ addRangeToControlSelection(this, range);
+ } else {
+ if (backwards && selectionHasExtend) {
+ addRangeBackwards(this, range);
+ } else {
+ var previousRangeCount;
+ if (selectionSupportsMultipleRanges) {
+ previousRangeCount = this.rangeCount;
+ } else {
+ this.removeAllRanges();
+ previousRangeCount = 0;
+ }
+ this.nativeSelection.addRange(getNativeRange(range));
+
+ // 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 && !DomRange.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, selectionIsBackwards(this.nativeSelection));
+ this.isCollapsed = selectionIsCollapsed(this);
+ } else {
+ // The range was not added successfully. The simplest thing is to refresh
+ this.refresh();
+ }
+ }
+ }
+ };
+ } else {
+ selProto.addRange = function(range, backwards) {
+ if (backwards && selectionHasExtend) {
+ addRangeBackwards(this, range);
+ } else {
+ this.nativeSelection.addRange(getNativeRange(range));
+ this.refresh();
+ }
+ };
+ }
+
+ selProto.setRanges = function(ranges) {
+ if (implementsControlRange && 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 (util.isHostMethod(testSelection, "empty") && util.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 = dom.getDocument(this.anchorNode);
+ } else if (this.docSelection.type == CONTROL) {
+ var controlRange = this.docSelection.createRange();
+ if (controlRange.length) {
+ doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
+ }
+ }
+ if (doc) {
+ var textRange = doc.body.createTextRange();
+ textRange.select();
+ this.docSelection.empty();
+ }
+ }
+ } catch(ex) {}
+ updateEmptySelection(this);
+ };
+
+ selProto.addRange = function(range) {
+ if (this.docSelection.type == CONTROL) {
+ addRangeToControlSelection(this, range);
+ } else {
+ WrappedRange.rangeToTextRange(range).select();
+ this._ranges[0] = range;
+ this.rangeCount = 1;
+ this.isCollapsed = this._ranges[0].collapsed;
+ updateAnchorAndFocusFromRange(this, range, false);
+ }
+ };
+
+ 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 {
+ return this._ranges[index];
+ }
+ };
+
+ var refreshSelection;
+
+ if (useDocumentSelection) {
+ refreshSelection = function(sel) {
+ var range;
+ if (api.isSelectionValid(sel.win)) {
+ range = sel.docSelection.createRange();
+ } else {
+ range = dom.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 (util.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], selectionIsBackwards(sel.nativeSelection));
+ sel.isCollapsed = selectionIsCollapsed(sel);
+ } else {
+ updateEmptySelection(sel);
+ }
+ }
+ };
+ } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.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);
+ } 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;
+ refreshSelection(this);
+ if (checkForChanges) {
+ var i = oldRanges.length;
+ if (i != this._ranges.length) {
+ return false;
+ }
+ while (i--) {
+ if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ };
+
+ // Removal of a single range
+ var removeRangeManually = function(sel, range) {
+ var ranges = sel.getAllRanges(), removed = false;
+ sel.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ if (removed || range !== ranges[i]) {
+ sel.addRange(ranges[i]);
+ } else {
+ // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
+ // times. removeRange should only remove the first instance, so the following ensures only the first
+ // instance is removed
+ removed = true;
+ }
+ }
+ if (!sel.rangeCount) {
+ updateEmptySelection(sel);
+ }
+ };
+
+ if (implementsControlRange) {
+ 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 = dom.getDocument(controlRange.item(0));
+ var newControlRange = dom.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 backwards
+ var selectionIsBackwards;
+ if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
+ selectionIsBackwards = function(sel) {
+ var backwards = false;
+ if (sel.anchorNode) {
+ backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+ }
+ return backwards;
+ };
+
+ selProto.isBackwards = function() {
+ return selectionIsBackwards(this);
+ };
+ } else {
+ selectionIsBackwards = selProto.isBackwards = function() {
+ return false;
+ };
+ }
+
+ // Selection text
+ // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
+ 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.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
+ throw new DOMException("WRONG_DOCUMENT_ERR");
+ }
+ }
+
+ // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
+ selProto.collapse = function(node, offset) {
+ assertNodeInSameDocument(this, node);
+ var range = api.createRange(dom.getDocument(node));
+ range.collapseToPoint(node, offset);
+ this.removeAllRanges();
+ this.addRange(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 HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
+ // never used by Rangy.
+ selProto.selectAllChildren = function(node) {
+ assertNodeInSameDocument(this, node);
+ var range = api.createRange(dom.getDocument(node));
+ range.selectNodeContents(node);
+ this.removeAllRanges();
+ this.addRange(range);
+ };
+
+ selProto.deleteFromDocument = function() {
+ // Sepcial behaviour required for 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);
+ element.parentNode.removeChild(element);
+ }
+ this.refresh();
+ } else if (this.rangeCount) {
+ var ranges = this.getAllRanges();
+ this.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ ranges[i].deleteContents();
+ }
+ // The HTML5 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.getAllRanges = function() {
+ return this._ranges.slice(0);
+ };
+
+ selProto.setSingleRange = function(range) {
+ this.setRanges( [range] );
+ };
+
+ selProto.containsNode = function(node, allowPartial) {
+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
+ if (this._ranges[i].containsNode(node, allowPartial)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ selProto.toHtml = function() {
+ var html = "";
+ if (this.rangeCount) {
+ var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
+ container.appendChild(this._ranges[i].cloneContents());
+ }
+ html = container.innerHTML;
+ }
+ return html;
+ };
+
+ 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() {
+ this.win[windowPropertyName] = null;
+ this.win = this.anchorNode = this.focusNode = null;
+ };
+
+ WrappedSelection.inspect = inspect;
+
+ api.Selection = WrappedSelection;
+
+ api.selectionPrototype = selProto;
+
+ api.addCreateMissingNativeApiListener(function(win) {
+ if (typeof win.getSelection == "undefined") {
+ win.getSelection = function() {
+ return api.getSelection(this);
+ };
+ }
+ win = null;
+ });
+});
+
+/**
+ * @license CSS Class Applier module for Rangy.
+ * Adds, removes and toggles CSS classes on Ranges and Selections
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2012, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.3
+ * Build date: 26 February 2012
+ */
+rangy.createModule("CssClassApplier", function(api, module) {
+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
+
+ var dom = api.dom;
+
+
+
+ var defaultTagName = "span";
+
+ function trim(str) {
+ return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
+ }
+
+ function hasClass(el, cssClass) {
+ return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
+ }
+
+ function addClass(el, cssClass) {
+ if (el.className) {
+ if (!hasClass(el, cssClass)) {
+ el.className += " " + cssClass;
+ }
+ } else {
+ el.className = cssClass;
+ }
+ }
+
+ var removeClass = (function() {
+ function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
+ return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
+ }
+
+ return function(el, cssClass) {
+ if (el.className) {
+ el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
+ }
+ };
+ })();
+
+ function sortClassName(className) {
+ return className.split(/\s+/).sort().join(" ");
+ }
+
+ function getSortedClassName(el) {
+ return sortClassName(el.className);
+ }
+
+ function haveSameClasses(el1, el2) {
+ return getSortedClassName(el1) == getSortedClassName(el2);
+ }
+
+ function replaceWithOwnChildren(el) {
+
+ var parent = el.parentNode;
+ while (el.hasChildNodes()) {
+ parent.insertBefore(el.firstChild, el);
+ }
+ parent.removeChild(el);
+ }
+
+ function rangeSelectsAnyText(range, textNode) {
+ var textRange = range.cloneRange();
+ textRange.selectNodeContents(textNode);
+
+ var intersectionRange = textRange.intersection(range);
+ var text = intersectionRange ? intersectionRange.toString() : "";
+ textRange.detach();
+
+ return text != "";
+ }
+
+ function getEffectiveTextNodes(range) {
+ return range.getNodes([3], function(textNode) {
+ return rangeSelectsAnyText(range, textNode);
+ });
+ }
+
+ 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.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 && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function elementHasProps(el, props) {
+ for (var p in props) {
+ if (props.hasOwnProperty(p) && el[p] !== props[p]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var getComputedStyleProperty;
+
+ if (typeof window.getComputedStyle != "undefined") {
+ getComputedStyleProperty = function(el, propName) {
+ return dom.getWindow(el).getComputedStyle(el, null)[propName];
+ };
+ } else if (typeof document.documentElement.currentStyle != "undefined") {
+ getComputedStyleProperty = function(el, propName) {
+ return el.currentStyle[propName];
+ };
+ } else {
+ module.fail("No means of obtaining computed style properties found");
+ }
+
+ var isEditableElement;
+
+ (function() {
+ var testEl = document.createElement("div");
+ if (typeof testEl.isContentEditable == "boolean") {
+ isEditableElement = function(node) {
+ return node && node.nodeType == 1 && node.isContentEditable;
+ };
+ } else {
+ isEditableElement = 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 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, rangesToPreserve) {
+ var newNode;
+ var splitAtStart = (descendantOffset == 0);
+
+ if (dom.isAncestorOf(descendantNode, node)) {
+
+ return node;
+ }
+
+ if (dom.isCharacterDataNode(descendantNode)) {
+ if (descendantOffset == 0) {
+ descendantOffset = dom.getNodeIndex(descendantNode);
+ descendantNode = descendantNode.parentNode;
+ } else if (descendantOffset == descendantNode.length) {
+ descendantOffset = dom.getNodeIndex(descendantNode) + 1;
+ descendantNode = descendantNode.parentNode;
+ } else {
+ throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
+ + descendantOffset + " in " + descendantNode.data);
+ }
+ }
+
+ if (isSplitPoint(descendantNode, descendantOffset)) {
+ if (!newNode) {
+ newNode = descendantNode.cloneNode(false);
+ if (newNode.id) {
+ newNode.removeAttribute("id");
+ }
+ var child;
+ while ((child = descendantNode.childNodes[descendantOffset])) {
+ newNode.appendChild(child);
+ }
+ dom.insertAfter(newNode, descendantNode);
+ }
+ return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
+ } 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, rangesToPreserve);
+ }
+ return node;
+ }
+
+ function areElementsMergeable(el1, el2) {
+ return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
+ }
+
+ function createAdjacentMergeableTextNodeGetter(forward) {
+ var propName = forward ? "nextSibling" : "previousSibling";
+
+ return function(textNode, checkParentElement) {
+ var el = textNode.parentNode;
+ var adjacentNode = textNode[propName];
+ 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[propName];
+
+ if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
+ return adjacentNode[forward ? "firstChild" : "lastChild"];
+ }
+ }
+ return null;
+ }
+ }
+
+ var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
+ getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
+
+
+ function Merge(firstNode) {
+ this.isElementMerge = (firstNode.nodeType == 1);
+ this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
+ this.textNodes = [this.firstTextNode];
+ }
+
+ Merge.prototype = {
+ doMerge: function() {
+ var textBits = [], textNode, parent, text;
+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
+ textNode = this.textNodes[i];
+ parent = textNode.parentNode;
+ textBits[i] = textNode.data;
+ if (i) {
+ parent.removeChild(textNode);
+ if (!parent.hasChildNodes()) {
+ parent.parentNode.removeChild(parent);
+ }
+ }
+ }
+ this.firstTextNode.data = text = textBits.join("");
+ return text;
+ },
+
+ getLength: function() {
+ var i = this.textNodes.length, len = 0;
+ while (i--) {
+ len += this.textNodes[i].length;
+ }
+ return len;
+ },
+
+ toString: function() {
+ var textBits = [];
+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
+ textBits[i] = "'" + this.textNodes[i].data + "'";
+ }
+ return "[Merge(" + textBits.join(",") + ")]";
+ }
+ };
+
+ var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
+
+ // Allow "class" as a property name in object properties
+ var mappedPropertyNames = {"class" : "className"};
+
+ function CssClassApplier(cssClass, options, tagNames) {
+ this.cssClass = cssClass;
+ var normalize, i, len, propName;
+
+ var elementPropertiesFromOptions = null;
+
+ // Initialize from options object
+ if (typeof options == "object" && options !== null) {
+ tagNames = options.tagNames;
+ elementPropertiesFromOptions = options.elementProperties;
+
+ for (i = 0; propName = optionProperties[i++]; ) {
+ if (options.hasOwnProperty(propName)) {
+ this[propName] = options[propName];
+ }
+ }
+ normalize = options.normalize;
+ } else {
+ normalize = options;
+ }
+
+ // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
+ this.normalize = (typeof normalize == "undefined") ? true : normalize;
+
+ // Initialize element properties and attribute exceptions
+ this.attrExceptions = [];
+ var el = document.createElement(this.elementTagName);
+ this.elementProperties = {};
+ for (var p in elementPropertiesFromOptions) {
+ if (elementPropertiesFromOptions.hasOwnProperty(p)) {
+ // Map "class" to "className"
+ if (mappedPropertyNames.hasOwnProperty(p)) {
+ p = mappedPropertyNames[p];
+ }
+ el[p] = elementPropertiesFromOptions[p];
+
+ // 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.
+ this.elementProperties[p] = el[p];
+ this.attrExceptions.push(p);
+ }
+ }
+
+ this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
+ sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
+
+ // Initialize tag names
+ this.applyToAnyTagName = false;
+ var type = typeof tagNames;
+ if (type == "string") {
+ if (tagNames == "*") {
+ this.applyToAnyTagName = true;
+ } else {
+ this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
+ }
+ } else if (type == "object" && typeof tagNames.length == "number") {
+ this.tagNames = [];
+ for (i = 0, len = tagNames.length; i < len; ++i) {
+ if (tagNames[i] == "*") {
+ this.applyToAnyTagName = true;
+ } else {
+ this.tagNames.push(tagNames[i].toLowerCase());
+ }
+ }
+ } else {
+ this.tagNames = [this.elementTagName];
+ }
+ }
+
+ CssClassApplier.prototype = {
+ elementTagName: defaultTagName,
+ elementProperties: {},
+ ignoreWhiteSpace: true,
+ applyToEditableOnly: false,
+
+ hasClass: function(node) {
+ return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
+ },
+
+ getSelfOrAncestorWithClass: function(node) {
+ while (node) {
+ if (this.hasClass(node, this.cssClass)) {
+ 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 CSS class to a Range.
+ postApply: function(textNodes, range, 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 textNode, precedingTextNode;
+
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+ precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
+
+ if (precedingTextNode) {
+ if (!currentMerge) {
+ currentMerge = new Merge(precedingTextNode);
+ merges.push(currentMerge);
+ }
+ currentMerge.textNodes.push(textNode);
+ if (textNode === firstNode) {
+ rangeStartNode = currentMerge.firstTextNode;
+ rangeStartOffset = rangeStartNode.length;
+ }
+ if (textNode === lastNode) {
+ rangeEndNode = currentMerge.firstTextNode;
+ 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);
+ }
+
+ // Do the merges
+ if (merges.length) {
+
+ for (i = 0, len = merges.length; i < len; ++i) {
+ merges[i].doMerge();
+ }
+
+
+ // Set the range boundaries
+ range.setStart(rangeStartNode, rangeStartOffset);
+ range.setEnd(rangeEndNode, rangeEndOffset);
+ }
+
+ },
+
+ createContainer: function(doc) {
+ var el = doc.createElement(this.elementTagName);
+ api.util.extend(el, this.elementProperties);
+ addClass(el, this.cssClass);
+ return el;
+ },
+
+ applyToTextNode: function(textNode) {
+
+
+ var parent = textNode.parentNode;
+ if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
+ addClass(parent, this.cssClass);
+ } else {
+ var el = this.createContainer(dom.getDocument(textNode));
+ textNode.parentNode.insertBefore(el, textNode);
+ el.appendChild(textNode);
+ }
+
+ },
+
+ isRemovable: function(el) {
+ return el.tagName.toLowerCase() == this.elementTagName
+ && getSortedClassName(el) == this.elementSortedClassName
+ && elementHasProps(el, this.elementProperties)
+ && !elementHasNonClassAttributes(el, this.attrExceptions)
+ && this.isModifiable(el);
+ },
+
+ undoToTextNode: function(textNode, range, ancestorWithClass) {
+
+ if (!range.containsNode(ancestorWithClass)) {
+ // Split out the portion of the ancestor from which we can remove the CSS class
+ //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
+ var ancestorRange = range.cloneRange();
+ ancestorRange.selectNode(ancestorWithClass);
+
+ if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
+ splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
+ range.setEndAfter(ancestorWithClass);
+ }
+ if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
+ ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
+ }
+ }
+
+ if (this.isRemovable(ancestorWithClass)) {
+ replaceWithOwnChildren(ancestorWithClass);
+ } else {
+ removeClass(ancestorWithClass, this.cssClass);
+ }
+ },
+
+ applyToRange: function(range) {
+ range.splitBoundaries();
+ var textNodes = getEffectiveTextNodes(range);
+
+ if (textNodes.length) {
+ var textNode;
+
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+
+ if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
+ && this.isModifiable(textNode)) {
+ this.applyToTextNode(textNode);
+ }
+ }
+ range.setStart(textNodes[0], 0);
+ textNode = textNodes[textNodes.length - 1];
+ range.setEnd(textNode, textNode.length);
+ if (this.normalize) {
+ this.postApply(textNodes, range, false);
+ }
+ }
+ },
+
+ applyToSelection: function(win) {
+
+ win = win || window;
+ var sel = api.getSelection(win);
+
+ var range, ranges = sel.getAllRanges();
+ sel.removeAllRanges();
+ var i = ranges.length;
+ while (i--) {
+ range = ranges[i];
+ this.applyToRange(range);
+ sel.addRange(range);
+ }
+
+ },
+
+ undoToRange: function(range) {
+
+ range.splitBoundaries();
+ var textNodes = getEffectiveTextNodes(range);
+ var textNode, ancestorWithClass;
+ var lastTextNode = textNodes[textNodes.length - 1];
+
+ if (textNodes.length) {
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+ ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
+ if (ancestorWithClass && this.isModifiable(textNode)) {
+ this.undoToTextNode(textNode, range, ancestorWithClass);
+ }
+
+ // Ensure the range is still valid
+ range.setStart(textNodes[0], 0);
+ range.setEnd(lastTextNode, lastTextNode.length);
+ }
+
+
+
+ if (this.normalize) {
+ this.postApply(textNodes, range, true);
+ }
+ }
+ },
+
+ undoToSelection: function(win) {
+ win = win || window;
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges(), range;
+ sel.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ range = ranges[i];
+ this.undoToRange(range);
+ sel.addRange(range);
+ }
+ },
+
+ getTextSelectedByRange: function(textNode, range) {
+ var textRange = range.cloneRange();
+ textRange.selectNodeContents(textNode);
+
+ var intersectionRange = textRange.intersection(range);
+ var text = intersectionRange ? intersectionRange.toString() : "";
+ textRange.detach();
+
+ return text;
+ },
+
+ isAppliedToRange: function(range) {
+ if (range.collapsed) {
+ return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
+ } else {
+ var textNodes = range.getNodes( [3] );
+ for (var i = 0, textNode; textNode = textNodes[i++]; ) {
+ if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
+ && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ },
+
+ isAppliedToSelection: function(win) {
+ win = win || window;
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges();
+ var i = ranges.length;
+ while (i--) {
+ if (!this.isAppliedToRange(ranges[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ toggleRange: function(range) {
+ if (this.isAppliedToRange(range)) {
+ this.undoToRange(range);
+ } else {
+ this.applyToRange(range);
+ }
+ },
+
+ toggleSelection: function(win) {
+ if (this.isAppliedToSelection(win)) {
+ this.undoToSelection(win);
+ } else {
+ this.applyToSelection(win);
+ }
+ },
+
+ detach: function() {}
+ };
+
+ function createCssClassApplier(cssClass, options, tagNames) {
+ return new CssClassApplier(cssClass, options, tagNames);
+ }
+
+ CssClassApplier.util = {
+ hasClass: hasClass,
+ addClass: addClass,
+ removeClass: removeClass,
+ hasSameClasses: haveSameClasses,
+ replaceWithOwnChildren: replaceWithOwnChildren,
+ elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
+ elementHasNonClassAttributes: elementHasNonClassAttributes,
+ splitNodeAt: splitNodeAt,
+ isEditableElement: isEditableElement,
+ isEditingHost: isEditingHost,
+ isEditable: isEditable
+ };
+
+ api.CssClassApplier = CssClassApplier;
+ api.createCssClassApplier = createCssClassApplier;
+});
+
+/**
+ * @license 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
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2012, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.3
+ * Build date: 26 February 2012
+ */
+rangy.createModule("SaveRestore", function(api, module) {
+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+ var dom = api.dom;
+
+ 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);
+ boundaryRange.detach();
+ return markerEl;
+ }
+
+ function setRangeBoundary(doc, range, markerId, atStart) {
+ var markerEl = gEBI(markerId, doc);
+ if (markerEl) {
+ range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
+ markerEl.parentNode.removeChild(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 saveSelection(win) {
+ win = win || window;
+ var doc = win.document;
+ 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;
+ }
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges();
+ var rangeInfos = [], startEl, endEl, range;
+
+ // Order the ranges by position within the DOM, latest first
+ ranges.sort(compareRanges);
+
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ range = ranges[i];
+ if (range.collapsed) {
+ endEl = insertRangeBoundaryMarker(range, false);
+ rangeInfos.push({
+ markerId: endEl.id,
+ collapsed: true
+ });
+ } else {
+ endEl = insertRangeBoundaryMarker(range, false);
+ startEl = insertRangeBoundaryMarker(range, true);
+
+ rangeInfos[i] = {
+ startMarkerId: startEl.id,
+ endMarkerId: endEl.id,
+ collapsed: false,
+ backwards: ranges.length == 1 && sel.isBackwards()
+ };
+ }
+ }
+
+ // 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];
+ if (range.collapsed) {
+ range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
+ } else {
+ range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
+ range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
+ }
+ }
+
+ // Ensure current selection is unaffected
+ sel.setRanges(ranges);
+ return {
+ win: win,
+ doc: doc,
+ rangeInfos: rangeInfos,
+ restored: false
+ };
+ }
+
+ function restoreSelection(savedSelection, preserveDirection) {
+ if (!savedSelection.restored) {
+ var rangeInfos = savedSelection.rangeInfos;
+ var sel = api.getSelection(savedSelection.win);
+ 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.
+ for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
+ rangeInfo = rangeInfos[i];
+ range = api.createRange(savedSelection.doc);
+ if (rangeInfo.collapsed) {
+ var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
+ if (markerEl) {
+ markerEl.style.display = "inline";
+ var previousNode = markerEl.previousSibling;
+
+ // Workaround for issue 17
+ if (previousNode && previousNode.nodeType == 3) {
+ markerEl.parentNode.removeChild(markerEl);
+ range.collapseToPoint(previousNode, previousNode.length);
+ } else {
+ range.collapseBefore(markerEl);
+ markerEl.parentNode.removeChild(markerEl);
+ }
+ } else {
+ module.warn("Marker element has been removed. Cannot restore selection.");
+ }
+ } else {
+ setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
+ setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
+ }
+
+ // Normalizing range boundaries is only viable if the selection contains only one range. For example,
+ // if the selection contained two ranges that were both contained within the same single text node,
+ // both would alter the same text node when restoring and break the other range.
+ if (len == 1) {
+ range.normalizeBoundaries();
+ }
+ ranges[i] = range;
+ }
+ if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
+ 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) {
+ markerEl.parentNode.removeChild(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.saveSelection = saveSelection;
+ api.restoreSelection = restoreSelection;
+ api.removeMarkerElement = removeMarkerElement;
+ api.removeMarkers = removeMarkers;
+});
+
+/**
+ * @license 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
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2012, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.3
+ * Build date: 26 February 2012
+ */
+rangy.createModule("Serializer", function(api, module) {
+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
+ var UNDEF = "undefined";
+
+ // encodeURIComponent and decodeURIComponent are required for cookie handling
+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
+ module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
+ }
+
+ // 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 pathBits = [], n = node;
+ rootNode = rootNode || dom.getDocument(node).documentElement;
+ while (n && n != rootNode) {
+ pathBits.push(dom.getNodeIndex(n, true));
+ n = n.parentNode;
+ }
+ return pathBits.join("/") + ":" + offset;
+ }
+
+ function deserializePosition(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var bits = serialized.split(":");
+ var node = rootNode;
+ var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
+
+ while (i--) {
+ nodeIndex = parseInt(nodeIndices[i], 10);
+ if (nodeIndex < node.childNodes.length) {
+ node = node.childNodes[parseInt(nodeIndices[i], 10)];
+ } else {
+ throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
+ " has no child with index " + nodeIndex + ", " + i);
+ }
+ }
+
+ return new dom.DomPosition(node, parseInt(bits[1], 10));
+ }
+
+ function serializeRange(range, omitChecksum, rootNode) {
+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
+ if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
+ throw new Error("serializeRange: range is not wholly contained within specified root node");
+ }
+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
+ serializePosition(range.endContainer, range.endOffset, rootNode);
+ if (!omitChecksum) {
+ serialized += "{" + getElementChecksum(rootNode) + "}";
+ }
+ return serialized;
+ }
+
+ function deserializeRange(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
+ var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
+ if (checksum && checksum !== getElementChecksum(rootNode)) {
+ throw new Error("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.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+ return range;
+ }
+
+ function canDeserializeRange(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
+ var checksum = result[3];
+ return !checksum || checksum === getElementChecksum(rootNode);
+ }
+
+ function serializeSelection(selection, omitChecksum, rootNode) {
+ selection = selection || api.getSelection();
+ 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;
+ }
+
+ api.serializePosition = serializePosition;
+ api.deserializePosition = deserializePosition;
+
+ api.serializeRange = serializeRange;
+ api.deserializeRange = deserializeRange;
+ api.canDeserializeRange = canDeserializeRange;
+
+ api.serializeSelection = serializeSelection;
+ api.deserializeSelection = deserializeSelection;
+ api.canDeserializeSelection = canDeserializeSelection;
+
+ api.restoreSelectionFromCookie = restoreSelectionFromCookie;
+ api.saveSelectionCookie = saveSelectionCookie;
+
+ api.getElementChecksum = getElementChecksum;
+});
+YUI.add('moodle-editor_atto-rangy', 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 .
+
+if (!rangy.initialized) {
+ rangy.init();
+}
+
+
+}, '@VERSION@', {"requires": []});
diff --git a/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js b/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js
new file mode 100644
index 00000000000..f900c4eb35f
--- /dev/null
+++ b/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js
@@ -0,0 +1,10 @@
+window.rangy=function(){function u(n,r){var i=typeof n[r];return i==t||i==e&&!!n[r]||i=="unknown"}function a(t,n){return typeof t[n]==e&&!!t[n]}function f(e,t){return typeof e[t]!=n}function l(e){return function(t,n){var r=n.length;while(r--)if(!e(t,n[r]))return!1;return!0}}function d(e){return e&&c(e,o)&&p(e,s)}function m(e){window.alert("Rangy not supported in your browser. Reason: "+e),v.initialized=!0,v.supported=!1}function g(e){var t="Rangy warning: "+e;v.config.alertOnWarn?window.alert(t):typeof window.console!=n&&typeof window.console.log!=n&&window.console.log(t)}function w(){if(v.initialized)return;var e,t=!1,n=!1;u(document,"createRange")&&(e=document.createRange(),c(e,i)&&p(e,r)&&(t=!0),e.detach());var s=a(document,"body")?document.body:document.getElementsByTagName("body")[0];s&&u(s,"createTextRange")&&(e=s.createTextRange(),d(e)&&(n=!0)),!t&&!n&&m("Neither Range nor TextRange are implemented"),v.initialized=!0,v.features={implementsDomRange:t,implementsTextRange:n};var o=b.concat(y);for(var f=0,l=o.length;f["+e.childNodes.length+"]"}return e.nodeName}function C(e){this.root=e,this._next=e}function k(e){return new C(e)}function L(e,t){this.node=e,this.offset=t}function A(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var n="undefined",r=e.util;r.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),r.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var i=document.createElement("div");r.areHostMethods(i,["insertBefore","appendChild","cloneNode"]||!r.areHostObjects(i,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),r.isHostProperty(i,"innerHTML")||t.fail("Element is missing innerHTML property");var s=document.createTextNode("test");r.areHostMethods(s,["splitText","deleteData","insertData","appendData","cloneNode"]||!r.areHostObjects(i,["previousSibling","nextSibling","childNodes","parentNode"])||!r.areHostProperties(s,["data"]))||t.fail("Incomplete Text Node implementation");var o=function(e,t){var n=e.length;while(n--)if(e[n]===t)return!0;return!1};C.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next,t,n;if(this._current){t=e.firstChild;if(t)this._next=t;else{n=null;while(e!==this.root&&!(n=e.nextSibling))e=e.parentNode;this._next=n}}return this._current},detach:function(){this._current=this._next=this.root=null}},L.prototype={equals:function(e){return this.node===e.node&this.offset==e.offset},inspect:function(){return"[DomPosition("+N(this.node)+":"+this.offset+")]"}},A.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},A.prototype.toString=function(){return this.message},e.dom={arrayContains:o,isHtmlNamespace:u,parentElement:a,getNodeIndex:f,getNodeLength:l,getCommonAncestor:c,isAncestorOf:h,getClosestAncestorIn:p,isCharacterDataNode:d,insertAfter:v,splitDataNode:m,getDocument:g,getWindow:y,getIframeWindow:w,getIframeDocument:b,getBody:E,getRootContainer:S,comparePoints:x,inspectNode:N,fragmentFromNodeChildren:T,createIterator:k,DomPosition:L},e.DOMException=A}),rangy.createModule("DomRange",function(e,t){function s(e,t){return e.nodeType!=3&&(n.isAncestorOf(e,t.startContainer,!0)||n.isAncestorOf(e,t.endContainer,!0))}function o(e){return n.getDocument(e.startContainer)}function u(e,t,n){var r=e._listeners[t];if(r)for(var i=0,s=r.length;i=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[r]),i}function c(e){var t;for(var n,r=o(e.range).createDocumentFragment(),s;n=e.next();){t=e.isPartiallySelectedSubtree(),n=n.cloneNode(!t),t&&(s=e.getSubtreeIterator(),n.appendChild(c(s)),s.detach(!0));if(n.nodeType==10)throw new i("HIERARCHY_REQUEST_ERR");r.appendChild(n)}return r}function h(e,t,r){var i,s;r=r||{stop:!1};for(var o,u;o=e.next();)if(e.isPartiallySelectedSubtree()){if(t(o)===!1){r.stop=!0;return}u=e.getSubtreeIterator(),h(u,t,r),u.detach(!0);if(r.stop)return}else{i=n.createIterator(o);while(s=i.next())if(t(s)===!1){r.stop=!0;return}}}function p(e){var t;while(e.next())e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),p(t),t.detach(!0)):e.remove()}function d(e){for(var t,n=o(e.range).createDocumentFragment(),r;t=e.next();){e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(d(r)),r.detach(!0)):e.remove();if(t.nodeType==10)throw new i("HIERARCHY_REQUEST_ERR");n.appendChild(t)}return n}function v(e,t,n){var r=!!t&&!!t.length,i,s=!!n;r&&(i=new RegExp("^("+t.join("|")+")$"));var o=[];return h(new g(e,!1),function(e){(!r||i.test(e.nodeType))&&(!s||n(e))&&o.push(e)}),o}function m(e){var t=typeof e.getName=="undefined"?"Range":e.getName();return"["+t+"("+n.inspectNode(e.startContainer)+":"+e.startOffset+", "+n.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function g(e,t){this.range=e,this.clonePartiallySelectedTextNodes=t;if(!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var r=e.commonAncestorContainer;this.sc===this.ec&&n.isCharacterDataNode(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc===r&&!n.isCharacterDataNode(this.sc)?this.sc.childNodes[this.so]:n.getClosestAncestorIn(this.sc,r,!0),this._last=this.ec===r&&!n.isCharacterDataNode(this.ec)?this.ec.childNodes[this.eo-1]:n.getClosestAncestorIn(this.ec,r,!0))}}function y(e){this.code=this[e],this.codeName=e,this.message="RangeException: "+this.codeName}function b(e,t,n){this.nodes=v(e,t,n),this._next=this.nodes[0],this._position=0}function N(e){return function(t,r){var i,s=r?t:t.parentNode;while(s){i=s.nodeType;if(n.arrayContains(e,i))return s;s=s.parentNode}return null}}function O(e,t){if(A(e,t))throw new y("INVALID_NODE_TYPE_ERR")}function M(e){if(!e.startContainer)throw new i("INVALID_STATE_ERR")}function _(e,t){if(!n.arrayContains(t,e.nodeType))throw new y("INVALID_NODE_TYPE_ERR")}function D(e,t){if(t<0||t>(n.isCharacterDataNode(e)?e.length:e.childNodes.length))throw new i("INDEX_SIZE_ERR")}function P(e,t){if(k(e,!0)!==k(t,!0))throw new i("WRONG_DOCUMENT_ERR")}function H(e){if(L(e,!0))throw new i("NO_MODIFICATION_ALLOWED_ERR")}function B(e,t){if(!e)throw new i(t)}function j(e){return!n.arrayContains(E,e.nodeType)&&!k(e,!0)}
+function F(e,t){return t<=(n.isCharacterDataNode(e)?e.length:e.childNodes.length)}function I(e){return!!e.startContainer&&!!e.endContainer&&!j(e.startContainer)&&!j(e.endContainer)&&F(e.startContainer,e.startOffset)&&F(e.endContainer,e.endOffset)}function q(e){M(e);if(!I(e))throw new Error("Range error: Range is no longer valid after DOM mutation ("+e.inspect()+")")}function et(){}function tt(e){e.START_TO_START=V,e.START_TO_END=$,e.END_TO_END=J,e.END_TO_START=K,e.NODE_BEFORE=Q,e.NODE_AFTER=G,e.NODE_BEFORE_AND_AFTER=Y,e.NODE_INSIDE=Z}function nt(e){tt(e),tt(e.prototype)}function rt(e,t){return function(){q(this);var r=this.startContainer,i=this.startOffset,s=this.commonAncestorContainer,o=new g(this,!0),u,a;r!==s&&(u=n.getClosestAncestorIn(r,s,!0),a=f(u),r=a.node,i=a.offset),h(o,H),o.reset();var l=e(o);return o.detach(),t(this,r,i,r,i),l}}function it(t,r,i){function o(e,t){return function(n){M(this),_(n,w),_(C(n),E);var r=(e?a:f)(n);(t?u:l)(this,r.node,r.offset)}}function u(e,t,i){var s=e.endContainer,o=e.endOffset;if(t!==e.startContainer||i!==e.startOffset){if(C(t)!=C(s)||n.comparePoints(t,i,s,o)==1)s=t,o=i;r(e,t,i,s,o)}}function l(e,t,i){var s=e.startContainer,o=e.startOffset;if(t!==e.endContainer||i!==e.endOffset){if(C(t)!=C(s)||n.comparePoints(t,i,s,o)==-1)s=t,o=i;r(e,s,o,t,i)}}function c(e,t,n){(t!==e.startContainer||n!==e.startOffset||t!==e.endContainer||n!==e.endOffset)&&r(e,t,n,t,n)}t.prototype=new et,e.util.extend(t.prototype,{setStart:function(e,t){M(this),O(e,!0),D(e,t),u(this,e,t)},setEnd:function(e,t){M(this),O(e,!0),D(e,t),l(this,e,t)},setStartBefore:o(!0,!0),setStartAfter:o(!1,!0),setEndBefore:o(!0,!1),setEndAfter:o(!1,!1),collapse:function(e){q(this),e?r(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):r(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){M(this),O(e,!0),r(this,e,0,e,n.getNodeLength(e))},selectNode:function(e){M(this),O(e,!1),_(e,w);var t=a(e),n=f(e);r(this,t.node,t.offset,n.node,n.offset)},extractContents:rt(d,r),deleteContents:rt(p,r),canSurroundContents:function(){q(this),H(this.startContainer),H(this.endContainer);var e=new g(this,!0),t=e._first&&s(e._first,this)||e._last&&s(e._last,this);return e.detach(),!t},detach:function(){i(this)},splitBoundaries:function(){q(this);var e=this.startContainer,t=this.startOffset,i=this.endContainer,s=this.endOffset,o=e===i;n.isCharacterDataNode(i)&&s>0&&s0&&t=n.getNodeIndex(e)&&s++,t=0),r(this,e,t,i,s)},normalizeBoundaries:function(){q(this);var e=this.startContainer,t=this.startOffset,i=this.endContainer,s=this.endOffset,o=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(i=e,s=e.length,e.appendData(t.data),t.parentNode.removeChild(t))},u=function(r){var o=r.previousSibling;if(o&&o.nodeType==r.nodeType){e=r;var u=r.length;t=o.length,r.insertData(0,o.data),o.parentNode.removeChild(o);if(e==i)s+=t,i=e;else if(i==r.parentNode){var a=n.getNodeIndex(r);s==a?(i=r,s=u):s>a&&s--}}},a=!0;if(n.isCharacterDataNode(i))i.length==s&&o(i);else{if(s>0){var f=i.childNodes[s-1];f&&n.isCharacterDataNode(f)&&o(f)}a=!this.collapsed}if(a){if(n.isCharacterDataNode(e))t==0&&u(e);else if(tx",U=R.firstChild.nodeType==3}catch(z){}e.features.htmlParsingConforms=U;var W=U?function(e){var t=this.startContainer,r=n.getDocument(t);if(!t)throw new i("INVALID_STATE_ERR");var s=null;return t.nodeType==1?s=t:n.isCharacterDataNode(t)&&(s=n.parentElement(t)),s===null||s.nodeName=="HTML"&&n.isHtmlNamespace(n.getDocument(s).documentElement)&&n.isHtmlNamespace(s)?s=r.createElement("body"):s=s.cloneNode(!1),s.innerHTML=e,n.fragmentFromNodeChildren(s)}:function(e){M(this);var t=o(this),r=t.createElement("body");return r.innerHTML=e,n.fragmentFromNodeChildren(r)},X=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],V=0,$=1,J=2,K=3,Q=0,G=1,Y=2,Z=3;et.prototype={attachListener:function(e,t){this._listeners[e].push(t)},compareBoundaryPoints:function(e,t){q(this),P(this.startContainer,t.startContainer);var r,i,s,o,u=e==K||e==V?"start":"end",a=e==$||e==V?"start":"end";return r=this[u+"Container"],i=this[u+"Offset"],s=t[a+"Container"],o=t[a+"Offset"],n.comparePoints(r,i,s,o)},insertNode:function(e){q(this),_(e,x),H(this.startContainer);if(n.isAncestorOf(e,this.startContainer,!0))throw new i("HIERARCHY_REQUEST_ERR");var t=l(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){q(this);var e,t;if(this.collapsed)return o(this).createDocumentFragment();if(this.startContainer===this.endContainer&&n.isCharacterDataNode(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=o(this).createDocumentFragment(),t.appendChild(e),t;var r=new g(this,!0);return e=c(r),r.detach(),e},canSurroundContents:function(){q(this),H(this.startContainer),H(this.endContainer);var e=new g(this,!0),t=e._first&&s(e._first,this)||e._last&&s(e._last,this);return e.detach(),!t},surroundContents:function(e){_(e,T);if(!this.canSurroundContents())throw new y("BAD_BOUNDARYPOINTS_ERR");var t=this.extractContents();if(e.hasChildNodes())while(e.lastChild)e.removeChild(e.lastChild);l(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){q(this);var e=new at(o(this)),t=X.length,n;while(t--)n=X[t],e[n]=this[n];return e},toString:function(){q(this);var e=this.startContainer;if(e===this.endContainer&&n.isCharacterDataNode(e))return e.nodeType==3||e.nodeType==4?e.data.slice(this.startOffset,this.endOffset):"";var t=[],r=new g(this,!0);return h(r,function(e){(e.nodeType==3||e.nodeType==4)&&t.push(e.data)}),r.detach(),t.join("")},compareNode:function(e){q(this);var t=e.parentNode,r=n.getNodeIndex(e);if(!t)throw new i("NOT_FOUND_ERR");var s=this.comparePoint(t,r),o=this.comparePoint(t,r+1);return s<0?o>0?Y:Q:o>0?G:Z},comparePoint:function(e,t){return q(this),B(e,"HIERARCHY_REQUEST_ERR"),P(e,this.startContainer),n.comparePoints(e,t,this.startContainer,this.startOffset)<0?-1:n.comparePoints(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:W,toHtml:function(){q(this);var e=o(this).createElement("div");return e.appendChild(this.cloneContents()),e.innerHTML},intersectsNode:function(e,t){q(this),B(e,"NOT_FOUND_ERR");if(n.getDocument(e)!==o(this))return!1;var r=e.parentNode,i=n.getNodeIndex(e);B(r,"NOT_FOUND_ERR");var s=n.comparePoints(r,i,this.endContainer,this.endOffset),u=n.comparePoints(r,i+1,this.startContainer,this.startOffset);return t?s<=0&&u>=0:s<0&&u>0},isPointInRange:function(e,t){return q(this),B(e,"HIERARCHY_REQUEST_ERR"),P(e,this.startContainer),n.comparePoints(e,t,this.startContainer,this.startOffset)>=0&&n.comparePoints(e,t,this.endContainer,this.endOffset)<=0},intersectsRange:function(e,t){q(this);if(o(e)!=o(this))throw new i("WRONG_DOCUMENT_ERR");var r=n.comparePoints(this.startContainer,this.startOffset,e.endContainer,e.endOffset),s=n.comparePoints(this.endContainer,this.endOffset,e.startContainer,e.startOffset);return t?r<=0&&s>=0:r<0&&s>0},intersection:function(e){if(this.intersectsRange(e)){var t=n.comparePoints(this.startContainer,this.startOffset,e.startContainer,e.startOffset),r=n.comparePoints(this.endContainer,this.endOffset,e.endContainer,e.endOffset),i=this.cloneRange();return t==-1&&i.setStart(e.startContainer,e.startOffset),r==1&&i.setEnd(e.endContainer,e.endOffset),i}return null},union:function(e){if(this.intersectsRange(e,!0)){var t=this.cloneRange();return n.comparePoints(e.startContainer,e.startOffset,this.startContainer,this.startOffset)==-1&&t.setStart(e.startContainer,e.startOffset),n.comparePoints(e.endContainer,e.endOffset,this.endContainer,this.endOffset)==1&&t.setEnd(e.endContainer,e.endOffset),t}throw new y("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==Z},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&this.comparePoint(e,n.getNodeLength(e))<=0},containsRange:function(e){return this.intersection(e).equals(e)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();t.setEnd(r,r.length);var i=this.containsRange(t);return t.detach(),i}return this.containsNodeContents(e)},createNodeIterator:function(e,t){return q(this),new b(this,e,t)},getNodes:function(e,t){return q(this),v(this,e,t)},getDocument:function(){return o(this)},collapseBefore:function(e){M(this),this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){M(this),this.setStartAfter(e),this.collapse(!0)},getName:function(){return"DomRange"},equals:function(e){return at.rangesEqual(this,e)},isValid:function(){return I(this)},inspect:function(){return m(this)}},it(at,ot,ut),e.rangePrototype=et.prototype,at.rangeProperties=X,at.RangeIterator=g,at.copyComparisonConstants=nt,at.createPrototypeRange=it,at.inspect=m,at.getRangeDocument=o,at.rangesEqual=function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t
+.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset},e.DomRange=at,e.RangeException=y}),rangy.createModule("WrappedRange",function(e,t){function o(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var i=n.parentElement();n=e.duplicate(),n.collapse(!1);var s=n.parentElement(),o=i==s?i:r.getCommonAncestor(i,s);return o==t?o:r.getCommonAncestor(t,o)}function u(e){return e.compareEndPoints("StartToEnd",e)==0}function a(e,t,n,s){var o=e.duplicate();o.collapse(n);var u=o.parentElement();r.isAncestorOf(t,u,!0)||(u=t);if(!u.canHaveHTML)return new i(u.parentNode,r.getNodeIndex(u));var a=r.getDocument(u).createElement("span"),f,l=n?"StartToStart":"StartToEnd",c,h,p,d;do u.insertBefore(a,a.previousSibling),o.moveToElementText(a);while((f=o.compareEndPoints(l,e))>0&&a.previousSibling);d=a.nextSibling;if(f==-1&&d&&r.isCharacterDataNode(d)){o.setEndPoint(n?"EndToStart":"EndToEnd",e);var v;if(/[\r\n]/.test(d.data)){var m=o.duplicate(),g=m.text.replace(/\r\n/g,"\r").length;v=m.moveStart("character",g);while((f=m.compareEndPoints("StartToEnd",m))==-1)v++,m.moveStart("character",1)}else v=o.text.length;p=new i(d,v)}else c=(s||!n)&&a.previousSibling,h=(s||n)&&a.nextSibling,h&&r.isCharacterDataNode(h)?p=new i(h,0):c&&r.isCharacterDataNode(c)?p=new i(c,c.length):p=new i(u,r.getNodeIndex(a));return a.parentNode.removeChild(a),p}function f(e,t){var n,i,s=e.offset,o=r.getDocument(e.node),u,a,f=o.body.createTextRange(),l=r.isCharacterDataNode(e.node);return l?(n=e.node,i=n.parentNode):(a=e.node.childNodes,n=s12