This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/components/composer-editor.js.es6
Penar Musaraj 1a8c6577e0 FIX: refactor padding when exiting composer
Previously, when existing composer, the `#main-outlet` element padding was set to zero. This inline style would override any CSS set for that element, causing issues with the mobile footer nav.

The fix removes the inline padding style instead of setting it to zero. It also uses integers for the set values, and removes a duplicate style.
2019-04-24 10:26:35 -04:00

1111 lines
31 KiB
JavaScript

import userSearch from "discourse/lib/user-search";
import {
default as computed,
observes,
on
} from "ember-addons/ember-computed-decorators";
import {
linkSeenMentions,
fetchUnseenMentions
} from "discourse/lib/link-mentions";
import {
linkSeenCategoryHashtags,
fetchUnseenCategoryHashtags
} from "discourse/lib/link-category-hashtags";
import {
linkSeenTagHashtags,
fetchUnseenTagHashtags
} from "discourse/lib/link-tag-hashtag";
import Composer from "discourse/models/composer";
import { load, LOADING_ONEBOX_CSS_CLASS } from "pretty-text/oneboxer";
import { applyInlineOneboxes } from "pretty-text/inline-oneboxer";
import { ajax } from "discourse/lib/ajax";
import InputValidation from "discourse/models/input-validation";
import { findRawTemplate } from "discourse/lib/raw-templates";
import { iconHTML } from "discourse-common/lib/icon-library";
import {
tinyAvatar,
displayErrorForUpload,
getUploadMarkdown,
validateUploadedFiles,
authorizesOneOrMoreImageExtensions,
formatUsername,
clipboardData,
safariHacksDisabled
} from "discourse/lib/utilities";
import {
cacheShortUploadUrl,
resolveAllShortUrls
} from "pretty-text/image-short-url";
import {
INLINE_ONEBOX_LOADING_CSS_CLASS,
INLINE_ONEBOX_CSS_CLASS
} from "pretty-text/inline-oneboxer";
const REBUILD_SCROLL_MAP_EVENTS = ["composer:resized", "composer:typed-reply"];
const uploadHandlers = [];
export function addComposerUploadHandler(extensions, method) {
uploadHandlers.push({
extensions,
method
});
}
export default Ember.Component.extend({
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
uploadProgress: 0,
_xhr: null,
shouldBuildScrollMap: true,
scrollMap: null,
uploadFilenamePlaceholder: null,
@computed("uploadFilenamePlaceholder")
uploadPlaceholder(uploadFilenamePlaceholder) {
const clipboard = I18n.t("clipboard");
const filename = uploadFilenamePlaceholder
? uploadFilenamePlaceholder
: clipboard;
return `[${I18n.t("uploading_filename", { filename })}]() `;
},
@computed("composer.requiredCategoryMissing")
replyPlaceholder(requiredCategoryMissing) {
if (requiredCategoryMissing) {
return "composer.reply_placeholder_choose_category";
} else {
const key = authorizesOneOrMoreImageExtensions()
? "reply_placeholder"
: "reply_placeholder_no_images";
return `composer.${key}`;
}
},
@computed
showLink() {
return (
this.currentUser && this.currentUser.get("link_posting_access") !== "none"
);
},
@computed("composer.requiredCategoryMissing", "composer.replyLength")
disableTextarea(requiredCategoryMissing, replyLength) {
return requiredCategoryMissing && replyLength === 0;
},
@observes("composer.uploadCancelled")
_cancelUpload() {
if (!this.get("composer.uploadCancelled")) {
return;
}
this.set("composer.uploadCancelled", false);
if (this._xhr) {
this._xhr._userCancelled = true;
this._xhr.abort();
}
this._resetUpload(true);
},
@observes("focusTarget")
setFocus() {
if (this.get("focusTarget") === "editor") {
this.$("textarea").putCursorAtEnd();
}
},
@computed
markdownOptions() {
return {
previewing: true,
formatUsername,
lookupAvatarByPostNumber: (postNumber, topicId) => {
const topic = this.get("topic");
if (!topic) {
return;
}
const posts = topic.get("postStream.posts");
if (posts && topicId === topic.get("id")) {
const quotedPost = posts.findBy("post_number", postNumber);
if (quotedPost) {
return tinyAvatar(quotedPost.get("avatar_template"));
}
}
},
lookupPrimaryUserGroupByPostNumber: (postNumber, topicId) => {
const topic = this.get("topic");
if (!topic) {
return;
}
const posts = topic.get("postStream.posts");
if (posts && topicId === topic.get("id")) {
const quotedPost = posts.findBy("post_number", postNumber);
if (quotedPost) {
return quotedPost.primary_group_name;
}
}
}
};
},
@on("didInsertElement")
_composerEditorInit() {
const topicId = this.get("topic.id");
const $input = this.$(".d-editor-input");
const $preview = this.$(".d-editor-preview-wrapper");
if (this.siteSettings.enable_mentions) {
$input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"),
dataSource: term =>
userSearch({
term,
topicId,
includeMentionableGroups: true
}),
key: "@",
transformComplete: v => v.username || v.name,
afterComplete() {
// ensures textarea scroll position is correct
Ember.run.scheduleOnce("afterRender", () => $input.blur().focus());
}
});
}
if (this._enableAdvancedEditorPreviewSync()) {
this._initInputPreviewSync($input, $preview);
} else {
$input.on("scroll", () =>
Ember.run.throttle(
this,
this._syncEditorAndPreviewScroll,
$input,
$preview,
20
)
);
}
if (!this.site.mobileView) {
$preview
.off("touchstart mouseenter", "img")
.on("touchstart mouseenter", "img", () => {
this._placeImageScaleButtons($preview);
});
}
// Focus on the body unless we have a title
if (
!this.get("composer.canEditTitle") &&
(!this.capabilities.isIOS || safariHacksDisabled())
) {
this.$(".d-editor-input").putCursorAtEnd();
}
this._bindUploadTarget();
this.appEvents.trigger("composer:will-open");
},
@computed(
"composer.reply",
"composer.replyLength",
"composer.missingReplyCharacters",
"composer.minimumPostLength",
"lastValidatedAt"
)
validation(
reply,
replyLength,
missingReplyCharacters,
minimumPostLength,
lastValidatedAt
) {
const postType = this.get("composer.post.post_type");
if (postType === this.site.get("post_types.small_action")) {
return;
}
let reason;
if (replyLength < 1) {
reason = I18n.t("composer.error.post_missing");
} else if (missingReplyCharacters > 0) {
reason = I18n.t("composer.error.post_length", { min: minimumPostLength });
const tl = Discourse.User.currentProp("trust_level");
if (tl === 0 || tl === 1) {
reason +=
"<br/>" +
I18n.t("composer.error.try_like", { heart: iconHTML("heart") });
}
}
if (reason) {
return InputValidation.create({
failed: true,
reason,
lastShownAt: lastValidatedAt
});
}
},
_setUploadPlaceholderSend(data) {
const filename = this._filenamePlaceholder(data);
this.set("uploadFilenamePlaceholder", filename);
// 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: [Uplodading: test.png(1)...]
const regexString = `\\[${I18n.t("uploading_filename", {
filename: filename + "(?:\\()?([0-9])?(?:\\))?"
})}\\]\\(\\)`;
const globalRegex = new RegExp(regexString, "g");
const matchingPlaceholder = this.get("composer.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]) + 1
: 1;
data.orderNr = orderNr;
const filenameWithOrderNr = `${filename}(${orderNr})`;
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
}
},
_setUploadPlaceholderDone(data) {
const filename = this._filenamePlaceholder(data);
const filenameWithSize = `${filename} (${data.total})`;
this.set("uploadFilenamePlaceholder", filenameWithSize);
if (data.orderNr) {
const filenameWithOrderNr = `${filename}(${data.orderNr})`;
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
} else {
this.set("uploadFilenamePlaceholder", filename);
}
},
_filenamePlaceholder(data) {
return data.files[0].name.replace(/\u200B-\u200D\uFEFF]/g, "");
},
_resetUploadFilenamePlaceholder() {
this.set("uploadFilenamePlaceholder", null);
},
_enableAdvancedEditorPreviewSync() {
return this.siteSettings.enable_advanced_editor_preview_sync;
},
_resetShouldBuildScrollMap() {
this.set("shouldBuildScrollMap", true);
},
_initInputPreviewSync($input, $preview) {
REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
this.appEvents.on(event, this, this._resetShouldBuildScrollMap);
});
Ember.run.scheduleOnce("afterRender", () => {
$input.on("touchstart mouseenter", () => {
if (!$preview.is(":visible")) return;
$preview.off("scroll");
$input.on("scroll", () => {
this._syncScroll(this._syncEditorAndPreviewScroll, $input, $preview);
});
});
$preview.on("touchstart mouseenter", () => {
$input.off("scroll");
$preview.on("scroll", () => {
this._syncScroll(this._syncPreviewAndEditorScroll, $input, $preview);
});
});
});
},
_syncScroll($callback, $input, $preview) {
if (!this.get("scrollMap") || this.get("shouldBuildScrollMap")) {
this.set("scrollMap", this._buildScrollMap($input, $preview));
this.set("shouldBuildScrollMap", false);
}
Ember.run.throttle(
this,
$callback,
$input,
$preview,
this.get("scrollMap"),
20
);
},
_teardownInputPreviewSync() {
[this.$(".d-editor-input"), this.$(".d-editor-preview-wrapper")].forEach(
$element => {
$element.off("mouseenter touchstart");
$element.off("scroll");
}
);
REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
this.appEvents.off(event, this, this._resetShouldBuildScrollMap);
});
},
// Adapted from https://github.com/markdown-it/markdown-it.github.io
_buildScrollMap($input, $preview) {
let sourceLikeDiv = $("<div />")
.css({
position: "absolute",
height: "auto",
visibility: "hidden",
width: $input[0].clientWidth,
"font-size": $input.css("font-size"),
"font-family": $input.css("font-family"),
"line-height": $input.css("line-height"),
"white-space": $input.css("white-space")
})
.appendTo("body");
const linesMap = [];
let numberOfLines = 0;
$input
.val()
.split("\n")
.forEach(text => {
linesMap.push(numberOfLines);
if (text.length === 0) {
numberOfLines++;
} else {
sourceLikeDiv.text(text);
let height;
let lineHeight;
height = parseFloat(sourceLikeDiv.css("height"));
lineHeight = parseFloat(sourceLikeDiv.css("line-height"));
numberOfLines += Math.round(height / lineHeight);
}
});
linesMap.push(numberOfLines);
sourceLikeDiv.remove();
const previewOffsetTop = $preview.offset().top;
const offset =
$preview.scrollTop() -
previewOffsetTop -
($input.offset().top - previewOffsetTop);
const nonEmptyList = [];
const scrollMap = [];
for (let i = 0; i < numberOfLines; i++) {
scrollMap.push(-1);
}
nonEmptyList.push(0);
scrollMap[0] = 0;
$preview.find(".preview-sync-line").each((_, element) => {
let $element = $(element);
let lineNumber = $element.data("line-number");
let linesToTop = linesMap[lineNumber];
if (linesToTop !== 0) {
nonEmptyList.push(linesToTop);
}
scrollMap[linesToTop] = Math.round($element.offset().top + offset);
});
nonEmptyList.push(numberOfLines);
scrollMap[numberOfLines] = $preview[0].scrollHeight;
let position = 0;
for (let i = 1; i < numberOfLines; i++) {
if (scrollMap[i] !== -1) {
position++;
continue;
}
let top = nonEmptyList[position];
let bottom = nonEmptyList[position + 1];
scrollMap[i] = (
(scrollMap[bottom] * (i - top) + scrollMap[top] * (bottom - i)) /
(bottom - top)
).toFixed(2);
}
return scrollMap;
},
_syncEditorAndPreviewScroll($input, $preview, scrollMap) {
if (this._enableAdvancedEditorPreviewSync()) {
let scrollTop;
const inputHeight = $input.height();
const inputScrollHeight = $input[0].scrollHeight;
const inputClientHeight = $input[0].clientHeight;
const scrollable = inputScrollHeight > inputClientHeight;
if (
scrollable &&
inputHeight + $input.scrollTop() + 100 > inputScrollHeight
) {
scrollTop = $preview[0].scrollHeight;
} else {
const lineHeight = parseFloat($input.css("line-height"));
const lineNumber = Math.floor($input.scrollTop() / lineHeight);
scrollTop = scrollMap[lineNumber];
}
$preview.stop(true).animate({ scrollTop }, 100, "linear");
} else {
if (!$input) {
return;
}
if ($input.scrollTop() === 0) {
$preview.scrollTop(0);
return;
}
const inputHeight = $input[0].scrollHeight;
const previewHeight = $preview[0].scrollHeight;
if ($input.height() + $input.scrollTop() + 100 > inputHeight) {
// cheat, special case for bottom
$preview.scrollTop(previewHeight);
return;
}
const scrollPosition = $input.scrollTop();
const factor = previewHeight / inputHeight;
const desired = scrollPosition * factor;
$preview.scrollTop(desired + 50);
}
},
_syncPreviewAndEditorScroll($input, $preview, scrollMap) {
if (scrollMap.length < 1) return;
let scrollTop;
const previewScrollTop = $preview.scrollTop();
if ($preview.height() + previewScrollTop + 100 > $preview[0].scrollHeight) {
scrollTop = $input[0].scrollHeight;
} else {
const lineHeight = parseFloat($input.css("line-height"));
scrollTop =
lineHeight * scrollMap.findIndex(offset => offset > previewScrollTop);
}
$input.stop(true).animate({ scrollTop }, 100, "linear");
},
_renderUnseenMentions($preview, unseen) {
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
fetchUnseenMentions(unseen, this.get("composer.topic.id")).then(() => {
linkSeenMentions($preview, this.siteSettings);
this._warnMentionedGroups($preview);
this._warnCannotSeeMention($preview);
});
},
_renderUnseenCategoryHashtags($preview, unseen) {
fetchUnseenCategoryHashtags(unseen).then(() => {
linkSeenCategoryHashtags($preview);
});
},
_renderUnseenTagHashtags($preview, unseen) {
fetchUnseenTagHashtags(unseen).then(() => {
linkSeenTagHashtags($preview);
});
},
_loadInlineOneboxes(inline) {
applyInlineOneboxes(inline, ajax);
},
_loadOneboxes(oneboxes) {
const post = this.get("composer.post");
let refresh = false;
// If we are editing a post, we'll refresh its contents once.
if (post && !post.get("refreshedPost")) {
refresh = true;
post.set("refreshedPost", true);
}
Object.values(oneboxes).forEach(onebox => {
onebox.forEach($onebox => {
load({
elem: $onebox,
refresh,
ajax,
categoryId: this.get("composer.category.id"),
topicId: this.get("composer.topic.id")
});
});
});
},
_warnMentionedGroups($preview) {
Ember.run.scheduleOnce("afterRender", () => {
var found = this.get("warnedGroupMentions") || [];
$preview.find(".mention-group.notify").each((idx, e) => {
const $e = $(e);
var name = $e.data("name");
if (found.indexOf(name) === -1) {
this.groupsMentioned([
{
name: name,
user_count: $e.data("mentionable-user-count"),
max_mentions: $e.data("max-mentions")
}
]);
found.push(name);
}
});
this.set("warnedGroupMentions", found);
});
},
_warnCannotSeeMention($preview) {
const composerDraftKey = this.get("composer.draftKey");
if (
composerDraftKey === Composer.CREATE_TOPIC ||
composerDraftKey === Composer.NEW_PRIVATE_MESSAGE_KEY ||
composerDraftKey === Composer.REPLY_AS_NEW_TOPIC_KEY ||
composerDraftKey === Composer.REPLY_AS_NEW_PRIVATE_MESSAGE_KEY
) {
return;
}
Ember.run.scheduleOnce("afterRender", () => {
let found = this.get("warnedCannotSeeMentions") || [];
$preview.find(".mention.cannot-see").each((idx, e) => {
const $e = $(e);
let name = $e.data("name");
if (found.indexOf(name) === -1) {
// add a delay to allow for typing, so you don't open the warning right away
// previously we would warn after @bob even if you were about to mention @bob2
Ember.run.later(
this,
() => {
if (
$preview.find('.mention.cannot-see[data-name="' + name + '"]')
.length > 0
) {
this.cannotSeeMention([{ name }]);
found.push(name);
}
},
2000
);
}
});
this.set("warnedCannotSeeMentions", found);
});
},
_resetUpload(removePlaceholder) {
Ember.run.next(() => {
if (this._validUploads > 0) {
this._validUploads--;
}
if (this._validUploads === 0) {
this.setProperties({
uploadProgress: 0,
isUploading: false,
isCancellable: false
});
}
if (removePlaceholder) {
this.appEvents.trigger(
"composer:replace-text",
this.get("uploadPlaceholder"),
""
);
}
this._resetUploadFilenamePlaceholder();
});
},
_bindUploadTarget() {
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
this._pasted = false;
const $element = this.$();
const csrf = this.session.get("csrfToken");
$element.fileupload({
url: Discourse.getURL(
`/uploads.json?client_id=${
this.messageBus.clientId
}&authenticity_token=${encodeURIComponent(csrf)}`
),
dataType: "json",
pasteZone: $element
});
$element.on("fileuploadpaste", e => {
this._pasted = true;
if (!$(".d-editor-input").is(":focus")) {
return;
}
const { canUpload, canPasteHtml } = clipboardData(e, true);
if (!canUpload || canPasteHtml) {
e.preventDefault();
}
});
$element.on("fileuploadsubmit", (e, data) => {
const max = this.siteSettings.simultaneous_uploads;
// Limit the number of simultaneous uploads
if (max > 0 && data.files.length > max) {
bootbox.alert(
I18n.t("post.errors.too_many_dragged_and_dropped_files", { max })
);
return false;
}
// Look for a matching file upload handler contributed from a plugin
const matcher = handler => {
const ext = handler.extensions.join("|");
const regex = new RegExp(`\\.(${ext})$`, "i");
return regex.test(data.files[0].name);
};
const matchingHandler = uploadHandlers.find(matcher);
if (data.files.length === 1 && matchingHandler) {
if (!matchingHandler.method(data.files[0])) {
return false;
}
}
// If no plugin, continue as normal
const isPrivateMessage = this.get("composer.privateMessage");
data.formData = { type: "composer" };
if (isPrivateMessage) data.formData.for_private_message = true;
if (this._pasted) data.formData.pasted = true;
const opts = {
isPrivateMessage,
allowStaffToUploadAnyFileInPm: this.siteSettings
.allow_staff_to_upload_any_file_in_pm
};
const isUploading = validateUploadedFiles(data.files, opts);
this.setProperties({ uploadProgress: 0, isUploading });
return isUploading;
});
$element.on("fileuploadprogressall", (e, data) => {
this.set(
"uploadProgress",
parseInt((data.loaded / data.total) * 100, 10)
);
});
$element.on("fileuploadsend", (e, data) => {
this._pasted = false;
this._validUploads++;
this._setUploadPlaceholderSend(data);
this.appEvents.trigger(
"composer:insert-text",
this.get("uploadPlaceholder")
);
if (data.xhr && data.originalFiles.length === 1) {
this.set("isCancellable", true);
this._xhr = data.xhr();
}
});
$element.on("fileuploaddone", (e, data) => {
let upload = data.result;
this._setUploadPlaceholderDone(data);
if (!this._xhr || !this._xhr._userCancelled) {
const markdown = getUploadMarkdown(upload);
cacheShortUploadUrl(upload.short_url, upload.url);
this.appEvents.trigger(
"composer:replace-text",
this.get("uploadPlaceholder").trim(),
markdown
);
this._resetUpload(false);
} else {
this._resetUpload(true);
}
});
$element.on("fileuploadfail", (e, data) => {
this._setUploadPlaceholderDone(data);
this._resetUpload(true);
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data);
}
});
if (this.site.mobileView) {
$("#reply-control .mobile-file-upload").on("click.uploader", function() {
// redirect the click on the hidden file input
$("#mobile-uploader").click();
});
}
},
_appendImageScaleButtons($images, imageScaleRegex) {
const buttonScales = [100, 75, 50];
const imageWrapperTemplate = `<div class="image-wrapper"></div>`;
const buttonWrapperTemplate = `<div class="button-wrapper"></div>`;
const scaleButtonTemplate = `<span class="scale-btn"></a>`;
$images.each((i, e) => {
const $e = $(e);
const matches = this.get("composer.reply").match(imageScaleRegex);
// ignore previewed upload markdown in codeblock
if (!matches || $e.hasClass("codeblock-image")) return;
if (!$e.parent().hasClass("image-wrapper")) {
const match = matches[i];
const matchingPlaceholder = imageScaleRegex.exec(match);
if (!matchingPlaceholder) return;
const currentScale = matchingPlaceholder[2] || 100;
$e.data("index", i).wrap(imageWrapperTemplate);
$e.parent().append(
$(buttonWrapperTemplate).attr("data-image-index", i)
);
buttonScales.forEach((buttonScale, buttonIndex) => {
const activeClass =
parseInt(currentScale, 10) === buttonScale ? "active" : "";
const $scaleButton = $(scaleButtonTemplate)
.addClass(activeClass)
.attr("data-scale", buttonScale)
.text(`${buttonScale}%`);
const $buttonWrapper = $e.parent().find(".button-wrapper");
$buttonWrapper.append($scaleButton);
if (buttonIndex !== buttonScales.length - 1) {
$buttonWrapper.append(`<span class="separator"> • </span>`);
}
});
}
});
},
_registerImageScaleButtonClick($preview, imageScaleRegex) {
$preview.off("click", ".scale-btn").on("click", ".scale-btn", e => {
const index = parseInt(
$(e.target)
.parent()
.attr("data-image-index")
);
const scale = e.target.attributes["data-scale"].value;
const matchingPlaceholder = this.get("composer.reply").match(
imageScaleRegex
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
if (!match) {
return;
}
const replacement = match.replace(imageScaleRegex, `$1,${scale}%$3`);
this.appEvents.trigger(
"composer:replace-text",
matchingPlaceholder[index],
replacement,
{ regex: imageScaleRegex, index }
);
}
});
},
_placeImageScaleButtons($preview) {
// regex matches only upload placeholders with size defined,
// which is required for resizing
// original string `![28|690x226,5%](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
// match 1 `![28|690x226`
// match 2 `5`
// match 3 `](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
const imageScaleRegex = /(!\[(?:\S*?(?=\|)\|)*?(?:\d{1,6}x\d{1,6})+?)(?:,?(\d{1,3})?%?)?(\]\(upload:\/\/\S*?\))/g;
// wraps previewed upload markdown in a codeblock in its own class to keep a track
// of indexes later on to replace the correct upload placeholder in the composer
if ($preview.find(".codeblock-image").length === 0) {
this.$(".d-editor-preview *")
.contents()
.each(function() {
if (this.nodeType !== 3) return; // TEXT_NODE
const $this = $(this);
if ($this.text().match(imageScaleRegex)) {
$this.wrap("<span class='codeblock-image'></span>");
}
});
}
const $images = $preview.find("img.resizable, span.codeblock-image");
this._appendImageScaleButtons($images, imageScaleRegex);
this._registerImageScaleButtonClick($preview, imageScaleRegex);
},
@on("willDestroyElement")
_unbindUploadTarget() {
this._validUploads = 0;
$("#reply-control .mobile-file-upload").off("click.uploader");
this.messageBus.unsubscribe("/uploads/composer");
const $uploadTarget = this.$();
try {
$uploadTarget.fileupload("destroy");
} catch (e) {
/* wasn't initialized yet */
}
$uploadTarget.off();
},
@on("willDestroyElement")
_composerClosed() {
this.appEvents.trigger("composer:will-close");
Ember.run.next(() => {
// need to wait a bit for the "slide down" transition of the composer
Ember.run.later(
() => this.appEvents.trigger("composer:closed"),
Ember.testing ? 0 : 400
);
});
if (this._enableAdvancedEditorPreviewSync())
this._teardownInputPreviewSync();
},
showUploadSelector(toolbarEvent) {
this.send("showUploadSelector", toolbarEvent);
},
onExpandPopupMenuOptions(toolbarEvent) {
const selected = toolbarEvent.selected;
toolbarEvent.selectText(selected.start, selected.end - selected.start);
this.storeToolbarState(toolbarEvent);
},
showPreview() {
const $preview = this.$(".d-editor-preview-wrapper");
this._placeImageScaleButtons($preview);
this.send("togglePreview");
},
actions: {
importQuote(toolbarEvent) {
this.importQuote(toolbarEvent);
},
onExpandPopupMenuOptions(toolbarEvent) {
this.onExpandPopupMenuOptions(toolbarEvent);
},
togglePreview() {
this.togglePreview();
},
extraButtons(toolbar) {
toolbar.addButton({
id: "quote",
group: "fontStyles",
icon: "far-comment",
sendAction: this.get("importQuote"),
title: "composer.quote_post_title",
unshift: true
});
if (
this.get("allowUpload") &&
this.get("uploadIcon") &&
!this.site.mobileView
) {
toolbar.addButton({
id: "upload",
group: "insertions",
icon: this.get("uploadIcon"),
title: "upload",
sendAction: this.get("showUploadModal")
});
}
toolbar.addButton({
id: "options",
group: "extras",
icon: "cog",
title: "composer.options",
sendAction: this.onExpandPopupMenuOptions.bind(this),
popupMenu: true
});
if (this.site.mobileView) {
toolbar.addButton({
id: "preview",
group: "mobileExtras",
icon: "television",
title: "composer.show_preview",
sendAction: this.showPreview.bind(this)
});
}
},
previewUpdated($preview) {
// Paint mentions
const unseenMentions = linkSeenMentions($preview, this.siteSettings);
if (unseenMentions.length) {
Ember.run.debounce(
this,
this._renderUnseenMentions,
$preview,
unseenMentions,
450
);
}
this._warnMentionedGroups($preview);
this._warnCannotSeeMention($preview);
// Paint category hashtags
const unseenCategoryHashtags = linkSeenCategoryHashtags($preview);
if (unseenCategoryHashtags.length) {
Ember.run.debounce(
this,
this._renderUnseenCategoryHashtags,
$preview,
unseenCategoryHashtags,
450
);
}
// Paint tag hashtags
if (this.siteSettings.tagging_enabled) {
const unseenTagHashtags = linkSeenTagHashtags($preview);
if (unseenTagHashtags.length) {
Ember.run.debounce(
this,
this._renderUnseenTagHashtags,
$preview,
unseenTagHashtags,
450
);
}
}
// Paint oneboxes
Ember.run.debounce(
this,
() => {
const inlineOneboxes = {};
const oneboxes = {};
let oneboxLeft =
this.siteSettings.max_oneboxes_per_post -
$(
`aside.onebox, a.${INLINE_ONEBOX_CSS_CLASS}, a.${LOADING_ONEBOX_CSS_CLASS}`
).length;
$preview
.find(`a.${INLINE_ONEBOX_LOADING_CSS_CLASS}, a.onebox`)
.each((_index, link) => {
const $link = $(link);
const text = $link.text();
const isInline =
$link.attr("class") === INLINE_ONEBOX_LOADING_CSS_CLASS;
const map = isInline ? inlineOneboxes : oneboxes;
if (oneboxLeft <= 0) {
if (map[text] !== undefined) {
map[text].push(link);
} else if (isInline) {
$link.removeClass(INLINE_ONEBOX_LOADING_CSS_CLASS);
}
} else {
if (!map[text]) {
map[text] = [];
oneboxLeft--;
}
map[text].push(link);
}
});
if (Object.keys(oneboxes).length > 0) {
this._loadOneboxes(oneboxes);
}
if (Object.keys(inlineOneboxes).length > 0) {
this._loadInlineOneboxes(inlineOneboxes);
}
},
450
);
// Short upload urls need resolution
resolveAllShortUrls(ajax);
if (this._enableAdvancedEditorPreviewSync()) {
this._syncScroll(
this._syncEditorAndPreviewScroll,
this.$(".d-editor-input"),
$preview
);
}
if (this.site.mobileView && $preview.is(":visible")) {
this._placeImageScaleButtons($preview);
}
this.trigger("previewRefreshed", $preview);
this.afterRefresh($preview);
}
}
});