fixes for global search/replace

* Enter key in input fields:
  * [Find mode] closes the dialog and focuses the matched text
  * [Replace mode] performs a single replace action

* Undo button checks the new CM changeGeneration and disables itself
  if the changes were already undone

* doUndo focuses the visible undo button
This commit is contained in:
tophf 2018-01-01 09:56:41 +03:00
parent 671b276532
commit b2100ea853

View File

@ -47,7 +47,6 @@ onDOMready().then(() => {
tally: null, tally: null,
originalFocus: null, originalFocus: null,
undo: null,
undoHistory: [], undoHistory: [],
searchInApplies: !document.documentElement.classList.contains('usercss'), searchInApplies: !document.documentElement.classList.contains('usercss'),
@ -59,11 +58,19 @@ onDOMready().then(() => {
const ACTIONS = { const ACTIONS = {
key: { key: {
'Enter': event => { 'Enter': event => {
if (event.target.closest(focusAccessibility.ELEMENTS.join(','))) { switch (document.activeElement) {
return false; case state.input:
} if (state.dialog.dataset.type === 'find') {
destroyDialog();
doSearch({canAdvance: false}); doSearch({canAdvance: false});
destroyDialog();
return;
}
// fallthrough
case state.input2:
doReplace();
return;
}
return !event.target.closest(focusAccessibility.ELEMENTS.join(','));
}, },
'Esc': () => { 'Esc': () => {
destroyDialog({restoreFocus: true}); destroyDialog({restoreFocus: true});
@ -109,6 +116,19 @@ onDOMready().then(() => {
if (action && action.call(el, event) !== false) { if (action && action.call(el, event) !== false) {
event.preventDefault(); event.preventDefault();
} }
},
onfocusout() {
if (!state.dialog.contains(document.activeElement)) {
state.dialog.addEventListener('focusin', EVENTS.onfocusin);
state.dialog.removeEventListener('focusout', EVENTS.onfocusout);
}
},
onfocusin() {
state.dialog.addEventListener('focusout', EVENTS.onfocusout);
state.dialog.removeEventListener('focusin', EVENTS.onfocusin);
trimUndoHistory();
enableUndoButton(state.undoHistory.length);
if (state.find) doSearch({canAdvance: false});
} }
}; };
@ -298,6 +318,7 @@ onDOMready().then(() => {
function doReplace() { function doReplace() {
initState({initReplace: true}); initState({initReplace: true});
const cm = state.cmStart; const cm = state.cmStart;
const generation = cm.changeGeneration();
const pos = getContinuationPos({cm, reverse: true}); const pos = getContinuationPos({cm, reverse: true});
const cursor = doReplaceInEditor({cm, pos}); const cursor = doReplaceInEditor({cm, pos});
if (!cursor) { if (!cursor) {
@ -315,7 +336,7 @@ onDOMready().then(() => {
if (cm.curOp) cm.endOperation(); if (cm.curOp) cm.endOperation();
if (cursor) { if (cursor) {
state.undoHistory.push([cm]); state.undoHistory.push([[cm, generation]]);
enableUndoButton(true); enableUndoButton(true);
} }
} }
@ -324,10 +345,11 @@ onDOMready().then(() => {
function doReplaceAll() { function doReplaceAll() {
initState({initReplace: true}); initState({initReplace: true});
clearMarker(); clearMarker();
const generations = new Map(editors.map(cm => [cm, cm.changeGeneration()]));
const found = editors.filter(cm => doReplaceInEditor({cm, all: true})); const found = editors.filter(cm => doReplaceInEditor({cm, all: true}));
if (found.length) { if (found.length) {
state.lastFind = null; state.lastFind = null;
state.undoHistory.push(found); state.undoHistory.push(found.map(cm => [cm, generations.get(cm)]));
enableUndoButton(true); enableUndoButton(true);
doSearch({canAdvance: false}); doSearch({canAdvance: false});
} }
@ -368,8 +390,8 @@ onDOMready().then(() => {
function doUndo() { function doUndo() {
let undoneSome; let undoneSome;
saveWindowScrollPos(); saveWindowScrollPos();
for (const cm of state.undoHistory.pop() || []) { for (const [cm, generation] of state.undoHistory.pop() || []) {
if (document.body.contains(cm.display.wrapper) && !cm.isClean()) { if (document.body.contains(cm.display.wrapper) && !cm.isClean(generation)) {
cm.undo(); cm.undo();
cm.getAllMarks().forEach(marker => cm.getAllMarks().forEach(marker =>
marker !== state.marker && marker !== state.marker &&
@ -379,7 +401,11 @@ onDOMready().then(() => {
} }
} }
enableUndoButton(state.undoHistory.length); enableUndoButton(state.undoHistory.length);
(state.undoHistory.length ? state.undo : state.input).focus(); if (state.undoHistory.length) {
focusUndoButton();
} else {
state.input.focus();
}
if (undoneSome) { if (undoneSome) {
state.lastFind = null; state.lastFind = null;
restoreWindowScrollPos(); restoreWindowScrollPos();
@ -529,6 +555,7 @@ onDOMready().then(() => {
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true); const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true);
Object.assign(dialog, DIALOG_PROPS.dialog); Object.assign(dialog, DIALOG_PROPS.dialog);
dialog.addEventListener('focusout', EVENTS.onfocusout);
dialog.dataset.type = type; dialog.dataset.type = type;
const content = $('[data-type="content"]', dialog); const content = $('[data-type="content"]', dialog);
@ -646,6 +673,7 @@ onDOMready().then(() => {
} }
} }
function enableUndoButton(enabled) { function enableUndoButton(enabled) {
if (state.dialog && state.dialog.dataset.type === 'replace') { if (state.dialog && state.dialog.dataset.type === 'replace') {
for (const el of $$('[data-action="undo"]', state.dialog)) { for (const el of $$('[data-action="undo"]', state.dialog)) {
@ -654,6 +682,16 @@ onDOMready().then(() => {
} }
} }
function focusUndoButton() {
for (const btn of $$('[data-action="undo"]', state.dialog)) {
if (getComputedStyle(btn).display !== 'none') {
btn.focus();
break;
}
}
}
//endregion //endregion
//region Utility //region Utility
@ -787,6 +825,20 @@ onDOMready().then(() => {
} }
function trimUndoHistory() {
const history = state.undoHistory;
for (let last; (last = history[history.length - 1]);) {
const undoables = last.filter(([cm, generation]) =>
document.body.contains(cm.display.wrapper) && !cm.isClean(generation));
if (undoables.length) {
history[history.length - 1] = undoables;
break;
}
history.length--;
}
}
function focusNoScroll(el) { function focusNoScroll(el) {
if (el) { if (el) {
saveWindowScrollPos(); saveWindowScrollPos();