DEV: refactor placeholder handling from UppyComposerUpload (#29976)
Extracts the textual upload placeholder handle logic from UppyComposerUpload to a new TextareaPlaceholderHandler class, implicitly instantiated by TextareaTextManipulation.
This commit is contained in:
parent
8ce6aa3e7d
commit
e37952c9db
|
@ -200,6 +200,7 @@ export default class ComposerEditor extends Component {
|
|||
@bind
|
||||
setupEditor(textManipulation) {
|
||||
this.textManipulation = textManipulation;
|
||||
this.uppyComposerUpload.textManipulation = textManipulation;
|
||||
|
||||
const input = this.element.querySelector(".d-editor-input");
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "discourse/lib/utilities";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const INDENT_DIRECTION_LEFT = "left";
|
||||
|
@ -51,8 +52,11 @@ export default class TextareaTextManipulation {
|
|||
textarea;
|
||||
$textarea;
|
||||
|
||||
placeholder;
|
||||
|
||||
constructor(owner, { markdownOptions, textarea, eventPrefix = "composer" }) {
|
||||
setOwner(this, owner);
|
||||
this.placeholder = new TextareaPlaceholderHandler(owner, this);
|
||||
|
||||
this.eventPrefix = eventPrefix;
|
||||
this.textarea = textarea;
|
||||
|
@ -829,3 +833,134 @@ export default class TextareaTextManipulation {
|
|||
return this.$textarea.autocomplete(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class TextareaPlaceholderHandler {
|
||||
@service composer;
|
||||
|
||||
textManipulation;
|
||||
|
||||
#placeholders = {};
|
||||
|
||||
constructor(owner, textManipulation) {
|
||||
setOwner(this, owner);
|
||||
|
||||
this.textManipulation = textManipulation;
|
||||
}
|
||||
|
||||
#uploadPlaceholder(file, currentMarkdown) {
|
||||
const clipboard = i18n("clipboard");
|
||||
const uploadFilenamePlaceholder = this.#uploadFilenamePlaceholder(
|
||||
file,
|
||||
currentMarkdown
|
||||
);
|
||||
const filename = uploadFilenamePlaceholder
|
||||
? uploadFilenamePlaceholder
|
||||
: clipboard;
|
||||
|
||||
let placeholder = `[${i18n("uploading_filename", { filename })}]()\n`;
|
||||
if (!this.#cursorIsOnEmptyLine()) {
|
||||
placeholder = `\n${placeholder}`;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
#cursorIsOnEmptyLine() {
|
||||
const selectionStart = this.textManipulation.textarea.selectionStart;
|
||||
return (
|
||||
selectionStart === 0 ||
|
||||
this.textManipulation.value.charAt(selectionStart - 1) === "\n"
|
||||
);
|
||||
}
|
||||
|
||||
#uploadFilenamePlaceholder(file, currentMarkdown) {
|
||||
const filename = this.#filenamePlaceholder(file);
|
||||
|
||||
// when adding two separate files with the same filename search for matching
|
||||
// placeholder already existing in the editor ie [Uploading: test.png…]
|
||||
// and add order nr to the next one: [Uploading: test.png(1)…]
|
||||
const escapedFilename = escapeRegExp(filename);
|
||||
const regexString = `\\[${i18n("uploading_filename", {
|
||||
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
||||
})}\\]\\(\\)`;
|
||||
const globalRegex = new RegExp(regexString, "g");
|
||||
const matchingPlaceholder = currentMarkdown.match(globalRegex);
|
||||
if (matchingPlaceholder) {
|
||||
// get last matching placeholder and its consecutive nr in regex
|
||||
// capturing group and apply +1 to the placeholder
|
||||
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
|
||||
const regex = new RegExp(regexString);
|
||||
const orderNr = regex.exec(lastMatch)[1]
|
||||
? parseInt(regex.exec(lastMatch)[1], 10) + 1
|
||||
: 1;
|
||||
return `${filename}(${orderNr})`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
#filenamePlaceholder(data) {
|
||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
|
||||
insert(file) {
|
||||
const placeholder = this.#uploadPlaceholder(
|
||||
file,
|
||||
this.composer.model.reply
|
||||
);
|
||||
|
||||
this.textManipulation.insertText(placeholder);
|
||||
|
||||
this.#placeholders[file.id] = { uploadPlaceholder: placeholder };
|
||||
}
|
||||
|
||||
progress(file) {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
placeholderData.processingPlaceholder = `[${i18n("processing_filename", {
|
||||
filename: file.name,
|
||||
})}]()\n`;
|
||||
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.uploadPlaceholder,
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
|
||||
// Safari applies user-defined replacements to text inserted programmatically.
|
||||
// One of the most common replacements is ... -> …, so we take care of the case
|
||||
// where that transformation has been applied to the original placeholder
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.uploadPlaceholder.replace("...", "…"),
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
}
|
||||
|
||||
progressComplete(file) {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
this.textManipulation.replaceText(
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
Object.values(this.#placeholders).forEach((data) => {
|
||||
this.textManipulation.replaceText(data.uploadPlaceholder, "");
|
||||
});
|
||||
}
|
||||
|
||||
cancel(file) {
|
||||
if (this.#placeholders[file.id]) {
|
||||
this.textManipulation.replaceText(
|
||||
this.#placeholders[file.id].uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
success(file, markdown) {
|
||||
this.textManipulation.replaceText(
|
||||
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
|||
import { clipboardHelpers } from "discourse/lib/utilities";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class UppyComposerUpload {
|
||||
|
@ -53,12 +52,12 @@ export default class UppyComposerUpload {
|
|||
uploadPreProcessors;
|
||||
uploadHandlers;
|
||||
|
||||
textManipulation;
|
||||
|
||||
#inProgressUploads = [];
|
||||
#bufferedUploadErrors = [];
|
||||
#placeholders = {};
|
||||
#consecutiveImages = [];
|
||||
|
||||
#useUploadPlaceholders = true;
|
||||
#uploadTargetBound = false;
|
||||
#userCancelled = false;
|
||||
|
||||
|
@ -288,7 +287,7 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
file.meta.cancelled = true;
|
||||
this.#removeInProgressUpload(file.id);
|
||||
this.#resetUpload(file, { removePlaceholder: true });
|
||||
this.#resetUpload(file);
|
||||
if (this.#inProgressUploads.length === 0) {
|
||||
this.#userCancelled = true;
|
||||
this.uppyWrapper.uppyInstance.cancelAll();
|
||||
|
@ -335,17 +334,7 @@ export default class UppyComposerUpload {
|
|||
})
|
||||
);
|
||||
|
||||
const placeholder = this.#uploadPlaceholder(file);
|
||||
this.#placeholders[file.id] = {
|
||||
uploadPlaceholder: placeholder,
|
||||
};
|
||||
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:insert-text`,
|
||||
placeholder
|
||||
);
|
||||
}
|
||||
this.textManipulation.placeholder.insert(file);
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:upload-started`,
|
||||
|
@ -366,28 +355,22 @@ export default class UppyComposerUpload {
|
|||
return;
|
||||
}
|
||||
let upload = response.body;
|
||||
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
// Only remove in progress after async resolvers finish:
|
||||
this.#removeInProgressUpload(file.id);
|
||||
cacheShortUploadUrl(upload.short_url, upload);
|
||||
|
||||
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
new ComposerVideoThumbnailUppy(getOwner(this)).generateVideoThumbnail(
|
||||
file,
|
||||
upload.url,
|
||||
() => {
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
}
|
||||
this.textManipulation.placeholder.success(file, markdown);
|
||||
|
||||
this.#resetUpload(file, { removePlaceholder: false });
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:upload-success`,
|
||||
file.name,
|
||||
|
@ -412,18 +395,7 @@ export default class UppyComposerUpload {
|
|||
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
||||
// Do the manual cancelling work only if the user clicked cancel
|
||||
if (this.#userCancelled) {
|
||||
Object.values(this.#placeholders).forEach((data) => {
|
||||
run(() => {
|
||||
if (this.#useUploadPlaceholders) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
data.uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.textManipulation.placeholder.cancelAll();
|
||||
this.#userCancelled = false;
|
||||
this.#reset();
|
||||
|
||||
|
@ -442,7 +414,7 @@ export default class UppyComposerUpload {
|
|||
@bind
|
||||
_handleUploadError(file, error, response) {
|
||||
this.#removeInProgressUpload(file.id);
|
||||
this.#resetUpload(file, { removePlaceholder: true });
|
||||
this.#resetUpload(file);
|
||||
|
||||
file.meta.error = error;
|
||||
|
||||
|
@ -508,36 +480,13 @@ export default class UppyComposerUpload {
|
|||
});
|
||||
|
||||
this.uppyWrapper.onPreProcessProgress((file) => {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
placeholderData.processingPlaceholder = `[${i18n("processing_filename", {
|
||||
filename: file.name,
|
||||
})}]()\n`;
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.uploadPlaceholder,
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
|
||||
// Safari applies user-defined replacements to text inserted programmatically.
|
||||
// One of the most common replacements is ... -> …, so we take care of the case
|
||||
// where that transformation has been applied to the original placeholder
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.uploadPlaceholder.replace("...", "…"),
|
||||
placeholderData.processingPlaceholder
|
||||
);
|
||||
this.textManipulation.placeholder.progress(file);
|
||||
});
|
||||
|
||||
this.uppyWrapper.onPreProcessComplete(
|
||||
(file) => {
|
||||
run(() => {
|
||||
let placeholderData = this.#placeholders[file.id];
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
this.textManipulation.placeholder.progressComplete(file);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
|
@ -554,47 +503,6 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
}
|
||||
|
||||
#uploadFilenamePlaceholder(file) {
|
||||
const filename = this.#filenamePlaceholder(file);
|
||||
|
||||
// when adding two separate files with the same filename search for matching
|
||||
// placeholder already existing in the editor ie [Uploading: test.png…]
|
||||
// and add order nr to the next one: [Uploading: test.png(1)…]
|
||||
const escapedFilename = escapeRegExp(filename);
|
||||
const regexString = `\\[${i18n("uploading_filename", {
|
||||
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
||||
})}\\]\\(\\)`;
|
||||
const globalRegex = new RegExp(regexString, "g");
|
||||
const matchingPlaceholder = this.composerModel.reply.match(globalRegex);
|
||||
if (matchingPlaceholder) {
|
||||
// get last matching placeholder and its consecutive nr in regex
|
||||
// capturing group and apply +1 to the placeholder
|
||||
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
|
||||
const regex = new RegExp(regexString);
|
||||
const orderNr = regex.exec(lastMatch)[1]
|
||||
? parseInt(regex.exec(lastMatch)[1], 10) + 1
|
||||
: 1;
|
||||
return `${filename}(${orderNr})`;
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
#uploadPlaceholder(file) {
|
||||
const clipboard = i18n("clipboard");
|
||||
const uploadFilenamePlaceholder = this.#uploadFilenamePlaceholder(file);
|
||||
const filename = uploadFilenamePlaceholder
|
||||
? uploadFilenamePlaceholder
|
||||
: clipboard;
|
||||
|
||||
let placeholder = `[${i18n("uploading_filename", { filename })}]()\n`;
|
||||
if (!this.#cursorIsOnEmptyLine()) {
|
||||
placeholder = `\n${placeholder}`;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
#useXHRUploads() {
|
||||
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
||||
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
||||
|
@ -620,14 +528,8 @@ export default class UppyComposerUpload {
|
|||
this.#fileInputEl.value = "";
|
||||
}
|
||||
|
||||
#resetUpload(file, opts) {
|
||||
if (opts.removePlaceholder && this.#placeholders[file.id]) {
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:replace-text`,
|
||||
this.#placeholders[file.id].uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
}
|
||||
#resetUpload(file) {
|
||||
this.textManipulation.placeholder.cancel(file);
|
||||
}
|
||||
|
||||
@bind
|
||||
|
@ -704,10 +606,6 @@ export default class UppyComposerUpload {
|
|||
);
|
||||
}
|
||||
|
||||
#filenamePlaceholder(data) {
|
||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
|
||||
#findMatchingUploadHandler(fileName) {
|
||||
return this.uploadHandlers.find((handler) => {
|
||||
const ext = handler.extensions.join("|");
|
||||
|
@ -716,14 +614,6 @@ export default class UppyComposerUpload {
|
|||
});
|
||||
}
|
||||
|
||||
#cursorIsOnEmptyLine() {
|
||||
const textArea = this.#editorEl.querySelector(this.editorInputClass);
|
||||
const selectionStart = textArea.selectionStart;
|
||||
return (
|
||||
selectionStart === 0 || textArea.value.charAt(selectionStart - 1) === "\n"
|
||||
);
|
||||
}
|
||||
|
||||
#autoGridImages() {
|
||||
const reply = this.composerModel.get("reply");
|
||||
const imagesToWrapGrid = new Set(this.#consecutiveImages);
|
||||
|
|
Loading…
Reference in New Issue