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:
Renato Atilio 2024-12-05 15:07:55 -03:00 committed by GitHub
parent 8ce6aa3e7d
commit e37952c9db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 152 additions and 126 deletions

View File

@ -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");

View File

@ -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
);
}
}

View File

@ -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);