From 338fa585d3af28597e451ef69aae8dfd73f03965 Mon Sep 17 00:00:00 2001 From: Mathieu Saby Date: Sun, 23 Sep 2018 20:42:33 +0200 Subject: [PATCH] Add menu for searching and replacing Add a new menu "Replace" in "Edit Cells" > "Common Transforms" and a new interaction box for entering parameters. Under the hood this is a wrapper around GREL replace() function. The behavior is the same as replace() except the automatic backslah escaping of "Replace with" parameter. The 2 main parameters in the box are 2 textaras : "Find" and "Replace with" 2 cumulative options are available for "Find" parameter : "Case insensitive" and "Regular Expression" 1 option is available for "Replace with" parameter : "Do not escape backslash automatically" A validity control of the pattern is made when "Regular expression" is checked. --- .../modules/core/langs/translation-en.json | 13 +- .../views/data-table/menu-edit-cells.js | 193 +++++++++++++++--- .../views/data-table/replace-dialog.html | 45 ++++ 3 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 main/webapp/modules/core/scripts/views/data-table/replace-dialog.html diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json index dffddfe4d..223daf114 100644 --- a/main/webapp/modules/core/langs/translation-en.json +++ b/main/webapp/modules/core/langs/translation-en.json @@ -633,6 +633,7 @@ "to-text": "To text", "blank-out": "To null", "blank-out-empty": "To empty string", + "replace": "Replace", "fill-down": "Fill down", "blank-down": "Blank down", "split-cells": "Split multi-valued cells", @@ -674,7 +675,17 @@ "rows": "rows", "records": "records", "show": "Show", - "hide": "Hide" + "hide": "Hide", + "text-to-find": "Find:", + "replacement-text": "Replace with:", + "case-insensitive": "case insensitive", + "finding-info1": "Leave blank to add the remplacement string after each character.", + "finding-info2": "Check \"regular expression\" to find special characters (new lines, tabulations...) or complex patterns.", + "finding-options": "Finding options:", + "replacement-options": "Replacement options:", + "replacement-info": "If \"regular expression\" option is checked and finding pattern contains groups delimited with parentheses, $0 will return the complete string matching the pattern, and $1, $2... the 1st, 2d... group.", + "replace-dont-escape": "Do not escape backslash (\\) automatically.", + "warning-regex": "Invalid regular expression." }, "core-buttons": { "cancel": "Cancel", diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js index 8298bdc2e..da563f467 100644 --- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js +++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js @@ -23,8 +23,8 @@ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @@ -36,7 +36,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { Refine.postCoreProcess( "text-transform", { - columnName: column.name, + columnName: column.name, onError: onError, repeat: repeat, repeatCount: repeatCount @@ -53,7 +53,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var elmts = DOM.bind(frame); elmts.dialogHeader.text($.i18n._('core-views')["custom-text-trans"]+" " + column.name); - + elmts.or_views_errorOn.text($.i18n._('core-views')["on-error"]); elmts.or_views_keepOr.text($.i18n._('core-views')["keep-or"]); elmts.or_views_setBlank.text($.i18n._('core-views')["set-blank"]); @@ -61,7 +61,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { elmts.or_views_reTrans.text($.i18n._('core-views')["re-trans"]); elmts.or_views_timesChang.text($.i18n._('core-views')["times-chang"]); elmts.okButton.html($.i18n._('core-buttons')["ok"]); - elmts.cancelButton.text($.i18n._('core-buttons')["cancel"]); + elmts.cancelButton.text($.i18n._('core-buttons')["cancel"]); var level = DialogSystem.showDialog(frame); var dismiss = function() { DialogSystem.dismissUntil(level - 1); }; @@ -96,7 +96,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var doFillDown = function() { Refine.postCoreProcess( - "fill-down", + "fill-down", { columnName: column.name }, @@ -107,7 +107,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var doBlankDown = function() { Refine.postCoreProcess( - "blank-down", + "blank-down", { columnName: column.name }, @@ -120,7 +120,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var separator = window.prompt($.i18n._('core-views')["enter-separator"], ", "); if (separator !== null) { Refine.postCoreProcess( - "join-multi-value-cells", + "join-multi-value-cells", { columnName: column.name, keyColumnName: theProject.columnModel.keyColumnName, @@ -132,12 +132,145 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { } }; + var doReplace = function() { + function isValidPattern(p) { + // check if a string can be used as a regexp pattern + // parameters : + // p : a string without beginning and trailing "/" + + // we need a manual check for unescaped / + // the GREL replace function cannot contain a pattern with unescaped / + // but javascript Regexp accepts it and auto escape it + var pos = p.replace(/\\\//g,'').indexOf("/"); + if (pos != -1) { + alert($.i18n._('core-views')["warning-regex"] + " : " + p); + return 0;} + try { + var pattern = new RegExp(p); + return 1; + } catch (e) { + alert($.i18n._('core-views')["warning-regex"] + " : " + p); + return 0;} + } + function escapeInputString(s) { + // if the textarea input is used as a plain string + // user input | result after escaping + // 4 characters : A\tA | 5 characters : A\\tA + // 4 characters : A\nA | 5 characters : A\\nA + // 1 characters : \ | 2 characters : \\ + // 1 character : ' | 2 characters : \' + // 1 character : " | 2 characters : \" + // new line | 2 characters : \n + // tab | 2 characters : \t + // parameters : + // s : a string from a textarea + if (typeof s != 'string') return "" + // convert new lines and tabs manually typed in the textarea into \n and \t and suppress other printable characters, escape \ ' and " + return s.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/[\x00-\x1F\x80-\x9F]/g, '').replace(/'/g, "\\'").replace(/"/g, '\\"') + function hex(c) { + // non used + var v = '0' + c.charCodeAt(0).toString(16); + return '\\x' + v.substr(v.length - 2); + } + } + function escapeInputRegex(s) { + // if the textarea input is used as a regex pattern + // suppress new lines and tabs manually typed in the textarea, and other non printable characters + // no need to escape \ or / + // parameters : + // s : a string from a textarea + return (typeof s == 'string') ? s.replace(/[\x00-\x1F\x80-\x9F]/g, '') : "" + } + function stringToRegex(s) { + // converts a plain string to regex, in order to use the i flag + // escaping is needed to force the regex processor to interpret every character litteraly + // parameters : + // s : a string from a textarea, preprocessed with escapeInputString + if (typeof s != 'string') return "" + // no need to escape \ : already escaped by escapeInputString + var EscapeCharsRegex = /[-|{}()[\]^$+*?.]/g; + // FIXME escaping of / directly by adding / or \/ or // in EscapeCharsRegex don't work... + return s.replace(EscapeCharsRegex, '\\$&').replace(/\//g, '\\/'); + } + function escapeReplacementString(dont_escape,s) { + // in replacement string, the GREL replace function handle in a specific way the Java special escape sequences + // cf https://docs.oracle.com/javase/tutorial/java/data/characters.html + // Escape Sequence | Java documentation | GREL replace function + // \t | tab | tabs + // \n | new line | new line + // \r | carriage return | + // \b | backspace | b + // \f | formfeed | f + // \' | ' | '' + // \" | " | "" + // \\ | \ | \ + // GREL replace function returns an error if a \ is not followed by an other character + // it could be unexpected for the user, so the replace menu escape the replacement string, but gives the possibility to choose the default behavior + // if dont_escape = 0, a complete escaping is done + // if dont_escape = 1, no escaping is done, but a slight cleaning + // parameters : + // replace_dont_escape : 0 or 1 + // s : a string from a textarea + if (typeof s != 'string') {return "";} + var temp = s; + if (dont_escape == 0) {temp = temp.replace(/\\/g, '\\\\\\\\');} + // convert newlines and tabs manually typed in the textarea into \n and \t and suppress other non printable characters + return temp.replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/[\x00-\x1F\x80-\x9F]/g,''); + } + var frame = $(DOM.loadHTML("core", "scripts/views/data-table/replace-dialog.html")); + var elmts = DOM.bind(frame); + elmts.dialogHeader.text($.i18n._('core-views')["replace"]); + elmts.or_views_text_to_find.text($.i18n._('core-views')["text-to-find"]); + elmts.or_views_replacement.text($.i18n._('core-views')["replacement-text"]); + elmts.or_views_finding_options.text($.i18n._('core-views')["finding-options"]); + elmts.or_views_finding_info1.text($.i18n._('core-views')["finding-info1"]); + elmts.or_views_finding_info2.text($.i18n._('core-views')["finding-info2"]); + elmts.or_views_replacement_info.text($.i18n._('core-views')["replacement-info"]); + elmts.or_views_replacement_options.text($.i18n._('core-views')["replacement-options"]); + elmts.or_views_find_regExp.text($.i18n._('core-views')["reg-exp"]); + elmts.or_views_find_case_insensitive.text($.i18n._('core-views')["case-insensitive"]); + elmts.or_views_replace_dont_escape.text($.i18n._('core-views')["replace-dont-escape"]); + elmts.okButton.html($.i18n._('core-buttons')["ok"]); + elmts.cancelButton.text($.i18n._('core-buttons')["cancel"]); + var level = DialogSystem.showDialog(frame); + var dismiss = function() { DialogSystem.dismissUntil(level - 1); }; + elmts.cancelButton.click(dismiss); + elmts.okButton.click(function() { + var text_to_find = elmts.text_to_findTextarea[0].value; + var replacement_text = elmts.replacementTextarea[0].value; + var replace_dont_escape = elmts.replace_dont_escapeInput[0].checked; + var find_regex = elmts.find_regexInput[0].checked; + var find_case_insensitive = elmts.find_case_insensitiveInput[0].checked; + replacement_text = escapeReplacementString(replace_dont_escape, replacement_text) + if (find_regex) { + text_to_find = escapeInputRegex(text_to_find); + if (!isValidPattern (text_to_find)) {return} + text_to_find = "/"+text_to_find+"/"; + if (find_case_insensitive) {text_to_find = text_to_find + "i"} + } + else { + text_to_find = escapeInputString(text_to_find); + if (find_case_insensitive) { + text_to_find = stringToRegex(text_to_find); + if (!isValidPattern (text_to_find)) {return} + text_to_find = "/"+text_to_find+"/i"; + } + else { + text_to_find = '"'+text_to_find+'"'; + } + } + expression = 'value.replace('+text_to_find+',"'+replacement_text+'")'; + doTextTransform(expression, "keep-original", false, ""); + dismiss(); + }); + }; + var doSplitMultiValueCells = function() { var frame = $(DOM.loadHTML("core", "scripts/views/data-table/split-multi-valued-cells-dialog.html")); var elmts = DOM.bind(frame); elmts.dialogHeader.text($.i18n._('core-views')["split-cells"]); - + elmts.or_views_howSplit.text($.i18n._('core-views')["how-split-cells"]); elmts.or_views_bySep.text($.i18n._('core-views')["by-sep"]); elmts.or_views_separator.text($.i18n._('core-views')["separator"]); @@ -175,9 +308,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var a = JSON.parse(s); var lengths = []; - $.each(a, function(i,n) { + $.each(a, function(i,n) { if (typeof n == "number") { - lengths.push(n); + lengths.push(n); } }); @@ -187,7 +320,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { } config.fieldLengths = JSON.stringify(lengths); - + } catch (e) { alert($.i18n._('core-views')["warning-format"]); return; @@ -195,7 +328,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { } Refine.postCoreProcess( - "split-multi-value-cells", + "split-multi-value-cells", config, null, { rowsChanged: true } @@ -273,6 +406,12 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { id: "core/to-empty", label: $.i18n._('core-views')["blank-out-empty"], click: function() { doTextTransform("\"\"", "keep-original", false, ""); } + }, + {}, + { + id: "core/replace", + label: $.i18n._('core-views')["replace"], + click: doReplace } ] }, @@ -311,7 +450,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var elmts = DOM.bind(dialog); var level = DialogSystem.showDialog(dialog); - + elmts.dialogHeader.html($.i18n._('core-views')["transp-cell"]); elmts.or_views_fromCol.html($.i18n._('core-views')["from-col"]); elmts.or_views_toCol.html($.i18n._('core-views')["to-col"]); @@ -329,7 +468,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { elmts.or_views_fillOther.html($.i18n._('core-views')["fill-other"]); elmts.okButton.html($.i18n._('core-buttons')["transpose"]); elmts.cancelButton.html($.i18n._('core-buttons')["cancel"]); - + var dismiss = function() { DialogSystem.dismissUntil(level - 1); }; @@ -344,7 +483,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { ignoreBlankCells: elmts.ignoreBlankCellsCheckbox[0].checked, fillDown: elmts.fillDownCheckbox[0].checked }; - + var mode = dialog.find('input[name="transpose-dialog-column-choices"]:checked')[0].value; if (mode == "2") { config.keyColumnName = $.trim(elmts.keyColumnNameInput[0].value); @@ -370,7 +509,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { } Refine.postCoreProcess( - "transpose-columns-into-rows", + "transpose-columns-into-rows", config, null, { modelsChanged: true }, @@ -405,7 +544,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { var column2 = columns[k]; $('