DEV: refactor `composer` references on composer-container/-editor (#29629)
Most of it is removing the ComposerContainer > ComposerEditor indirect references to the composer service, so ComposerEditor now deals with the service directly. Form template was moved from DEditor to ComposerEditor.
This commit is contained in:
parent
8fd2980685
commit
6e5d4ee492
|
@ -102,36 +102,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ComposerEditor
|
<ComposerEditor>
|
||||||
@topic={{this.composer.topic}}
|
|
||||||
@composer={{this.composer.model}}
|
|
||||||
@lastValidatedAt={{this.composer.lastValidatedAt}}
|
|
||||||
@canWhisper={{this.composer.canWhisper}}
|
|
||||||
@storeToolbarState={{this.composer.storeToolbarState}}
|
|
||||||
@onPopupMenuAction={{this.composer.onPopupMenuAction}}
|
|
||||||
@showUploadModal={{route-action "showUploadSelector"}}
|
|
||||||
@popupMenuOptions={{this.composer.popupMenuOptions}}
|
|
||||||
@draftStatus={{this.composer.model.draftStatus}}
|
|
||||||
@isUploading={{this.composer.isUploading}}
|
|
||||||
@isProcessingUpload={{this.composer.isProcessingUpload}}
|
|
||||||
@allowUpload={{this.composer.allowUpload}}
|
|
||||||
@uploadIcon={{this.composer.uploadIcon}}
|
|
||||||
@isCancellable={{this.composer.isCancellable}}
|
|
||||||
@uploadProgress={{this.composer.uploadProgress}}
|
|
||||||
@groupsMentioned={{this.composer.groupsMentioned}}
|
|
||||||
@cannotSeeMention={{this.composer.cannotSeeMention}}
|
|
||||||
@hereMention={{this.composer.hereMention}}
|
|
||||||
@importQuote={{this.composer.importQuote}}
|
|
||||||
@togglePreview={{this.composer.togglePreview}}
|
|
||||||
@processPreview={{this.composer.showPreview}}
|
|
||||||
@showToolbar={{this.composer.showToolbar}}
|
|
||||||
@afterRefresh={{this.composer.afterRefresh}}
|
|
||||||
@focusTarget={{this.composer.focusTarget}}
|
|
||||||
@disableTextarea={{this.composer.disableTextarea}}
|
|
||||||
@formTemplateIds={{this.composer.formTemplateIds}}
|
|
||||||
@formTemplateInitialValues={{this.composer.formTemplateInitialValues}}
|
|
||||||
@onSelectFormTemplate={{this.composer.onSelectFormTemplate}}
|
|
||||||
>
|
|
||||||
<div class="composer-fields">
|
<div class="composer-fields">
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="before-composer-fields"
|
@name="before-composer-fields"
|
||||||
|
|
|
@ -1,38 +1,56 @@
|
||||||
<DEditor
|
{{#if this.showFormTemplateForm}}
|
||||||
@value={{this.composer.reply}}
|
<div class="d-editor">
|
||||||
@placeholder={{this.replyPlaceholder}}
|
<div class="d-editor-container">
|
||||||
@previewUpdated={{action "previewUpdated"}}
|
<div class="d-editor-textarea-column">
|
||||||
@markdownOptions={{this.markdownOptions}}
|
{{yield}}
|
||||||
@extraButtons={{action "extraButtons"}}
|
|
||||||
@importQuote={{this.importQuote}}
|
|
||||||
@showUploadModal={{this.showUploadModal}}
|
|
||||||
@togglePreview={{this.togglePreview}}
|
|
||||||
@processPreview={{this.processPreview}}
|
|
||||||
@validation={{this.validation}}
|
|
||||||
@loading={{this.composer.loading}}
|
|
||||||
@forcePreview={{this.forcePreview}}
|
|
||||||
@showLink={{this.showLink}}
|
|
||||||
@composerEvents={{true}}
|
|
||||||
@onExpandPopupMenuOptions={{action "onExpandPopupMenuOptions"}}
|
|
||||||
@onPopupMenuAction={{this.onPopupMenuAction}}
|
|
||||||
@popupMenuOptions={{this.popupMenuOptions}}
|
|
||||||
@formTemplateId={{this.composer.formTemplateId}}
|
|
||||||
@formTemplateIds={{this.formTemplateIds}}
|
|
||||||
@formTemplateInitialValues={{@formTemplateInitialValues}}
|
|
||||||
@onSelectFormTemplate={{@onSelectFormTemplate}}
|
|
||||||
@replyingToTopic={{this.composer.replyingToTopic}}
|
|
||||||
@editingPost={{this.composer.editingPost}}
|
|
||||||
@disabled={{this.disableTextarea}}
|
|
||||||
@outletArgs={{hash composer=this.composer editorType="composer"}}
|
|
||||||
@topicId={{this.composer.topic.id}}
|
|
||||||
@categoryId={{this.composer.category.id}}
|
|
||||||
>
|
|
||||||
{{yield}}
|
|
||||||
</DEditor>
|
|
||||||
|
|
||||||
{{#if this.allowUpload}}
|
{{#if (gt this.composer.formTemplateIds.length 1)}}
|
||||||
|
<FormTemplateChooser
|
||||||
|
@filteredIds={{this.composer.formTemplateIds}}
|
||||||
|
@value={{this.selectedFormTemplateId}}
|
||||||
|
@onChange={{this.updateSelectedFormTemplateId}}
|
||||||
|
@options={{hash maximum=1}}
|
||||||
|
class="composer-select-form-template"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
<form id="form-template-form">
|
||||||
|
<FormTemplateField::Wrapper
|
||||||
|
@id={{this.selectedFormTemplateId}}
|
||||||
|
@initialValues={{this.composer.formTemplateInitialValues}}
|
||||||
|
@onSelectFormTemplate={{this.composer.onSelectFormTemplate}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<DEditor
|
||||||
|
@value={{this.composer.model.reply}}
|
||||||
|
@placeholder={{this.replyPlaceholder}}
|
||||||
|
@previewUpdated={{action "previewUpdated"}}
|
||||||
|
@markdownOptions={{this.markdownOptions}}
|
||||||
|
@extraButtons={{action "extraButtons"}}
|
||||||
|
@importQuote={{this.composer.importQuote}}
|
||||||
|
@processPreview={{this.composer.showPreview}}
|
||||||
|
@validation={{this.validation}}
|
||||||
|
@loading={{this.composer.loading}}
|
||||||
|
@forcePreview={{this.forcePreview}}
|
||||||
|
@showLink={{this.showLink}}
|
||||||
|
@composerEvents={{true}}
|
||||||
|
@onPopupMenuAction={{this.composer.onPopupMenuAction}}
|
||||||
|
@popupMenuOptions={{this.composer.popupMenuOptions}}
|
||||||
|
@disabled={{this.composer.disableTextarea}}
|
||||||
|
@outletArgs={{hash composer=this.composer.model editorType="composer"}}
|
||||||
|
@topicId={{this.composer.model.topic.id}}
|
||||||
|
@categoryId={{this.composer.model.category.id}}
|
||||||
|
>
|
||||||
|
{{yield}}
|
||||||
|
</DEditor>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.composer.allowUpload}}
|
||||||
<PickFilesButton
|
<PickFilesButton
|
||||||
@fileInputId="file-uploader"
|
@fileInputId={{this.fileUploadElementId}}
|
||||||
@allowMultiple={{true}}
|
@allowMultiple={{true}}
|
||||||
name="file-uploader"
|
name="file-uploader"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import EmberObject, { action, computed } from "@ember/object";
|
import EmberObject, { action, computed } from "@ember/object";
|
||||||
import { alias } from "@ember/object/computed";
|
|
||||||
import { getOwner } from "@ember/owner";
|
import { getOwner } from "@ember/owner";
|
||||||
import { next, schedule, throttle } from "@ember/runloop";
|
import { next, schedule, throttle } from "@ember/runloop";
|
||||||
|
import { service } from "@ember/service";
|
||||||
import { classNameBindings } from "@ember-decorators/component";
|
import { classNameBindings } from "@ember-decorators/component";
|
||||||
import { observes, on } from "@ember-decorators/object";
|
import { observes, on } from "@ember-decorators/object";
|
||||||
import { BasePlugin } from "@uppy/core";
|
import { BasePlugin } from "@uppy/core";
|
||||||
|
@ -87,14 +87,15 @@ export function addApiImageWrapperButtonClickEvent(fn) {
|
||||||
const DEBOUNCE_FETCH_MS = 450;
|
const DEBOUNCE_FETCH_MS = 450;
|
||||||
const DEBOUNCE_JIT_MS = 2000;
|
const DEBOUNCE_JIT_MS = 2000;
|
||||||
|
|
||||||
@classNameBindings("showToolbar:toolbar-visible", ":wmd-controls")
|
@classNameBindings("composer.showToolbar:toolbar-visible", ":wmd-controls")
|
||||||
export default class ComposerEditor extends Component {
|
export default class ComposerEditor extends Component {
|
||||||
|
@service composer;
|
||||||
|
|
||||||
composerEventPrefix = "composer";
|
composerEventPrefix = "composer";
|
||||||
shouldBuildScrollMap = true;
|
shouldBuildScrollMap = true;
|
||||||
scrollMap = null;
|
scrollMap = null;
|
||||||
processPreview = true;
|
|
||||||
|
|
||||||
@alias("composer") composerModel;
|
fileUploadElementId = "file-uploader";
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(...arguments);
|
super.init(...arguments);
|
||||||
|
@ -103,14 +104,19 @@ export default class ComposerEditor extends Component {
|
||||||
|
|
||||||
this.uppyComposerUpload = new UppyComposerUpload(getOwner(this), {
|
this.uppyComposerUpload = new UppyComposerUpload(getOwner(this), {
|
||||||
composerEventPrefix: this.composerEventPrefix,
|
composerEventPrefix: this.composerEventPrefix,
|
||||||
composerModel: this.composerModel,
|
composerModel: this.composer.model,
|
||||||
uploadMarkdownResolvers,
|
uploadMarkdownResolvers,
|
||||||
uploadPreProcessors,
|
uploadPreProcessors,
|
||||||
uploadHandlers,
|
uploadHandlers,
|
||||||
|
fileUploadElementId: this.fileUploadElementId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("composer.requiredCategoryMissing")
|
get topic() {
|
||||||
|
return this.composer.get("model.topic");
|
||||||
|
}
|
||||||
|
|
||||||
|
@discourseComputed("composer.model.requiredCategoryMissing")
|
||||||
replyPlaceholder(requiredCategoryMissing) {
|
replyPlaceholder(requiredCategoryMissing) {
|
||||||
if (requiredCategoryMissing) {
|
if (requiredCategoryMissing) {
|
||||||
return "composer.reply_placeholder_choose_category";
|
return "composer.reply_placeholder_choose_category";
|
||||||
|
@ -130,9 +136,9 @@ export default class ComposerEditor extends Component {
|
||||||
return this.currentUser && this.currentUser.link_posting_access !== "none";
|
return this.currentUser && this.currentUser.link_posting_access !== "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@observes("focusTarget")
|
@observes("composer.focusTarget")
|
||||||
setFocus() {
|
setFocus() {
|
||||||
if (this.focusTarget === "editor") {
|
if (this.composer.focusTarget === "editor") {
|
||||||
putCursorAtEnd(this.element.querySelector("textarea"));
|
putCursorAtEnd(this.element.querySelector("textarea"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,11 +199,11 @@ export default class ComposerEditor extends Component {
|
||||||
this._registerImageAltTextButtonClick(preview);
|
this._registerImageAltTextButtonClick(preview);
|
||||||
|
|
||||||
// Focus on the body unless we have a title
|
// Focus on the body unless we have a title
|
||||||
if (!this.get("composer.canEditTitle")) {
|
if (!this.get("composer.model.canEditTitle")) {
|
||||||
putCursorAtEnd(input);
|
putCursorAtEnd(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.allowUpload) {
|
if (this.composer.allowUpload) {
|
||||||
this.uppyComposerUpload.setup(this.element);
|
this.uppyComposerUpload.setup(this.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,11 +211,11 @@ export default class ComposerEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"composer.reply",
|
"composer.model.reply",
|
||||||
"composer.replyLength",
|
"composer.model.replyLength",
|
||||||
"composer.missingReplyCharacters",
|
"composer.model.missingReplyCharacters",
|
||||||
"composer.minimumPostLength",
|
"composer.model.minimumPostLength",
|
||||||
"lastValidatedAt"
|
"composer.lastValidatedAt"
|
||||||
)
|
)
|
||||||
validation(
|
validation(
|
||||||
reply,
|
reply,
|
||||||
|
@ -254,9 +260,9 @@ export default class ComposerEditor extends Component {
|
||||||
@computed("composer.{creatingTopic,editingFirstPost,creatingSharedDraft}")
|
@computed("composer.{creatingTopic,editingFirstPost,creatingSharedDraft}")
|
||||||
get _isNewTopic() {
|
get _isNewTopic() {
|
||||||
return (
|
return (
|
||||||
this.composer.creatingTopic ||
|
this.composer.model.creatingTopic ||
|
||||||
this.composer.editingFirstPost ||
|
this.composer.model.editingFirstPost ||
|
||||||
this.composer.creatingSharedDraft
|
this.composer.model.creatingSharedDraft
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,8 +448,8 @@ export default class ComposerEditor extends Component {
|
||||||
_renderUnseenMentions(preview, unseen) {
|
_renderUnseenMentions(preview, unseen) {
|
||||||
fetchUnseenMentions({
|
fetchUnseenMentions({
|
||||||
names: unseen,
|
names: unseen,
|
||||||
topicId: this.get("composer.topic.id"),
|
topicId: this.get("composer.model.topic.id"),
|
||||||
allowedNames: this.get("composer.targetRecipients")?.split(","),
|
allowedNames: this.get("composer.model.targetRecipients")?.split(","),
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
linkSeenMentions(preview, this.siteSettings);
|
linkSeenMentions(preview, this.siteSettings);
|
||||||
this._warnMentionedGroups(preview);
|
this._warnMentionedGroups(preview);
|
||||||
|
@ -510,7 +516,7 @@ export default class ComposerEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.warnedGroupMentions.push(name);
|
this.warnedGroupMentions.push(name);
|
||||||
this.groupsMentioned({
|
this.composer.groupsMentioned({
|
||||||
name,
|
name,
|
||||||
userCount: mention.dataset.mentionableUserCount,
|
userCount: mention.dataset.mentionableUserCount,
|
||||||
maxMentions: mention.dataset.maxMentions,
|
maxMentions: mention.dataset.maxMentions,
|
||||||
|
@ -523,7 +529,7 @@ export default class ComposerEditor extends Component {
|
||||||
// previously we would warn after @bob even if you were about to mention @bob2
|
// previously we would warn after @bob even if you were about to mention @bob2
|
||||||
@debounce(DEBOUNCE_JIT_MS)
|
@debounce(DEBOUNCE_JIT_MS)
|
||||||
_warnCannotSeeMention(preview) {
|
_warnCannotSeeMention(preview) {
|
||||||
if (this.composer.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) {
|
if (this.composer.model?.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,7 +540,7 @@ export default class ComposerEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.warnedCannotSeeMentions.push(name);
|
this.warnedCannotSeeMentions.push(name);
|
||||||
this.cannotSeeMention({
|
this.composer.cannotSeeMention({
|
||||||
name,
|
name,
|
||||||
reason: mention.dataset.reason,
|
reason: mention.dataset.reason,
|
||||||
});
|
});
|
||||||
|
@ -549,7 +555,7 @@ export default class ComposerEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.warnedCannotSeeMentions.push(name);
|
this.warnedCannotSeeMentions.push(name);
|
||||||
this.cannotSeeMention({
|
this.composer.cannotSeeMention({
|
||||||
name,
|
name,
|
||||||
reason: mention.dataset.reason,
|
reason: mention.dataset.reason,
|
||||||
notifiedCount: mention.dataset.notifiedUserCount,
|
notifiedCount: mention.dataset.notifiedUserCount,
|
||||||
|
@ -563,7 +569,7 @@ export default class ComposerEditor extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hereMention(hereCount);
|
this.composer.hereMention(hereCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -578,8 +584,9 @@ export default class ComposerEditor extends Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
const scale = event.target.dataset.scale;
|
const scale = event.target.dataset.scale;
|
||||||
const matchingPlaceholder =
|
const matchingPlaceholder = this.get("composer.model.reply").match(
|
||||||
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
|
IMAGE_MARKDOWN_REGEX
|
||||||
|
);
|
||||||
|
|
||||||
if (matchingPlaceholder) {
|
if (matchingPlaceholder) {
|
||||||
const match = matchingPlaceholder[index];
|
const match = matchingPlaceholder[index];
|
||||||
|
@ -624,8 +631,9 @@ export default class ComposerEditor extends Component {
|
||||||
|
|
||||||
commitAltText(buttonWrapper) {
|
commitAltText(buttonWrapper) {
|
||||||
const index = parseInt(buttonWrapper.getAttribute("data-image-index"), 10);
|
const index = parseInt(buttonWrapper.getAttribute("data-image-index"), 10);
|
||||||
const matchingPlaceholder =
|
const matchingPlaceholder = this.get("composer.model.reply").match(
|
||||||
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
|
IMAGE_MARKDOWN_REGEX
|
||||||
|
);
|
||||||
const match = matchingPlaceholder[index];
|
const match = matchingPlaceholder[index];
|
||||||
const input = buttonWrapper.querySelector("input.alt-text-input");
|
const input = buttonWrapper.querySelector("input.alt-text-input");
|
||||||
const replacement = match.replace(
|
const replacement = match.replace(
|
||||||
|
@ -717,8 +725,9 @@ export default class ComposerEditor extends Component {
|
||||||
event.target.closest(".button-wrapper").dataset.imageIndex,
|
event.target.closest(".button-wrapper").dataset.imageIndex,
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
const matchingPlaceholder =
|
const matchingPlaceholder = this.get("composer.model.reply").match(
|
||||||
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
|
IMAGE_MARKDOWN_REGEX
|
||||||
|
);
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
`${this.composerEventPrefix}:replace-text`,
|
||||||
matchingPlaceholder[index],
|
matchingPlaceholder[index],
|
||||||
|
@ -737,7 +746,7 @@ export default class ComposerEditor extends Component {
|
||||||
event.target.closest(".button-wrapper").dataset.imageIndex,
|
event.target.closest(".button-wrapper").dataset.imageIndex,
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
const reply = this.get("composer.reply");
|
const reply = this.get("composer.model.reply");
|
||||||
const matches = reply.match(IMAGE_MARKDOWN_REGEX);
|
const matches = reply.match(IMAGE_MARKDOWN_REGEX);
|
||||||
const closingIndex =
|
const closingIndex =
|
||||||
index + parseInt(event.target.dataset.imageCount, 10) - 1;
|
index + parseInt(event.target.dataset.imageCount, 10) - 1;
|
||||||
|
@ -757,6 +766,10 @@ export default class ComposerEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerImageAltTextButtonClick(preview) {
|
_registerImageAltTextButtonClick(preview) {
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
||||||
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
||||||
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
||||||
|
@ -775,7 +788,7 @@ export default class ComposerEditor extends Component {
|
||||||
const input = this.element.querySelector(".d-editor-input");
|
const input = this.element.querySelector(".d-editor-input");
|
||||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||||
|
|
||||||
if (this.allowUpload) {
|
if (this.composer.allowUpload) {
|
||||||
this.uppyComposerUpload.teardown();
|
this.uppyComposerUpload.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -811,11 +824,11 @@ export default class ComposerEditor extends Component {
|
||||||
onExpandPopupMenuOptions(toolbarEvent) {
|
onExpandPopupMenuOptions(toolbarEvent) {
|
||||||
const selected = toolbarEvent.selected;
|
const selected = toolbarEvent.selected;
|
||||||
toolbarEvent.selectText(selected.start, selected.end - selected.start);
|
toolbarEvent.selectText(selected.start, selected.end - selected.start);
|
||||||
this.storeToolbarState(toolbarEvent);
|
this.composer.storeToolbarState(toolbarEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
showPreview() {
|
showPreview() {
|
||||||
this.send("togglePreview");
|
this.composer.togglePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInQuote(element) {
|
_isInQuote(element) {
|
||||||
|
@ -848,16 +861,20 @@ export default class ComposerEditor extends Component {
|
||||||
id: "quote",
|
id: "quote",
|
||||||
group: "fontStyles",
|
group: "fontStyles",
|
||||||
icon: "far-comment",
|
icon: "far-comment",
|
||||||
sendAction: this.importQuote,
|
sendAction: this.composer.importQuote,
|
||||||
title: "composer.quote_post_title",
|
title: "composer.quote_post_title",
|
||||||
unshift: true,
|
unshift: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.allowUpload && this.uploadIcon && this.site.desktopView) {
|
if (
|
||||||
|
this.composer.allowUpload &&
|
||||||
|
this.composer.uploadIcon &&
|
||||||
|
this.site.desktopView
|
||||||
|
) {
|
||||||
toolbar.addButton({
|
toolbar.addButton({
|
||||||
id: "upload",
|
id: "upload",
|
||||||
group: "insertions",
|
group: "insertions",
|
||||||
icon: this.uploadIcon,
|
icon: this.composer.uploadIcon,
|
||||||
title: "upload",
|
title: "upload",
|
||||||
sendAction: this.showUploadModal,
|
sendAction: this.showUploadModal,
|
||||||
});
|
});
|
||||||
|
@ -884,6 +901,40 @@ export default class ComposerEditor extends Component {
|
||||||
this._decorateCookedElement(preview);
|
this._decorateCookedElement(preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.afterRefresh(preview);
|
this.composer.afterRefresh(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed("composer.formTemplateIds")
|
||||||
|
get selectedFormTemplateId() {
|
||||||
|
if (this._selectedFormTemplateId) {
|
||||||
|
return this._selectedFormTemplateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.composer.model.formTemplateId || this.composer.formTemplateIds?.[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedFormTemplateId(value) {
|
||||||
|
this._selectedFormTemplateId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateSelectedFormTemplateId(formTemplateId) {
|
||||||
|
this.selectedFormTemplateId = formTemplateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@discourseComputed(
|
||||||
|
"composer.formTemplateIds",
|
||||||
|
"composer.model.replyingToTopic",
|
||||||
|
"composer.model.editingPost"
|
||||||
|
)
|
||||||
|
showFormTemplateForm(formTemplateIds, replyingToTopic, editingPost) {
|
||||||
|
return formTemplateIds?.length > 0 && !replyingToTopic && !editingPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showUploadModal() {
|
||||||
|
document.getElementById(this.fileUploadElementId).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +1,63 @@
|
||||||
<div class="d-editor-container">
|
<div class="d-editor-container">
|
||||||
<div class="d-editor-textarea-column">
|
<div class="d-editor-textarea-column">
|
||||||
{{yield}}
|
{{yield}}
|
||||||
{{#if this.showFormTemplateForm}}
|
|
||||||
{{#if (gt @formTemplateIds.length 1)}}
|
|
||||||
<FormTemplateChooser
|
|
||||||
@filteredIds={{@formTemplateIds}}
|
|
||||||
@value={{this.selectedFormTemplateId}}
|
|
||||||
@onChange={{this.updateSelectedFormTemplateId}}
|
|
||||||
@options={{hash maximum=1}}
|
|
||||||
class="composer-select-form-template"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
<form id="form-template-form">
|
|
||||||
<FormTemplateField::Wrapper
|
|
||||||
@id={{this.selectedFormTemplateId}}
|
|
||||||
@initialValues={{@formTemplateInitialValues}}
|
|
||||||
@onSelectFormTemplate={{@onSelectFormTemplate}}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<div
|
|
||||||
class="d-editor-textarea-wrapper
|
|
||||||
{{if this.disabled 'disabled'}}
|
|
||||||
{{if this.isEditorFocused 'in-focus'}}"
|
|
||||||
>
|
|
||||||
<div class="d-editor-button-bar" role="toolbar">
|
|
||||||
{{#each this.toolbar.groups as |group|}}
|
|
||||||
{{#each group.buttons as |b|}}
|
|
||||||
{{#if (b.condition this)}}
|
|
||||||
{{#if b.popupMenu}}
|
|
||||||
<ToolbarPopupMenuOptions
|
|
||||||
@content={{this.popupMenuOptions}}
|
|
||||||
@onChange={{this.onPopupMenuAction}}
|
|
||||||
@onOpen={{action b.action b}}
|
|
||||||
@tabindex={{-1}}
|
|
||||||
@onKeydown={{this.rovingButtonBar}}
|
|
||||||
@options={{hash icon=b.icon focusAfterOnChange=false}}
|
|
||||||
class={{b.className}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<DButton
|
|
||||||
@action={{fn (action b.action) b}}
|
|
||||||
@translatedTitle={{b.title}}
|
|
||||||
@label={{b.label}}
|
|
||||||
@icon={{b.icon}}
|
|
||||||
@preventFocus={{b.preventFocus}}
|
|
||||||
@onKeyDown={{this.rovingButtonBar}}
|
|
||||||
tabindex={{b.tabindex}}
|
|
||||||
class={{b.className}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
<div
|
||||||
<this.editorComponent
|
class="d-editor-textarea-wrapper
|
||||||
@onSetup={{this.setupEditor}}
|
{{if this.disabled 'disabled'}}
|
||||||
@markdownOptions={{this.markdownOptions}}
|
{{if this.isEditorFocused 'in-focus'}}"
|
||||||
@keymap={{this.keymap}}
|
>
|
||||||
@value={{this.value}}
|
<div class="d-editor-button-bar" role="toolbar">
|
||||||
@placeholder={{this.placeholderTranslated}}
|
{{#each this.toolbar.groups as |group|}}
|
||||||
@disabled={{this.disabled}}
|
{{#each group.buttons as |b|}}
|
||||||
@change={{this.change}}
|
{{#if (b.condition this)}}
|
||||||
@focusIn={{this.handleFocusIn}}
|
{{#if b.popupMenu}}
|
||||||
@focusOut={{this.handleFocusOut}}
|
<ToolbarPopupMenuOptions
|
||||||
@id={{this.textAreaId}}
|
@content={{this.popupMenuOptions}}
|
||||||
/>
|
@onChange={{this.onPopupMenuAction}}
|
||||||
<PopupInputTip @validation={{this.validation}} />
|
@onOpen={{action b.action b}}
|
||||||
<PluginOutlet
|
@tabindex={{-1}}
|
||||||
@name="after-d-editor"
|
@onKeydown={{this.rovingButtonBar}}
|
||||||
@connectorTagName="div"
|
@options={{hash icon=b.icon focusAfterOnChange=false}}
|
||||||
@outletArgs={{this.outletArgs}}
|
class={{b.className}}
|
||||||
/>
|
/>
|
||||||
|
{{else}}
|
||||||
|
<DButton
|
||||||
|
@action={{fn (action b.action) b}}
|
||||||
|
@translatedTitle={{b.title}}
|
||||||
|
@label={{b.label}}
|
||||||
|
@icon={{b.icon}}
|
||||||
|
@preventFocus={{b.preventFocus}}
|
||||||
|
@onKeyDown={{this.rovingButtonBar}}
|
||||||
|
tabindex={{b.tabindex}}
|
||||||
|
class={{b.className}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
|
||||||
|
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||||
|
<this.editorComponent
|
||||||
|
@onSetup={{this.setupEditor}}
|
||||||
|
@markdownOptions={{this.markdownOptions}}
|
||||||
|
@keymap={{this.keymap}}
|
||||||
|
@value={{this.value}}
|
||||||
|
@placeholder={{this.placeholderTranslated}}
|
||||||
|
@disabled={{this.disabled}}
|
||||||
|
@change={{this.change}}
|
||||||
|
@focusIn={{this.handleFocusIn}}
|
||||||
|
@focusOut={{this.handleFocusOut}}
|
||||||
|
@id={{this.textAreaId}}
|
||||||
|
/>
|
||||||
|
<PopupInputTip @validation={{this.validation}} />
|
||||||
|
<PluginOutlet
|
||||||
|
@name="after-d-editor"
|
||||||
|
@connectorTagName="div"
|
||||||
|
@outletArgs={{this.outletArgs}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { action, computed } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { getOwner } from "@ember/owner";
|
import { getOwner } from "@ember/owner";
|
||||||
import { schedule, scheduleOnce } from "@ember/runloop";
|
import { schedule, scheduleOnce } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
@ -82,30 +82,6 @@ export default class DEditor extends Component {
|
||||||
this.register = getRegister(this);
|
this.register = getRegister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("formTemplateIds")
|
|
||||||
get selectedFormTemplateId() {
|
|
||||||
if (this._selectedFormTemplateId) {
|
|
||||||
return this._selectedFormTemplateId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.formTemplateId || this.formTemplateIds?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
set selectedFormTemplateId(value) {
|
|
||||||
this._selectedFormTemplateId = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateSelectedFormTemplateId(formTemplateId) {
|
|
||||||
this.selectedFormTemplateId = formTemplateId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("formTemplateIds", "replyingToTopic", "editingPost")
|
|
||||||
showFormTemplateForm(formTemplateIds, replyingToTopic, editingPost) {
|
|
||||||
// TODO(@keegan): Remove !editingPost once we add edit/draft support for form templates
|
|
||||||
return formTemplateIds?.length > 0 && !replyingToTopic && !editingPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("placeholder")
|
@discourseComputed("placeholder")
|
||||||
placeholderTranslated(placeholder) {
|
placeholderTranslated(placeholder) {
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class UppyComposerUpload {
|
||||||
uploadType = "composer";
|
uploadType = "composer";
|
||||||
editorInputClass = ".d-editor-input";
|
editorInputClass = ".d-editor-input";
|
||||||
mobileFileUploaderId = "mobile-file-upload";
|
mobileFileUploaderId = "mobile-file-upload";
|
||||||
fileUploadElementId = "file-uploader";
|
fileUploadElementId;
|
||||||
editorClass = ".d-editor";
|
editorClass = ".d-editor";
|
||||||
|
|
||||||
composerEventPrefix;
|
composerEventPrefix;
|
||||||
|
@ -73,6 +73,7 @@ export default class UppyComposerUpload {
|
||||||
uploadMarkdownResolvers,
|
uploadMarkdownResolvers,
|
||||||
uploadPreProcessors,
|
uploadPreProcessors,
|
||||||
uploadHandlers,
|
uploadHandlers,
|
||||||
|
fileUploadElementId,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
setOwner(this, owner);
|
setOwner(this, owner);
|
||||||
|
@ -82,6 +83,7 @@ export default class UppyComposerUpload {
|
||||||
this.uploadMarkdownResolvers = uploadMarkdownResolvers;
|
this.uploadMarkdownResolvers = uploadMarkdownResolvers;
|
||||||
this.uploadPreProcessors = uploadPreProcessors;
|
this.uploadPreProcessors = uploadPreProcessors;
|
||||||
this.uploadHandlers = uploadHandlers;
|
this.uploadHandlers = uploadHandlers;
|
||||||
|
this.fileUploadElementId = fileUploadElementId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
|
|
@ -8,8 +8,6 @@ module("Integration | Component | ComposerEditor", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test("warns about users that will not see a mention", async function (assert) {
|
test("warns about users that will not see a mention", async function (assert) {
|
||||||
const model = {};
|
|
||||||
const noop = () => {};
|
|
||||||
const expectation = (warning) => {
|
const expectation = (warning) => {
|
||||||
if (warning.name === "user-no") {
|
if (warning.name === "user-no") {
|
||||||
assert.deepEqual(warning, { name: "user-no", reason: "a reason" });
|
assert.deepEqual(warning, { name: "user-no", reason: "a reason" });
|
||||||
|
@ -31,24 +29,24 @@ module("Integration | Component | ComposerEditor", function (hooks) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await render(<template>
|
const originalComposerService = this.owner.lookup("service:composer");
|
||||||
<ComposerEditor
|
const composerMockClass = class ComposerMock extends originalComposerService.constructor {
|
||||||
@composer={{model}}
|
cannotSeeMention() {
|
||||||
@afterRefresh={{noop}}
|
expectation(...arguments);
|
||||||
@cannotSeeMention={{expectation}}
|
}
|
||||||
/>
|
};
|
||||||
</template>);
|
this.owner.unregister("service:composer");
|
||||||
|
this.owner.register("service:composer", new composerMockClass(this.owner), {
|
||||||
|
instantiate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(<template><ComposerEditor /></template>);
|
||||||
|
|
||||||
await fillIn("textarea", "@user-no @user-ok @user-nope");
|
await fillIn("textarea", "@user-no @user-ok @user-nope");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preview sanitizes HTML", async function (assert) {
|
test("preview sanitizes HTML", async function (assert) {
|
||||||
const model = {};
|
await render(<template><ComposerEditor /></template>);
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
await render(<template>
|
|
||||||
<ComposerEditor @composer={{model}} @afterRefresh={{noop}} />
|
|
||||||
</template>);
|
|
||||||
|
|
||||||
await fillIn(".d-editor-input", `"><svg onload="prompt(/xss/)"></svg>`);
|
await fillIn(".d-editor-input", `"><svg onload="prompt(/xss/)"></svg>`);
|
||||||
assert.dom(".d-editor-preview").hasHtml('<p>"><svg></svg></p>');
|
assert.dom(".d-editor-preview").hasHtml('<p>"><svg></svg></p>');
|
||||||
|
|
|
@ -421,10 +421,6 @@ html.composer-open {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-uploader {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.composer-select-form-template {
|
.composer-select-form-template {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
Loading…
Reference in New Issue