From ea382deaafec74fab5651e5af0a13f477a4ef975 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 23 Apr 2014 11:22:44 +0800 Subject: [PATCH] MDL-45207 atto_equation: Handle multiple equations on the same line --- .../moodle-atto_equation-button-debug.js | 138 ++++++++++++++---- .../moodle-atto_equation-button-min.js | 4 +- .../moodle-atto_equation-button.js | 138 ++++++++++++++---- .../equation/yui/src/button/js/button.js | 127 ++++++++++++---- .../equation/yui/src/button/meta/button.json | 3 +- 5 files changed, 318 insertions(+), 92 deletions(-) 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 fa1adb2c69b..2392b18b7a7 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 @@ -128,10 +128,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. * The source equation we are editing in the text. * * @property _sourceEquation - * @type String + * @type Object * @private */ - _sourceEquation: '', + _sourceEquation: null, /** * A reference to the tab focus set on each group. @@ -144,6 +144,25 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. */ _groupFocus: null, + /** + * Regular Expression patterns used to pick out the equations in a String. + * + * @property _equationPatterns + * @type Array + * @private + */ + _equationPatterns: [ + // We use space or not space because . does not match new lines. + // $$ blah $$. + /\$\$([\S\s]+?)\$\$/, + // E.g. "\( blah \)". + /\\\(([\S\s]+?)\\\)/, + // E.g. "\[ blah \]". + /\\\[([\S\s]+?)\\\]/, + // E.g. "[tex] blah [/tex]". + /\[tex\]([\S\s]+?)\[\/tex\]/ + ], + initializer: function() { this._groupFocus = {}; @@ -225,39 +244,87 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), + selection = this.get('host').getSelection(), text, - equation, - patterns = [], i; + returnValue = false; + + this.sourceEquation = null; // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } - text = Y.one(selectedNode).get('text'); - // We use space or not space because . does not match new lines. - // $$ blah $$. - patterns.push(/\$\$([\S\s]*)\$\$/); - // E.g. "\( blah \)". - patterns.push(/\\\(([\S\s]*)\\\)/); - // E.g. "\[ blah \]". - patterns.push(/\\\[([\S\s]*)\\\]/); - // E.g. "[tex] blah [/tex]". - patterns.push(/\[tex\]([\S\s]*)\[\/tex\]/); - - for (i = 0; i < patterns.length; i++) { - pattern = patterns[i]; - equation = pattern.exec(text); - if (equation && equation.length) { - // Remember the inner match so we can replace it later. - this.sourceEquation = equation = equation[1]; - - return equation; - } + // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. + if (!selection || selection.length === 0) { + return false; } + selection = selection[0]; - this.sourceEquation = ''; - return false; + text = Y.one(selectedNode).get('text'); + + // For each of these patterns we have a RegExp which captures the inner component of the equation but also includes the delimiters. + // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire + // equation including delimiters and returning one entry per match of the whole equation. + // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the + // match results. + Y.Array.find(this._equationPatterns, function(pattern) { + // For each pattern in turn, find all whole matches (including the delimiters). + var patternMatches = text.match(new RegExp(pattern.source, "g")); + + if (patternMatches && patternMatches.length) { + // This pattern matches at least once. See if this pattern matches our current position. + // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent + // searches which is the required behaviour of this function. + return Y.Array.find(patternMatches, function(match) { + // Check each occurrence of this match. + var startIndex = 0; + while(text.indexOf(match, startIndex) !== -1) { + // Determine whether the cursor is in the current occurrence of this string. + // Note: We do not support a selection exceeding the bounds of an equation. + var startOuter = text.indexOf(match, startIndex), + endOuter = startOuter + match.length, + startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), + endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); + + if (startMatch && endMatch) { + // This match is in our current position - fetch the innerMatch data. + var innerMatch = match.match(pattern); + if (innerMatch && innerMatch.length) { + // We need the start and end of the inner match for later. + var startInner = text.indexOf(innerMatch[1], startOuter), + endInner = startInner + innerMatch[1].length; + + // We'll be returning the inner match for use in the editor itself. + returnValue = innerMatch[1]; + + // Save all data for later. + this.sourceEquation = { + // Outer match data. + startOuterPosition: startOuter, + endOuterPosition: endOuter, + outerMatch: match, + + // Inner match data. + startInnerPosition: startInner, + endInnerPosition: endInner, + innerMatch: innerMatch + }; + + // This breaks out of both Y.Array.find functions. + return true; + } + } + + // Update the startIndex to match the end of the current match so that we can continue hunting + // for further matches. + startIndex = endOuter; + } + }, this); + } + }, this); + + return returnValue; }, /** @@ -287,13 +354,15 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. if (value !== '') { host.setSelection(this._currentSelection); - if (this.sourceEquation.length) { + if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); + newText = text.slice(0, this.sourceEquation.startInnerPosition) + + value + + text.slice(this.sourceEquation.endInnerPosition); - text = text.replace(this.sourceEquation, value); - selectedNode.set('text', text); + selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; @@ -627,4 +696,13 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. }); -}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-core-event", "io", "event-valuechange", "tabview"]}); +}, '@VERSION@', { + "requires": [ + "moodle-editor_atto-plugin", + "moodle-core-event", + "io", + "event-valuechange", + "tabview", + "array-extras" + ] +}); 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 2e681ed435f..8db10819068 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,2 +1,2 @@ -YUI.add("moodle-atto_equation-button",function(e,t){var n="atto_equation",r="atto_equation",i={EQUATION_TEXT:"atto_equation_equation",EQUATION_PREVIEW:"atto_equation_preview",SUBMIT:"atto_equation_submit",LIBRARY:"atto_equation_library",LIBRARY_GROUPS:"atto_equation_groups",LIBRARY_GROUP_PREFIX:"atto_equation_group"},s={LIBRARY:"."+i.LIBRARY,LIBRARY_GROUP:"."+i.LIBRARY_GROUPS+" > div > div",EQUATION_TEXT:"."+i.EQUATION_TEXT,EQUATION_PREVIEW:"."+i.EQUATION_PREVIEW,SUBMIT:"."+i.SUBMIT,LIBRARY_BUTTON:"."+i.LIBRARY+" button"},o={START:"\\(",END:"\\)"},u={FORM:'
{{{library}}}

',LIBRARY:'
{{#each library}}
{{#split "\n" elements}}{{/split}}
{{/each}}
'};e.namespace("M.atto_equation").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_currentSelection:null,_lastCursorPos:0,_content:null,_sourceEquation:"",_groupFocus:null,initializer:function(){this._groupFocus={},this.get("texfilteractive")&&(this.addButton({icon:"e/math",callback:this._displayDialogue}),this.get("host").on("atto:selectionchanged",function(){this._resolveEquation()?this.highlightButtons():this.unHighlightButtons()},this),this.editor.all("tex").each(function(t){var n=e.Node.create(""+o.START+" "+t.get("text")+" "+o.END+"");t.replace(n)}))},_displayDialogue:function(){this._currentSelection=this.get("host").getSelection();if(this._currentSelection===!1)return;var t=this.getDialogue({headerContent:M.util.get_string("pluginname",n),focusAfterHide:!0,width:600}),r=this._getDialogueContent();t.set("bodyContent",r);var i=r.one(s.LIBRARY),o=new e.TabView({srcNode:i});o.render(),t.show(),e.fire(M.core.event.FILTER_CONTENT_UPDATED,{nodes:new e.NodeList(t.get("boundingBox"))});var u=this._resolveEquation();u&&r.one(s.EQUATION_TEXT).set("text",u),this._updatePreview(!1)},_resolveEquation:function(){var t=this.get("host").getSelectionParentNode(),n,r,i=[],s;if(!t)return!1;n=e.one(t).get("text"),i.push(/\$\$([\S\s]*)\$\$/),i.push(/\\\(([\S\s]*)\\\)/),i.push(/\\\[([\S\s]*)\\\]/),i.push(/\[tex\]([\S\s]*)\[\/tex\]/);for(s=0;s0)a-=1;c=/[a-zA-Z\{\}]/;while(c.test(r.charAt(a))&&a=r.size()&&(s=0),o=r.item(s),this._setGroupTabFocus(n,o),o.focus()},_setGroupTabFocus:function(e,t){var n=e.generateID();typeof this._groupFocus[n]!="undefined"&&this._groupFocus[n].setAttribute("tabindex","-1"),this._groupFocus[n]=t,t.setAttribute("tabindex",0),e.setAttribute("aria-activedescendant",t.generateID())},_selectLibraryItem:function(e){var t=e.currentTarget.getAttribute("data-tex");e.preventDefault( -),this._setGroupTabFocus(e.currentTarget.get("parentNode"),e.currentTarget),input=e.currentTarget.ancestor(".atto_form").one("textarea"),value=input.get("value"),value=value.substring(0,this._lastCursorPos)+t+value.substring(this._lastCursorPos,value.length),input.set("value",value),input.focus();var n=this._lastCursorPos+t.length,r=input.getDOMNode();if(typeof r.selectionStart=="number")r.selectionStart=r.selectionEnd=n;else if(typeof r.createTextRange!="undefined"){var i=r.createTextRange();i.moveToPoint(n),i.select()}this._updatePreview(!1)},_getLibraryContent:function(){var t=e.Handlebars.compile(u.LIBRARY),r=this.get("library"),s="";e.Handlebars.registerHelper("split",function(e,t,n){var r,i,s;if(typeof e=="undefined"||typeof t=="undefined")return"";s="",r=t.trim().split(e);while(r.length>0)i=r.shift().trim(),s+=n.fn(i);return s}),s=t({elementid:this.get("host").get("elementid"),component:n,library:r,CSS:i,DELIMITERS:o});var a=M.cfg.wwwroot+"/lib/editor/atto/plugins/equation/ajax.php",f={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"filtertext",text:s};return preview=e.io(a,{sync:!0,data:f,method:"POST"}),preview.status===200&&(s=preview.responseText),s}},{ATTRS:{texfilteractive:{value:!1},contextid:{value:null},library:{value:{}},texdocsurl:{value:null}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin","moodle-core-event","io","event-valuechange","tabview"]}); +YUI.add("moodle-atto_equation-button",function(e,t){var n="atto_equation",r="atto_equation",i={EQUATION_TEXT:"atto_equation_equation",EQUATION_PREVIEW:"atto_equation_preview",SUBMIT:"atto_equation_submit",LIBRARY:"atto_equation_library",LIBRARY_GROUPS:"atto_equation_groups",LIBRARY_GROUP_PREFIX:"atto_equation_group"},s={LIBRARY:"."+i.LIBRARY,LIBRARY_GROUP:"."+i.LIBRARY_GROUPS+" > div > div",EQUATION_TEXT:"."+i.EQUATION_TEXT,EQUATION_PREVIEW:"."+i.EQUATION_PREVIEW,SUBMIT:"."+i.SUBMIT,LIBRARY_BUTTON:"."+i.LIBRARY+" button"},o={START:"\\(",END:"\\)"},u={FORM:'
{{{library}}}

',LIBRARY:'
{{#each library}}
{{#split "\n" elements}}{{/split}}
{{/each}}
'};e.namespace("M.atto_equation").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_currentSelection:null,_lastCursorPos:0,_content:null,_sourceEquation:null,_groupFocus:null,_equationPatterns:[/\$\$([\S\s]+?)\$\$/,/\\\(([\S\s]+?)\\\)/,/\\\[([\S\s]+?)\\\]/,/\[tex\]([\S\s]+?)\[\/tex\]/],initializer:function(){this._groupFocus={},this.get("texfilteractive")&&(this.addButton({icon:"e/math",callback:this._displayDialogue}),this.get("host").on("atto:selectionchanged",function(){this._resolveEquation()?this.highlightButtons():this.unHighlightButtons()},this),this.editor.all("tex").each(function(t){var n=e.Node.create(""+o.START+" "+t.get("text")+" "+o.END+"");t.replace(n)}))},_displayDialogue:function(){this._currentSelection=this.get("host").getSelection();if(this._currentSelection===!1)return;var t=this.getDialogue({headerContent:M.util.get_string("pluginname",n),focusAfterHide:!0,width:600}),r=this._getDialogueContent();t.set("bodyContent",r);var i=r.one(s.LIBRARY),o=new e.TabView({srcNode:i});o.render(),t.show(),e.fire(M.core.event.FILTER_CONTENT_UPDATED,{nodes:new e.NodeList(t.get("boundingBox"))});var u=this._resolveEquation();u&&r.one(s.EQUATION_TEXT).set("text",u),this._updatePreview(!1)},_resolveEquation:function(){var t=this.get("host").getSelectionParentNode(),n=this.get("host").getSelection(),r,i=!1;return this.sourceEquation=null,t?!n||n.length===0?!1:(n=n[0],r=e.one(t).get("text"),e.Array.find(this._equationPatterns,function(t){var s=r.match(new RegExp(t.source,"g"));if(s&&s.length)return e.Array.find(s,function(e){var s=0;while(r.indexOf(e,s)!==-1){var o=r.indexOf(e,s),u=o+e.length,a=n.startOffset>=o&&n.startOffseto;if(a&&f){var l=e.match(t);if(l&&l.length){var c=r.indexOf(l[1],o),h=c+l[1].length;return i=l[1],this.sourceEquation={startOuterPosition:o,endOuterPosition:u,outerMatch:e,startInnerPosition:c,endInnerPosition:h,innerMatch:l},!0}}s=u}},this)},this),i):!1},_setEquation:function(t){var n,r,i,s,u;u=this.get("host"),t.preventDefault(),this.getDialogue({focusAfterHide:null}).hide(),n=t.currentTarget.ancestor(".atto_form").one("textarea"),s=n.get("value"),s!==""&&(u.setSelection(this._currentSelection),this.sourceEquation?(r=e.one(u.getSelectionParentNode()),i=r.get("text"),newText=i.slice(0,this.sourceEquation.startInnerPosition)+s+i.slice(this.sourceEquation.endInnerPosition),r.set("text",newText)):(s=o.START+" "+s+" "+o.END,u.insertContentAtFocusPoint(s)),this.markUpdated())},_throttle:function(e,t){var n=null;return function(){var r=this,i=arguments;clearTimeout(n),n=setTimeout(function(){e.apply(r,i)},t)}},_updatePreview:function(t){var n=this._content.one(s.EQUATION_TEXT),r=n.get("value"),i,u,a=n.get("selectionStart"),f="",l="\\square ",c,h;t&&t.preventDefault(),a||(a=0);while(r.charAt(a)==="\\"&&a>0)a-=1;c=/[a-zA-Z\{\}]/;while(c.test(r.charAt(a))&&a=r.size()&&(s=0),o=r.item(s),this._setGroupTabFocus(n,o),o.focus()},_setGroupTabFocus:function(e,t){var n=e.generateID();typeof this._groupFocus[n]!="undefined"&&this._groupFocus[n].setAttribute("tabindex","-1"),this._groupFocus[n]=t,t.setAttribute("tabindex",0),e.setAttribute("aria-activedescendant",t.generateID())},_selectLibraryItem:function(e){var t=e.currentTarget.getAttribute("data-tex");e.preventDefault(),this._setGroupTabFocus(e.currentTarget.get("parentNode"),e.currentTarget),input=e.currentTarget.ancestor(".atto_form").one("textarea"),value=input.get("value"),value=value.substring(0,this._lastCursorPos)+t+value.substring(this._lastCursorPos,value.length),input.set("value",value),input.focus();var n=this._lastCursorPos+t.length,r=input.getDOMNode();if(typeof r.selectionStart=="number")r.selectionStart=r.selectionEnd=n;else if(typeof r.createTextRange!="undefined"){var i=r.createTextRange();i.moveToPoint(n),i.select()}this._updatePreview(!1)},_getLibraryContent:function(){var t=e.Handlebars.compile(u.LIBRARY),r=this.get("library"),s="";e.Handlebars.registerHelper("split",function(e,t,n){var r,i,s;if(typeof e=="undefined"||typeof t=="undefined")return"";s="",r=t.trim().split(e);while(r.length>0)i=r.shift().trim(),s+=n.fn(i);return s}),s=t({elementid:this.get("host").get("elementid"),component:n,library:r,CSS:i,DELIMITERS:o});var a=M.cfg.wwwroot+"/lib/editor/atto/plugins/equation/ajax.php",f={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"filtertext",text:s};return preview=e.io(a,{sync:!0,data:f,method:"POST"}),preview.status===200&&(s=preview.responseText),s}},{ATTRS:{texfilteractive:{value:!1},contextid:{value:null},library:{value:{}},texdocsurl:{value:null}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin","moodle-core-event","io","event-valuechange","tabview","array-extras"]}); diff --git a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js index b47ca72d3ec..e6bd1110fe1 100644 --- a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js +++ b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js @@ -128,10 +128,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. * The source equation we are editing in the text. * * @property _sourceEquation - * @type String + * @type Object * @private */ - _sourceEquation: '', + _sourceEquation: null, /** * A reference to the tab focus set on each group. @@ -144,6 +144,25 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. */ _groupFocus: null, + /** + * Regular Expression patterns used to pick out the equations in a String. + * + * @property _equationPatterns + * @type Array + * @private + */ + _equationPatterns: [ + // We use space or not space because . does not match new lines. + // $$ blah $$. + /\$\$([\S\s]+?)\$\$/, + // E.g. "\( blah \)". + /\\\(([\S\s]+?)\\\)/, + // E.g. "\[ blah \]". + /\\\[([\S\s]+?)\\\]/, + // E.g. "[tex] blah [/tex]". + /\[tex\]([\S\s]+?)\[\/tex\]/ + ], + initializer: function() { this._groupFocus = {}; @@ -225,39 +244,87 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), + selection = this.get('host').getSelection(), text, - equation, - patterns = [], i; + returnValue = false; + + this.sourceEquation = null; // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } - text = Y.one(selectedNode).get('text'); - // We use space or not space because . does not match new lines. - // $$ blah $$. - patterns.push(/\$\$([\S\s]*)\$\$/); - // E.g. "\( blah \)". - patterns.push(/\\\(([\S\s]*)\\\)/); - // E.g. "\[ blah \]". - patterns.push(/\\\[([\S\s]*)\\\]/); - // E.g. "[tex] blah [/tex]". - patterns.push(/\[tex\]([\S\s]*)\[\/tex\]/); - - for (i = 0; i < patterns.length; i++) { - pattern = patterns[i]; - equation = pattern.exec(text); - if (equation && equation.length) { - // Remember the inner match so we can replace it later. - this.sourceEquation = equation = equation[1]; - - return equation; - } + // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. + if (!selection || selection.length === 0) { + return false; } + selection = selection[0]; - this.sourceEquation = ''; - return false; + text = Y.one(selectedNode).get('text'); + + // For each of these patterns we have a RegExp which captures the inner component of the equation but also includes the delimiters. + // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire + // equation including delimiters and returning one entry per match of the whole equation. + // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the + // match results. + Y.Array.find(this._equationPatterns, function(pattern) { + // For each pattern in turn, find all whole matches (including the delimiters). + var patternMatches = text.match(new RegExp(pattern.source, "g")); + + if (patternMatches && patternMatches.length) { + // This pattern matches at least once. See if this pattern matches our current position. + // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent + // searches which is the required behaviour of this function. + return Y.Array.find(patternMatches, function(match) { + // Check each occurrence of this match. + var startIndex = 0; + while(text.indexOf(match, startIndex) !== -1) { + // Determine whether the cursor is in the current occurrence of this string. + // Note: We do not support a selection exceeding the bounds of an equation. + var startOuter = text.indexOf(match, startIndex), + endOuter = startOuter + match.length, + startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), + endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); + + if (startMatch && endMatch) { + // This match is in our current position - fetch the innerMatch data. + var innerMatch = match.match(pattern); + if (innerMatch && innerMatch.length) { + // We need the start and end of the inner match for later. + var startInner = text.indexOf(innerMatch[1], startOuter), + endInner = startInner + innerMatch[1].length; + + // We'll be returning the inner match for use in the editor itself. + returnValue = innerMatch[1]; + + // Save all data for later. + this.sourceEquation = { + // Outer match data. + startOuterPosition: startOuter, + endOuterPosition: endOuter, + outerMatch: match, + + // Inner match data. + startInnerPosition: startInner, + endInnerPosition: endInner, + innerMatch: innerMatch + }; + + // This breaks out of both Y.Array.find functions. + return true; + } + } + + // Update the startIndex to match the end of the current match so that we can continue hunting + // for further matches. + startIndex = endOuter; + } + }, this); + } + }, this); + + return returnValue; }, /** @@ -287,13 +354,15 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. if (value !== '') { host.setSelection(this._currentSelection); - if (this.sourceEquation.length) { + if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); + newText = text.slice(0, this.sourceEquation.startInnerPosition) + + value + + text.slice(this.sourceEquation.endInnerPosition); - text = text.replace(this.sourceEquation, value); - selectedNode.set('text', text); + selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; @@ -625,4 +694,13 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. }); -}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-core-event", "io", "event-valuechange", "tabview"]}); +}, '@VERSION@', { + "requires": [ + "moodle-editor_atto-plugin", + "moodle-core-event", + "io", + "event-valuechange", + "tabview", + "array-extras" + ] +}); diff --git a/lib/editor/atto/plugins/equation/yui/src/button/js/button.js b/lib/editor/atto/plugins/equation/yui/src/button/js/button.js index 2bb932d6708..e57555d87d5 100644 --- a/lib/editor/atto/plugins/equation/yui/src/button/js/button.js +++ b/lib/editor/atto/plugins/equation/yui/src/button/js/button.js @@ -126,10 +126,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. * The source equation we are editing in the text. * * @property _sourceEquation - * @type String + * @type Object * @private */ - _sourceEquation: '', + _sourceEquation: null, /** * A reference to the tab focus set on each group. @@ -142,6 +142,25 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. */ _groupFocus: null, + /** + * Regular Expression patterns used to pick out the equations in a String. + * + * @property _equationPatterns + * @type Array + * @private + */ + _equationPatterns: [ + // We use space or not space because . does not match new lines. + // $$ blah $$. + /\$\$([\S\s]+?)\$\$/, + // E.g. "\( blah \)". + /\\\(([\S\s]+?)\\\)/, + // E.g. "\[ blah \]". + /\\\[([\S\s]+?)\\\]/, + // E.g. "[tex] blah [/tex]". + /\[tex\]([\S\s]+?)\[\/tex\]/ + ], + initializer: function() { this._groupFocus = {}; @@ -223,39 +242,87 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), + selection = this.get('host').getSelection(), text, - equation, - patterns = [], i; + returnValue = false; + + this.sourceEquation = null; // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } - text = Y.one(selectedNode).get('text'); - // We use space or not space because . does not match new lines. - // $$ blah $$. - patterns.push(/\$\$([\S\s]*)\$\$/); - // E.g. "\( blah \)". - patterns.push(/\\\(([\S\s]*)\\\)/); - // E.g. "\[ blah \]". - patterns.push(/\\\[([\S\s]*)\\\]/); - // E.g. "[tex] blah [/tex]". - patterns.push(/\[tex\]([\S\s]*)\[\/tex\]/); - - for (i = 0; i < patterns.length; i++) { - pattern = patterns[i]; - equation = pattern.exec(text); - if (equation && equation.length) { - // Remember the inner match so we can replace it later. - this.sourceEquation = equation = equation[1]; - - return equation; - } + // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. + if (!selection || selection.length === 0) { + return false; } + selection = selection[0]; - this.sourceEquation = ''; - return false; + text = Y.one(selectedNode).get('text'); + + // For each of these patterns we have a RegExp which captures the inner component of the equation but also includes the delimiters. + // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire + // equation including delimiters and returning one entry per match of the whole equation. + // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the + // match results. + Y.Array.find(this._equationPatterns, function(pattern) { + // For each pattern in turn, find all whole matches (including the delimiters). + var patternMatches = text.match(new RegExp(pattern.source, "g")); + + if (patternMatches && patternMatches.length) { + // This pattern matches at least once. See if this pattern matches our current position. + // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent + // searches which is the required behaviour of this function. + return Y.Array.find(patternMatches, function(match) { + // Check each occurrence of this match. + var startIndex = 0; + while(text.indexOf(match, startIndex) !== -1) { + // Determine whether the cursor is in the current occurrence of this string. + // Note: We do not support a selection exceeding the bounds of an equation. + var startOuter = text.indexOf(match, startIndex), + endOuter = startOuter + match.length, + startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), + endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); + + if (startMatch && endMatch) { + // This match is in our current position - fetch the innerMatch data. + var innerMatch = match.match(pattern); + if (innerMatch && innerMatch.length) { + // We need the start and end of the inner match for later. + var startInner = text.indexOf(innerMatch[1], startOuter), + endInner = startInner + innerMatch[1].length; + + // We'll be returning the inner match for use in the editor itself. + returnValue = innerMatch[1]; + + // Save all data for later. + this.sourceEquation = { + // Outer match data. + startOuterPosition: startOuter, + endOuterPosition: endOuter, + outerMatch: match, + + // Inner match data. + startInnerPosition: startInner, + endInnerPosition: endInner, + innerMatch: innerMatch + }; + + // This breaks out of both Y.Array.find functions. + return true; + } + } + + // Update the startIndex to match the end of the current match so that we can continue hunting + // for further matches. + startIndex = endOuter; + } + }, this); + } + }, this); + + return returnValue; }, /** @@ -285,13 +352,15 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto. if (value !== '') { host.setSelection(this._currentSelection); - if (this.sourceEquation.length) { + if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); + newText = text.slice(0, this.sourceEquation.startInnerPosition) + + value + + text.slice(this.sourceEquation.endInnerPosition); - text = text.replace(this.sourceEquation, value); - selectedNode.set('text', text); + selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; diff --git a/lib/editor/atto/plugins/equation/yui/src/button/meta/button.json b/lib/editor/atto/plugins/equation/yui/src/button/meta/button.json index 6103cb338de..144f41791d8 100644 --- a/lib/editor/atto/plugins/equation/yui/src/button/meta/button.json +++ b/lib/editor/atto/plugins/equation/yui/src/button/meta/button.json @@ -5,7 +5,8 @@ "moodle-core-event", "io", "event-valuechange", - "tabview" + "tabview", + "array-extras" ] } }