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
|
@bind
|
||||||
setupEditor(textManipulation) {
|
setupEditor(textManipulation) {
|
||||||
this.textManipulation = textManipulation;
|
this.textManipulation = textManipulation;
|
||||||
|
this.uppyComposerUpload.textManipulation = textManipulation;
|
||||||
|
|
||||||
const input = this.element.querySelector(".d-editor-input");
|
const input = this.element.querySelector(".d-editor-input");
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from "discourse/lib/utilities";
|
} from "discourse/lib/utilities";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
const INDENT_DIRECTION_LEFT = "left";
|
const INDENT_DIRECTION_LEFT = "left";
|
||||||
|
@ -51,8 +52,11 @@ export default class TextareaTextManipulation {
|
||||||
textarea;
|
textarea;
|
||||||
$textarea;
|
$textarea;
|
||||||
|
|
||||||
|
placeholder;
|
||||||
|
|
||||||
constructor(owner, { markdownOptions, textarea, eventPrefix = "composer" }) {
|
constructor(owner, { markdownOptions, textarea, eventPrefix = "composer" }) {
|
||||||
setOwner(this, owner);
|
setOwner(this, owner);
|
||||||
|
this.placeholder = new TextareaPlaceholderHandler(owner, this);
|
||||||
|
|
||||||
this.eventPrefix = eventPrefix;
|
this.eventPrefix = eventPrefix;
|
||||||
this.textarea = textarea;
|
this.textarea = textarea;
|
||||||
|
@ -829,3 +833,134 @@ export default class TextareaTextManipulation {
|
||||||
return this.$textarea.autocomplete(...arguments);
|
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 { clipboardHelpers } from "discourse/lib/utilities";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export default class UppyComposerUpload {
|
export default class UppyComposerUpload {
|
||||||
|
@ -53,12 +52,12 @@ export default class UppyComposerUpload {
|
||||||
uploadPreProcessors;
|
uploadPreProcessors;
|
||||||
uploadHandlers;
|
uploadHandlers;
|
||||||
|
|
||||||
|
textManipulation;
|
||||||
|
|
||||||
#inProgressUploads = [];
|
#inProgressUploads = [];
|
||||||
#bufferedUploadErrors = [];
|
#bufferedUploadErrors = [];
|
||||||
#placeholders = {};
|
|
||||||
#consecutiveImages = [];
|
#consecutiveImages = [];
|
||||||
|
|
||||||
#useUploadPlaceholders = true;
|
|
||||||
#uploadTargetBound = false;
|
#uploadTargetBound = false;
|
||||||
#userCancelled = false;
|
#userCancelled = false;
|
||||||
|
|
||||||
|
@ -288,7 +287,7 @@ export default class UppyComposerUpload {
|
||||||
);
|
);
|
||||||
file.meta.cancelled = true;
|
file.meta.cancelled = true;
|
||||||
this.#removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
this.#resetUpload(file, { removePlaceholder: true });
|
this.#resetUpload(file);
|
||||||
if (this.#inProgressUploads.length === 0) {
|
if (this.#inProgressUploads.length === 0) {
|
||||||
this.#userCancelled = true;
|
this.#userCancelled = true;
|
||||||
this.uppyWrapper.uppyInstance.cancelAll();
|
this.uppyWrapper.uppyInstance.cancelAll();
|
||||||
|
@ -335,17 +334,7 @@ export default class UppyComposerUpload {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholder = this.#uploadPlaceholder(file);
|
this.textManipulation.placeholder.insert(file);
|
||||||
this.#placeholders[file.id] = {
|
|
||||||
uploadPlaceholder: placeholder,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.#useUploadPlaceholders) {
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`${this.composerEventPrefix}:insert-text`,
|
|
||||||
placeholder
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:upload-started`,
|
`${this.composerEventPrefix}:upload-started`,
|
||||||
|
@ -366,28 +355,22 @@ export default class UppyComposerUpload {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let upload = response.body;
|
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:
|
// Only remove in progress after async resolvers finish:
|
||||||
this.#removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
cacheShortUploadUrl(upload.short_url, upload);
|
cacheShortUploadUrl(upload.short_url, upload);
|
||||||
|
|
||||||
|
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||||
|
(md, resolver) => resolver(upload) || md,
|
||||||
|
getUploadMarkdown(upload)
|
||||||
|
);
|
||||||
|
|
||||||
new ComposerVideoThumbnailUppy(getOwner(this)).generateVideoThumbnail(
|
new ComposerVideoThumbnailUppy(getOwner(this)).generateVideoThumbnail(
|
||||||
file,
|
file,
|
||||||
upload.url,
|
upload.url,
|
||||||
() => {
|
() => {
|
||||||
if (this.#useUploadPlaceholders) {
|
this.textManipulation.placeholder.success(file, markdown);
|
||||||
this.appEvents.trigger(
|
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
|
||||||
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
|
||||||
markdown
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#resetUpload(file, { removePlaceholder: false });
|
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:upload-success`,
|
`${this.composerEventPrefix}:upload-success`,
|
||||||
file.name,
|
file.name,
|
||||||
|
@ -412,18 +395,7 @@ export default class UppyComposerUpload {
|
||||||
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
||||||
// Do the manual cancelling work only if the user clicked cancel
|
// Do the manual cancelling work only if the user clicked cancel
|
||||||
if (this.#userCancelled) {
|
if (this.#userCancelled) {
|
||||||
Object.values(this.#placeholders).forEach((data) => {
|
this.textManipulation.placeholder.cancelAll();
|
||||||
run(() => {
|
|
||||||
if (this.#useUploadPlaceholders) {
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
|
||||||
data.uploadPlaceholder,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#userCancelled = false;
|
this.#userCancelled = false;
|
||||||
this.#reset();
|
this.#reset();
|
||||||
|
|
||||||
|
@ -442,7 +414,7 @@ export default class UppyComposerUpload {
|
||||||
@bind
|
@bind
|
||||||
_handleUploadError(file, error, response) {
|
_handleUploadError(file, error, response) {
|
||||||
this.#removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
this.#resetUpload(file, { removePlaceholder: true });
|
this.#resetUpload(file);
|
||||||
|
|
||||||
file.meta.error = error;
|
file.meta.error = error;
|
||||||
|
|
||||||
|
@ -508,36 +480,13 @@ export default class UppyComposerUpload {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uppyWrapper.onPreProcessProgress((file) => {
|
this.uppyWrapper.onPreProcessProgress((file) => {
|
||||||
let placeholderData = this.#placeholders[file.id];
|
this.textManipulation.placeholder.progress(file);
|
||||||
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.uppyWrapper.onPreProcessComplete(
|
this.uppyWrapper.onPreProcessComplete(
|
||||||
(file) => {
|
(file) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
let placeholderData = this.#placeholders[file.id];
|
this.textManipulation.placeholder.progressComplete(file);
|
||||||
this.appEvents.trigger(
|
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
|
||||||
placeholderData.processingPlaceholder,
|
|
||||||
placeholderData.uploadPlaceholder
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
|
@ -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() {
|
#useXHRUploads() {
|
||||||
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
||||||
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
||||||
|
@ -620,14 +528,8 @@ export default class UppyComposerUpload {
|
||||||
this.#fileInputEl.value = "";
|
this.#fileInputEl.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
#resetUpload(file, opts) {
|
#resetUpload(file) {
|
||||||
if (opts.removePlaceholder && this.#placeholders[file.id]) {
|
this.textManipulation.placeholder.cancel(file);
|
||||||
this.appEvents.trigger(
|
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
|
||||||
this.#placeholders[file.id].uploadPlaceholder,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -704,10 +606,6 @@ export default class UppyComposerUpload {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#filenamePlaceholder(data) {
|
|
||||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#findMatchingUploadHandler(fileName) {
|
#findMatchingUploadHandler(fileName) {
|
||||||
return this.uploadHandlers.find((handler) => {
|
return this.uploadHandlers.find((handler) => {
|
||||||
const ext = handler.extensions.join("|");
|
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() {
|
#autoGridImages() {
|
||||||
const reply = this.composerModel.get("reply");
|
const reply = this.composerModel.get("reply");
|
||||||
const imagesToWrapGrid = new Set(this.#consecutiveImages);
|
const imagesToWrapGrid = new Set(this.#consecutiveImages);
|
||||||
|
|
Loading…
Reference in New Issue