Add cursor tests, fix algorithm and extract method.

This commit is contained in:
Claas Augner 2016-12-29 09:54:15 +01:00
parent ba2db48dbb
commit 43c1dd82f6
No known key found for this signature in database
GPG Key ID: 63E8BCF5EB1A35AE
2 changed files with 129 additions and 26 deletions

View File

@ -534,34 +534,48 @@ export default Ember.Component.extend({
const textarea = this.$('textarea.d-editor-input')[0]; const textarea = this.$('textarea.d-editor-input')[0];
// Remember cursor/selection. // Determine post-replace selection.
const selectionStart = textarea.selectionStart; const newSelection = this._determinePostReplaceSelection({
const selectionEnd = textarea.selectionEnd; selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
const needleEnd = needleStart + oldVal.length; needle: { start: needleStart, end: needleStart + oldVal.length },
const replacementEnd = needleStart + newVal.length; replacement: { start: needleStart, end: needleStart + newVal.length }
});
// Replace value (side effect: cursor at end). // Replace value (side effect: cursor at the end).
this.set('value', val.replace(oldVal, newVal)); this.set('value', val.replace(oldVal, newVal));
// Determine cursor/selection.
let newSelectionStart, newSelectionEnd;
if (selectionEnd <= needleEnd) {
// Selection before needle.
newSelectionStart = selectionStart;
newSelectionEnd = selectionEnd;
} else if (selectionStart < needleEnd) {
// Selection within needle.
newSelectionStart = replacementEnd;
newSelectionEnd = replacementEnd;
} else {
// Selection behind needle.
const lengthDiff = replacementEnd - needleStart;
newSelectionStart = selectionStart + lengthDiff;
newSelectionEnd = selectionEnd + lengthDiff;
}
// Restore cursor. // Restore cursor.
this._selectText(newSelectionStart, newSelectionEnd - newSelectionStart); this._selectText(newSelection.start, newSelection.end - newSelection.start);
},
_determinePostReplaceSelection({ selection, needle, replacement }) {
const diff = (replacement.end - replacement.start) - (needle.end - needle.start);
if (selection.end <= needle.start) {
// Selection ends (and starts) before needle.
return { start: selection.start, end: selection.end };
} else if (selection.start <= needle.start) {
// Selection starts before needle...
if (selection.end < needle.end) {
// ... and ends inside needle.
return { start: selection.start, end: needle.start };
} else {
// ... and spans needle completely.
return { start: selection.start, end: selection.end + diff };
}
} else if (selection.start < needle.end) {
// Selection starts inside needle...
if (selection.end <= needle.end) {
// ... and ends inside needle.
return { start: replacement.end, end: replacement.end };
} else {
// ... and spans end of needle.
return { start: replacement.end, end: selection.end + diff };
}
} else {
// Selection starts (and ends) behind needle.
return { start: selection.start + diff, end: selection.end + diff };
}
}, },
_addText(sel, text) { _addText(sel, text) {

View File

@ -770,7 +770,96 @@ testCase("replace-text event", function(assert, textarea) {
andThen(() => { andThen(() => {
assert.equal(this.get('value'), 'red yellow blue'); assert.equal(this.get('value'), 'red yellow blue');
assert.equal(textarea.selectionStart, 10);
assert.equal(textarea.selectionEnd, 10);
}); });
}); });
(() => {
// Tests to check cursor/selection after replace-text event.
const BEFORE = 'red green blue';
const NEEDLE = 'green';
const REPLACE = 'yellow';
const AFTER = BEFORE.replace(NEEDLE, REPLACE);
const CASES = [
{
description: 'cursor at start remains there',
before: [0, 0],
after: [0, 0]
},{
description: 'cursor before needle becomes cursor before replacement',
before: [BEFORE.indexOf(NEEDLE), 0],
after: [AFTER.indexOf(REPLACE), 0]
},{
description: 'cursor at needle start + 1 moves behind replacement',
before: [BEFORE.indexOf(NEEDLE) + 1, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0]
},{
description: 'cursor at needle end - 1 stays behind replacement',
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0]
},{
description: 'cursor behind needle becomes cursor behind replacement',
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0]
},{
description: 'cursor at end remains there',
before: [BEFORE.length, 0],
after: [AFTER.length, 0]
},{
description: 'selection spanning needle start becomes selection until replacement start',
before: [BEFORE.indexOf(NEEDLE) - 1, 2],
after: [AFTER.indexOf(REPLACE) - 1, 1]
},{
description: 'selection spanning needle end becomes selection from replacement end',
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 2],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 1]
},{
description: 'selection spanning needle becomes selection spanning replacement',
before: [BEFORE.indexOf(NEEDLE) - 1, NEEDLE.length + 2],
after: [AFTER.indexOf(REPLACE) - 1, REPLACE.length + 2]
},{
description: 'complete selection remains complete',
before: [0, BEFORE.length],
after: [0, AFTER.length]
}
];
function setSelection(textarea, [start, len]) {
textarea.selectionStart = start;
textarea.selectionEnd = start + len;
}
function getSelection(textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd
return [start, end - start];
}
function formatTextWithSelection(text, [start, len]) {
return [
'"',
text.substr(0, start),
'<',
text.substr(start, len),
'>',
text.substr(start+len),
'"',
].join('');
}
for (let i = 0; i < CASES.length; i++) {
const CASE = CASES[i];
testCase(`replace-text event: ${CASE.description}`, function(assert, textarea) {
this.set('value', BEFORE);
setSelection(textarea, CASE.before);
andThen(() => {
this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow');
});
andThen(() => {
let expect = formatTextWithSelection(AFTER, CASE.after);
let actual = formatTextWithSelection(this.get('value'), getSelection(textarea));
assert.equal(actual, expect);
});
});
}
})();