mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
Merge pull request #4630 from caugner/feature-preserve-cursor-in-editor-upload
FEATURE: Preserve cursor in editor upload
This commit is contained in:
@@ -12,6 +12,7 @@ import { emojiSearch } from 'pretty-text/emoji';
|
|||||||
import { emojiUrlFor } from 'discourse/lib/text';
|
import { emojiUrlFor } from 'discourse/lib/text';
|
||||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||||
|
import { determinePostReplaceSelection } from 'discourse/lib/utilities';
|
||||||
import deprecated from 'discourse-common/lib/deprecated';
|
import deprecated from 'discourse-common/lib/deprecated';
|
||||||
|
|
||||||
// Our head can be a static string or a function that returns a string
|
// Our head can be a static string or a function that returns a string
|
||||||
@@ -525,11 +526,27 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
_replaceText(oldVal, newVal) {
|
_replaceText(oldVal, newVal) {
|
||||||
const val = this.get('value');
|
const val = this.get('value');
|
||||||
const loc = val.indexOf(oldVal);
|
const needleStart = val.indexOf(oldVal);
|
||||||
if (loc !== -1) {
|
|
||||||
this.set('value', val.replace(oldVal, newVal));
|
if (needleStart === -1) {
|
||||||
this._selectText(loc + newVal.length, 0);
|
// Nothing to replace.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textarea = this.$('textarea.d-editor-input')[0];
|
||||||
|
|
||||||
|
// Determine post-replace selection.
|
||||||
|
const newSelection = determinePostReplaceSelection({
|
||||||
|
selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
|
||||||
|
needle: { start: needleStart, end: needleStart + oldVal.length },
|
||||||
|
replacement: { start: needleStart, end: needleStart + newVal.length }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace value (side effect: cursor at the end).
|
||||||
|
this.set('value', val.replace(oldVal, newVal));
|
||||||
|
|
||||||
|
// Restore cursor.
|
||||||
|
this._selectText(newSelection.start, newSelection.end - newSelection.start);
|
||||||
},
|
},
|
||||||
|
|
||||||
_addText(sel, text) {
|
_addText(sel, text) {
|
||||||
|
|||||||
@@ -300,5 +300,35 @@ export function defaultHomepage() {
|
|||||||
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This prevents a mini racer crash
|
// This prevents a mini racer crash
|
||||||
export default {};
|
export default {};
|
||||||
|
|||||||
@@ -760,7 +760,7 @@ componentTest('emoji', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testCase("replace-text event", function(assert, textarea) {
|
testCase("replace-text event", function(assert) {
|
||||||
|
|
||||||
this.set('value', "red green blue");
|
this.set('value', "red green blue");
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user