Editor: add global-replace/replaceAll commands

* Collateral fix: correctly restore openDialog() after Esc
* refactor html templates
This commit is contained in:
tophf 2015-07-11 17:35:04 +03:00
parent b4eaac4ef9
commit 20141b7bfa
3 changed files with 201 additions and 43 deletions

View File

@ -100,6 +100,18 @@
"message": "Theme",
"description": "Label for the style editor's CSS theme."
},
"confirmNo": {
"message": "No",
"description": "'No' button in a confirm dialog"
},
"confirmStop": {
"message": "Stop",
"description": "'Stop' button in a confirm dialog"
},
"confirmYes": {
"message": "Yes",
"description": "'Yes' button in a confirm dialog"
},
"dbError": {
"message": "An error has occurred using the Stylish database. Would you like to visit a web page with possible solutions?",
"description": "Prompt when a DB error is encountered"
@ -229,6 +241,18 @@
"message": "Show number of styles active for the current site on the toolbar button",
"description": "Label for the checkbox controlling toolbar badge text."
},
"replace": {
"message": "Replace",
"description": "Label before the replace input field in the editor shown on Ctrl-H"
},
"replaceAll": {
"message": "Replace all",
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
},
"replaceWith": {
"message": "Replace with",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"search": {
"message": "Search",
"description": "Label before the search input field in the editor shown on Ctrl-F"

View File

@ -160,6 +160,15 @@
outline: -webkit-focus-ring-color auto 5px;
outline-offset: -2px;
}
.CodeMirror-search-field {
width: 10em;
}
.CodeMirror-jump-field {
width: 5em;
}
.CodeMirror-search-hint {
color: #888;
}
@-webkit-keyframes highlight {
from {
background-color: #ff9;

211
edit.js
View File

@ -10,8 +10,8 @@ var useHistoryBack; // use browser history back when "back to manage" is click
var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"};
// templates
var appliesToTemplate = tHTML('\
var template = {
appliesTo: '\
<li>\
<select name="applies-type" class="applies-type style-contributor">\
<option value="url" i18n-text="appliesUrlOption"></option>\
@ -23,15 +23,13 @@ var appliesToTemplate = tHTML('\
<button class="remove-applies-to" i18n-text="appliesRemove"></button>\
<button class="add-applies-to" i18n-text="appliesAdd"></button>\
</li>\
');
var appliesToEverythingTemplate = tHTML('\
',
appliesToEverything: '\
<li class="applies-to-everything" i18n-html="appliesToEverything")>\
<button class="add-applies-to" i18n-text="appliesSpecify"></button>\
</li>\
');
var sectionTemplate = tHTML('\
',
section: '\
<div>\
<label i18n-text="sectionCode"></label>\
<textarea class="code"></textarea>\
@ -46,12 +44,44 @@ var sectionTemplate = tHTML('\
<button class="add-section" i18n-text="sectionAdd"></button>\
<button class="beautify-section" i18n-text="styleBeautify"></button>\
</div>\
');
var findTemplate = t("search") + ': <input type="text" style="width: 10em" class="CodeMirror-search-field"/>&nbsp;' +
'<span style="color: #888" class="CodeMirror-search-hint">(' + t("searchRegexp") + ')</span>';
var jumpToLineTemplate = t('editGotoLine') + ': <input class="CodeMirror-jump-field" type="text" style="width: 5em"/>';
',
find: '\
<span i18n-text="search">:&nbsp;\
<input type="text" class="CodeMirror-search-field"/>&nbsp;\
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>\
</span>\
',
replace: '\
<span i18n-text="replace">:&nbsp;\
<input type="text" class="CodeMirror-search-field"/>&nbsp;\
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>\
</span>\
',
replaceAll: '\
<span i18n-text="replaceAll">:&nbsp;\
<input type="text" class="CodeMirror-search-field"/>&nbsp;\
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>\
</span>\
',
replaceWith: '\
<span i18n-text="replaceWith">:&nbsp;\
<input type="text" class="CodeMirror-search-field"/>\
</span>\
',
replaceConfirm: '\
<span i18n-text="replace">?&nbsp;\
<button i18n-text="confirmYes"></button>&nbsp;\
<button i18n-text="confirmNo"></button>&nbsp;\
<button i18n-text="confirmStop"></button>\
</span>\
',
jumpToLine: '\
<span i18n-text="editGotoLine">:&nbsp;\
<input class="CodeMirror-jump-field" type="text"/>\
</span>\
'
}
Object.keys(template).forEach(function(name) { template[name] = tHTML(template[name]); });
// make querySelectorAll enumeration code readable
["forEach", "some", "indexOf"].forEach(function(method) {
@ -61,6 +91,12 @@ var jumpToLineTemplate = t('editGotoLine') + ': <input class="CodeMirror-jump-fi
// Chrome pre-34
Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;
Array.prototype.rotate = function(amount) { // negative amount == rotate left
var r = this.slice(-amount, this.length);
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
return r;
}
// reroute handling to nearest editor when keypress resolves to one of these commands
var hotkeyRerouter = {
commands: {
@ -476,25 +512,25 @@ function addAppliesTo(list, name, value) {
}
var e;
if (name && value) {
e = appliesToTemplate.cloneNode(true);
e = template.appliesTo.cloneNode(true);
e.querySelector("[name=applies-type]").value = name;
e.querySelector("[name=applies-value]").value = value;
e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
} else if (showingEverything || list.hasChildNodes()) {
e = appliesToTemplate.cloneNode(true);
e = template.appliesTo.cloneNode(true);
if (list.hasChildNodes()) {
e.querySelector("[name=applies-type]").value = list.querySelector("li:last-child [name='applies-type']").value;
}
e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
} else {
e = appliesToEverythingTemplate.cloneNode(true);
e = template.appliesToEverything.cloneNode(true);
}
e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false);
list.appendChild(e);
}
function addSection(event, section) {
var div = sectionTemplate.cloneNode(true);
var div = template.section.cloneNode(true);
div.querySelector(".applies-to-help").addEventListener("click", showAppliesToHelp, false);
div.querySelector(".remove-section").addEventListener("click", removeSection, false);
div.querySelector(".add-section").addEventListener("click", addSection, false);
@ -589,8 +625,11 @@ function setupGlobalSearch() {
var originalCommand = {
find: CodeMirror.commands.find,
findNext: CodeMirror.commands.findNext,
findPrev: CodeMirror.commands.findPrev
findPrev: CodeMirror.commands.findPrev,
replace: CodeMirror.commands.replace
}
var originalOpenDialog = CodeMirror.prototype.openDialog;
var originalOpenConfirm = CodeMirror.prototype.openConfirm;
var curState; // cm.state.search for last used 'find'
@ -614,33 +653,43 @@ function setupGlobalSearch() {
return cm.state.search;
}
function find(activeCM) {
// temporarily overrides the original openDialog with the provided template's innerHTML
function customizeOpenDialog(cm, template, callback) {
cm.openDialog = function(tmpl, cb, opt) {
// invoke 'callback' and bind 'this' to the original callback
originalOpenDialog.call(cm, template.innerHTML, callback.bind(cb), opt);
};
setTimeout(function() { cm.openDialog = originalOpenDialog; }, 0);
}
function focusClosestCM(activeCM) {
editors.lastActive = activeCM;
var cm = getEditorInSight();
if (cm != activeCM) {
cm.focus();
activeCM = cm;
}
var originalOpenDialog = activeCM.openDialog;
activeCM.openDialog = function(template, callback, options) {
originalOpenDialog.call(activeCM, findTemplate, function(query) {
activeCM.openDialog = originalOpenDialog;
callback(query);
curState = activeCM.state.search;
if (editors.length == 1 || !curState.query) {
return;
return cm;
}
function find(activeCM) {
activeCM = focusClosestCM(activeCM);
customizeOpenDialog(activeCM, template.find, function(query) {
this(query);
curState = activeCM.state.search;
if (editors.length == 1 || !curState.query) {
return;
}
editors.forEach(function(cm) {
if (cm != activeCM) {
cm.execCommand("clearSearch");
updateState(cm, curState);
}
editors.forEach(function(cm) {
if (cm != activeCM) {
cm.execCommand("clearSearch");
updateState(cm, curState);
}
});
if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) == 0) {
findNext(activeCM);
}
}, options);
}
});
if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) == 0) {
findNext(activeCM);
}
});
originalCommand.find(activeCM);
}
@ -714,14 +763,90 @@ function setupGlobalSearch() {
findNext(cm, true);
}
function replace(activeCM, all) {
var queue, query, replacement;
activeCM = focusClosestCM(activeCM);
customizeOpenDialog(activeCM, template[all ? "replaceAll" : "replace"], function(txt) {
query = txt;
customizeOpenDialog(activeCM, template.replaceWith, function(txt) {
replacement = txt;
queue = editors.rotate(-editors.indexOf(activeCM));
all ? editors.forEach(doReplace) : doReplace();
});
this(query);
});
originalCommand.replace(activeCM, all);
function doReplace() {
var cm = queue.shift();
if (!cm) {
if (!all) {
editors.lastActive.focus();
}
return;
}
// hide the first two dialogs (replace, replaceWith)
cm.openDialog = function(tmpl, callback, opt) {
cm.openDialog = function(tmpl, callback, opt) {
cm.openDialog = originalOpenDialog;
if (all) {
callback(replacement);
} else {
doConfirm(cm);
callback(replacement);
if (!cm.getWrapperElement().querySelector(".CodeMirror-dialog")) {
// no dialog == nothing found in the current CM, move to the next
doReplace();
}
}
};
callback(query);
};
originalCommand.replace(cm, all);
}
function doConfirm(cm) {
var wrapAround = false;
var origPos = cm.getCursor();
cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
var ovrCallbacks = callbacks.map(function(callback) {
return function() {
makeSectionVisible(cm);
cm.openConfirm = overrideConfirm;
setTimeout(function() { cm.openConfirm = originalOpenConfirm; }, 0);
var pos = cm.getCursor();
callback();
var cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
wrapAround |= cmp <= 0;
var dlg = cm.getWrapperElement().querySelector(".CodeMirror-dialog");
if (!dlg || cmp == 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
if (dlg) {
dlg.remove();
}
doReplace();
}
}
});
originalOpenConfirm.call(cm, template.replaceConfirm.innerHTML, ovrCallbacks, opt);
};
}
}
function replaceAll(cm) {
replace(cm, true);
}
CodeMirror.commands.find = find;
CodeMirror.commands.findNext = findNext;
CodeMirror.commands.findPrev = findPrev;
CodeMirror.commands.replace = replace;
CodeMirror.commands.replaceAll = replaceAll;
}
function jumpToLine(cm) {
var cur = cm.getCursor();
cm.openDialog(jumpToLineTemplate, function(str) {
cm.openDialog(template.jumpToLine.innerHTML, function(str) {
var m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
@ -1087,7 +1212,7 @@ function validate() {
// validate the regexps
if (document.querySelectorAll(".applies-to-list").some(function(list) {
return list.childNodes.some(function(li) {
if (li.className == appliesToEverythingTemplate.className) {
if (li.className == template.appliesToEverything.className) {
return false;
}
var valueElement = li.querySelector("[name=applies-value]");
@ -1153,7 +1278,7 @@ function getSections() {
function getMeta(e) {
var meta = {};
e.querySelector(".applies-to-list").childNodes.forEach(function(li) {
if (li.className == appliesToEverythingTemplate.className) {
if (li.className == template.appliesToEverything.className) {
return;
}
var type = li.querySelector("[name=applies-type]").value;