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.
This commit is contained in:
Mathieu Saby 2018-09-23 20:42:33 +02:00
parent 2676c60fb2
commit 338fa585d3
3 changed files with 223 additions and 28 deletions

View File

@ -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",

View File

@ -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 | <blank>
// \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];
$('<option>').attr("value", k - j + 1).text(column2.name).appendTo(elmts.toColumnSelect);
}
$('<option>')
.attr("value", "-1")
.attr("selected", "true")
@ -443,20 +582,20 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
}
}
};
var doKeyValueColumnize = function() {
var dialog = $(DOM.loadHTML("core", "scripts/views/data-table/key-value-columnize.html"));
var elmts = DOM.bind(dialog);
var level = DialogSystem.showDialog(dialog);
elmts.dialogHeader.html($.i18n._('core-views')["columnize"]);
elmts.or_views_keyCol.html($.i18n._('core-views')["key-col"]);
elmts.or_views_valCol.html($.i18n._('core-views')["val-col"]);
elmts.or_views_noteCol.html($.i18n._('core-views')["note-col"]);
elmts.okButton.html($.i18n._('core-buttons')["ok"]);
elmts.cancelButton.html($.i18n._('core-buttons')["cancel"]);
var dismiss = function() {
DialogSystem.dismissUntil(level - 1);
};
@ -476,7 +615,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
alert($.i18n._('core-views')["sel-col-val"]);
return;
}
var noteColumnName = elmts.noteColumnSelect[0].value;
if (noteColumnName != null) {
if (noteColumnName == config.keyColumnName ||
@ -488,7 +627,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
}
Refine.postCoreProcess(
"key-value-columnize",
"key-value-columnize",
config,
null,
{ modelsChanged: true }
@ -499,18 +638,18 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
var valueColumnIndex = -1;
for (var i = 0; i < columns.length; i++) {
var column2 = columns[i];
var keyOption = $('<option>').attr("value", column2.name).text(column2.name).appendTo(elmts.keyColumnSelect);
if (column2.name == column.name) {
keyOption.attr("selected", "true");
valueColumnIndex = i + 1;
}
var valueOption = $('<option>').attr("value", column2.name).text(column2.name).appendTo(elmts.valueColumnSelect);
if (i === valueColumnIndex) {
valueOption.attr("selected", "true");
}
$('<option>').attr("value", column2.name).text(column2.name).appendTo(elmts.noteColumnSelect);
}
};
@ -534,4 +673,4 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
}
]
);
});
});

View File

@ -0,0 +1,45 @@
<div class="dialog-frame" style="width: 600px;">
<div class="dialog-border">
<div class="dialog-header" bind="dialogHeader"></div>
<div class="dialog-body" bind="dialogBody">
<div class="grid-layout layout-looser layout-full">
<table>
<tr>
<td>
<div class="grid-layout layout-tighter">
<table>
<tr>
<td >
<h3 bind="or_views_text_to_find"></h3>
<p bind="or_views_finding_options"></p>
<input type="checkbox" bind="find_case_insensitiveInput" id="$find-case" />
<label for="$find-case" bind="or_views_find_case_insensitive"></label>
<input type="checkbox" bind="find_regexInput" id="$find-regex" />
<label for="$find-regex" bind="or_views_find_regExp"></label>
<p bind="or_views_finding_info1"></p>
<p bind="or_views_finding_info2"></p>
<textarea style="width: 100%;" bind="text_to_findTextarea"></textarea>
</td>
</tr>
<tr>
<td>
<h3 bind="or_views_replacement"></h3>
<p bind="or_views_replacement_options"></p>
<input type="checkbox" bind="replace_dont_escapeInput" id="$replace-escape" />
<label for="$replace-escape" bind="or_views_replace_dont_escape"></label>
<p bind="or_views_replacement_info"></p>
<textarea style="width: 100%;" bind="replacementTextarea"></textarea>
</td>
</tr>
</table>
</div>
</td>
</table>
</div>
</div>
<div class="dialog-footer" bind="dialogFooter">
<button class="button" bind="okButton"></button>
<button class="button" bind="cancelButton"></button>
</div>
</div>
</div>