FIX: Don't show similar topics with no results

This commit is contained in:
Robin Ward 2015-02-27 15:43:04 -05:00
parent c04b214910
commit b8ef93e0a1
2 changed files with 90 additions and 89 deletions

View File

@ -271,11 +271,8 @@ export default DiscourseController.extend({
// We don't care about similar topics unless creating a topic // We don't care about similar topics unless creating a topic
if (!this.get('model.creatingTopic')) { return; } if (!this.get('model.creatingTopic')) { return; }
let body = this.get('model.reply'), let body = this.get('model.reply');
message; const title = this.get('model.title');
const title = this.get('model.title'),
self = this;
// Ensure the fields are of the minimum length // Ensure the fields are of the minimum length
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; } if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
@ -291,24 +288,24 @@ export default DiscourseController.extend({
const messageController = this.get('controllers.composer-messages'), const messageController = this.get('controllers.composer-messages'),
similarTopics = this.get('similarTopics'); similarTopics = this.get('similarTopics');
let message = this.get('similarTopicsMessage');
if (!message) {
message = Discourse.ComposerMessage.create({
templateName: 'composer/similar_topics',
extraClass: 'similar-topics'
});
this.set('similarTopicsMessage', message);
}
Discourse.Topic.findSimilarTo(title, body).then(function (newTopics) { Discourse.Topic.findSimilarTo(title, body).then(function (newTopics) {
similarTopics.clear(); similarTopics.clear();
similarTopics.pushObjects(newTopics); similarTopics.pushObjects(newTopics);
if (similarTopics.get('length') > 0) { if (similarTopics.get('length') > 0) {
message = Discourse.ComposerMessage.create({ message.set('similarTopics', similarTopics);
templateName: 'composer/similar_topics',
similarTopics,
extraClass: 'similar-topics'
});
self.set('similarTopicsMessage', message);
messageController.send("popup", message); messageController.send("popup", message);
} else { } else if (message) {
message = self.get('similarTopicsMessage'); messageController.send("hideMessage", message);
if (message) {
messageController.send("hideMessage", message);
}
} }
}); });

View File

@ -3,7 +3,8 @@
import userSearch from 'discourse/lib/user-search'; import userSearch from 'discourse/lib/user-search';
import afterTransition from 'discourse/lib/after-transition'; import afterTransition from 'discourse/lib/after-transition';
var ComposerView = Discourse.View.extend(Ember.Evented, { const ComposerView = Discourse.View.extend(Ember.Evented, {
_lastKeyTimeout: null,
templateName: 'composer', templateName: 'composer',
elementId: 'reply-control', elementId: 'reply-control',
classNameBindings: ['model.creatingPrivateMessage:private-message', classNameBindings: ['model.creatingPrivateMessage:private-message',
@ -48,12 +49,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
Ember.run.scheduleOnce('afterRender', this, 'refreshPreview'); Ember.run.scheduleOnce('afterRender', this, 'refreshPreview');
}.observes('model.reply', 'model.hidePreview'), }.observes('model.reply', 'model.hidePreview'),
focusIn: function() { focusIn() {
var controller = this.get('controller'); const controller = this.get('controller');
if (controller) controller.updateDraftStatus(); if (controller) controller.updateDraftStatus();
}, },
movePanels: function(sizePx) { movePanels(sizePx) {
$('#main-outlet').css('padding-bottom', sizePx); $('#main-outlet').css('padding-bottom', sizePx);
$('.composer-popup').css('bottom', sizePx); $('.composer-popup').css('bottom', sizePx);
// signal the progress bar it should move! // signal the progress bar it should move!
@ -61,14 +62,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, },
resize: function() { resize: function() {
var self = this; const self = this;
Em.run.scheduleOnce('afterRender', function() { Em.run.scheduleOnce('afterRender', function() {
var h = $('#reply-control').height() || 0; const h = $('#reply-control').height() || 0;
self.movePanels.apply(self, [h + "px"]); self.movePanels.apply(self, [h + "px"]);
// Figure out the size of the fields // Figure out the size of the fields
var $fields = self.$('.composer-fields'), const $fields = self.$('.composer-fields');
pos = $fields.position(); let pos = $fields.position();
if (pos) { if (pos) {
self.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); self.$('.wmd-controls').css('top', $fields.height() + pos.top + 5);
@ -83,17 +84,19 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}); });
}.observes('model.composeState', 'model.action'), }.observes('model.composeState', 'model.action'),
keyUp: function() { keyUp() {
var controller = this.get('controller'); const controller = this.get('controller');
controller.checkReplyLength(); controller.checkReplyLength();
var lastKeyUp = new Date(); const lastKeyUp = new Date();
this.set('lastKeyUp', lastKeyUp); this.set('lastKeyUp', lastKeyUp);
// One second from now, check to see if the last key was hit when // One second from now, check to see if the last key was hit when
// we recorded it. If it was, the user paused typing. // we recorded it. If it was, the user paused typing.
var self = this; const self = this;
Em.run.later(function() {
Ember.run.cancel(this._lastKeyTimeout);
this._lastKeyTimeout = Ember.run.later(function() {
if (lastKeyUp !== self.get('lastKeyUp')) return; if (lastKeyUp !== self.get('lastKeyUp')) return;
// Search for similar topics if the user pauses typing // Search for similar topics if the user pauses typing
@ -101,7 +104,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, 1000); }, 1000);
}, },
keyDown: function(e) { keyDown(e) {
if (e.which === 27) { if (e.which === 27) {
// ESC // ESC
this.get('controller').send('hitEsc'); this.get('controller').send('hitEsc');
@ -114,12 +117,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, },
_enableResizing: function() { _enableResizing: function() {
var $replyControl = $('#reply-control'), const $replyControl = $('#reply-control'),
self = this; self = this;
$replyControl.DivResizer({ $replyControl.DivResizer({
resize: this.resize.bind(self), resize: this.resize.bind(self),
onDrag: function (sizePx) { self.movePanels.apply(self, [sizePx]); } onDrag(sizePx) { self.movePanels.apply(self, [sizePx]); }
}); });
afterTransition($replyControl, this.resize.bind(self)); afterTransition($replyControl, this.resize.bind(self));
this.ensureMaximumDimensionForImagesInPreview(); this.ensureMaximumDimensionForImagesInPreview();
@ -130,14 +133,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.set('controller.view', null); this.set('controller.view', null);
}.on('willDestroyElement'), }.on('willDestroyElement'),
ensureMaximumDimensionForImagesInPreview: function() { ensureMaximumDimensionForImagesInPreview() {
// This enforce maximum dimensions of images in the preview according // This enforce maximum dimensions of images in the preview according
// to the current site settings. // to the current site settings.
// For interactivity, we immediately insert the locally cooked version // For interactivity, we immediately insert the locally cooked version
// of the post into the stream when the user hits reply. We therefore also // of the post into the stream when the user hits reply. We therefore also
// need to enforce these rules on the .cooked version. // need to enforce these rules on the .cooked version.
// Meanwhile, the server is busy post-processing the post and generating thumbnails. // Meanwhile, the server is busy post-processing the post and generating thumbnails.
var style = Discourse.Mobile.mobileView ? const style = Discourse.Mobile.mobileView ?
'max-width: 100%; height: auto;' : 'max-width: 100%; height: auto;' :
'max-width:' + Discourse.SiteSettings.max_image_width + 'px;' + 'max-width:' + Discourse.SiteSettings.max_image_width + 'px;' +
'max-height:' + Discourse.SiteSettings.max_image_height + 'px;'; 'max-height:' + Discourse.SiteSettings.max_image_height + 'px;';
@ -145,17 +148,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
$('<style>#wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head'); $('<style>#wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
}, },
click: function() { click() {
this.get('controller').send('openIfDraft'); this.get('controller').send('openIfDraft');
}, },
// Called after the preview renders. Debounced for performance // Called after the preview renders. Debounced for performance
afterRender: function() { afterRender() {
var $wmdPreview = $('#wmd-preview'); const $wmdPreview = $('#wmd-preview');
if ($wmdPreview.length === 0) return; if ($wmdPreview.length === 0) return;
var post = this.get('model.post'), const post = this.get('model.post');
refresh = false; let refresh = false;
// If we are editing a post, we'll refresh its contents once. This is a feature that // If we are editing a post, we'll refresh its contents once. This is a feature that
// allows a user to refresh its contents once. // allows a user to refresh its contents once.
@ -175,17 +178,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.trigger('previewRefreshed', $wmdPreview); this.trigger('previewRefreshed', $wmdPreview);
}, },
_applyEmojiAutocomplete: function() { _applyEmojiAutocomplete() {
if (!this.siteSettings.enable_emoji) { return; } if (!this.siteSettings.enable_emoji) { return; }
var template = this.container.lookup('template:emoji-selector-autocomplete.raw'); const template = this.container.lookup('template:emoji-selector-autocomplete.raw');
$('#wmd-input').autocomplete({ $('#wmd-input').autocomplete({
template: template, template: template,
key: ":", key: ":",
transformComplete: function(v){ return v.code + ":"; }, transformComplete(v) { return v.code + ":"; },
dataSource: function(term){ dataSource(term){
return new Ember.RSVP.Promise(function(resolve) { return new Ember.RSVP.Promise(function(resolve) {
var full = ":" + term; const full = ":" + term;
term = term.toLowerCase(); term = term.toLowerCase();
if (term === "") { if (term === "") {
@ -196,7 +199,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return resolve([Discourse.Emoji.translations[full]]); return resolve([Discourse.Emoji.translations[full]]);
} }
var options = Discourse.Emoji.search(term, {maxResults: 5}); const options = Discourse.Emoji.search(term, {maxResults: 5});
return resolve(options); return resolve(options);
}).then(function(list) { }).then(function(list) {
@ -208,10 +211,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}); });
}, },
initEditor: function() { initEditor() {
// not quite right, need a callback to pass in, meaning this gets called once, // not quite right, need a callback to pass in, meaning this gets called once,
// but if you start replying to another topic it will get the avatars wrong // but if you start replying to another topic it will get the avatars wrong
var $wmdInput, editor, self = this; let $wmdInput, editor;
const self = this;
this.wmdInput = $wmdInput = $('#wmd-input'); this.wmdInput = $wmdInput = $('#wmd-input');
if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return; if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return;
@ -219,11 +223,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
ComposerView.trigger("initWmdEditor"); ComposerView.trigger("initWmdEditor");
this._applyEmojiAutocomplete(); this._applyEmojiAutocomplete();
var template = this.container.lookup('template:user-selector-autocomplete.raw'); const template = this.container.lookup('template:user-selector-autocomplete.raw');
$wmdInput.data('init', true); $wmdInput.data('init', true);
$wmdInput.autocomplete({ $wmdInput.autocomplete({
template: template, template: template,
dataSource: function(term) { dataSource(term) {
return userSearch({ return userSearch({
term: term, term: term,
topicId: self.get('controller.controllers.topic.model.id'), topicId: self.get('controller.controllers.topic.model.id'),
@ -231,7 +235,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}); });
}, },
key: "@", key: "@",
transformComplete: function(v) { transformComplete(v) {
if (v.username) { if (v.username) {
return v.username; return v.username;
} else { } else {
@ -241,10 +245,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}); });
this.editor = editor = Discourse.Markdown.createEditor({ this.editor = editor = Discourse.Markdown.createEditor({
lookupAvatarByPostNumber: function(postNumber) { lookupAvatarByPostNumber(postNumber) {
var posts = self.get('controller.controllers.topic.postStream.posts'); const posts = self.get('controller.controllers.topic.postStream.posts');
if (posts) { if (posts) {
var quotedPost = posts.findProperty("post_number", postNumber); const quotedPost = posts.findProperty("post_number", postNumber);
if (quotedPost) { if (quotedPost) {
return Discourse.Utilities.tinyAvatar(quotedPost.get("avatar_template")); return Discourse.Utilities.tinyAvatar(quotedPost.get("avatar_template"));
} }
@ -273,7 +277,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.set('editor', this.editor); this.set('editor', this.editor);
this.loadingChanged(); this.loadingChanged();
var saveDraft = Discourse.debounce((function() { const saveDraft = Discourse.debounce((function() {
return self.get('controller').saveDraft(); return self.get('controller').saveDraft();
}), 2000); }), 2000);
@ -282,7 +286,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return true; return true;
}); });
var $replyTitle = $('#reply-title'); const $replyTitle = $('#reply-title');
$replyTitle.keyup(function() { $replyTitle.keyup(function() {
saveDraft(); saveDraft();
@ -305,9 +309,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// in case it's still bound somehow // in case it's still bound somehow
this._unbindUploadTarget(); this._unbindUploadTarget();
var $uploadTarget = $('#reply-control'), const $uploadTarget = $('#reply-control'),
csrf = Discourse.Session.currentProp('csrfToken'), csrf = Discourse.Session.currentProp('csrfToken');
cancelledByTheUser; let cancelledByTheUser;
// NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9 // NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9
$uploadTarget.fileupload({ $uploadTarget.fileupload({
@ -318,7 +322,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// submit - this event is triggered for each upload // submit - this event is triggered for each upload
$uploadTarget.on('fileuploadsubmit', function (e, data) { $uploadTarget.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files); const result = Discourse.Utilities.validateUploadedFiles(data.files);
// reset upload status when everything is ok // reset upload status when everything is ok
if (result) self.setProperties({ uploadProgress: 0, isUploading: true }); if (result) self.setProperties({ uploadProgress: 0, isUploading: true });
return result; return result;
@ -331,7 +335,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
self.get('controller').send('closeModal'); self.get('controller').send('closeModal');
// NOTE: IE9 doesn't support XHR // NOTE: IE9 doesn't support XHR
if (data["xhr"]) { if (data["xhr"]) {
var jqHXR = data.xhr(); const jqHXR = data.xhr();
if (jqHXR) { if (jqHXR) {
// need to wait for the link to show up in the DOM // need to wait for the link to show up in the DOM
Em.run.schedule('afterRender', function() { Em.run.schedule('afterRender', function() {
@ -351,7 +355,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// progress all // progress all
$uploadTarget.on('fileuploadprogressall', function (e, data) { $uploadTarget.on('fileuploadprogressall', function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10); const progress = parseInt(data.loaded / data.total * 100, 10);
self.set('uploadProgress', progress); self.set('uploadProgress', progress);
}); });
@ -360,7 +364,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
if (!cancelledByTheUser) { if (!cancelledByTheUser) {
// make sure we have a url // make sure we have a url
if (data.result.url) { if (data.result.url) {
var markdown = Discourse.Utilities.getUploadMarkdown(data.result); const markdown = Discourse.Utilities.getUploadMarkdown(data.result);
// appends a space at the end of the inserted markdown // appends a space at the end of the inserted markdown
self.addMarkdown(markdown + " "); self.addMarkdown(markdown + " ");
self.set('isUploading', false); self.set('isUploading', false);
@ -385,7 +389,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// Firefox. This is pretty dangerous because it can potentially break // Firefox. This is pretty dangerous because it can potentially break
// Ctrl+v to paste so we should be conservative about what browsers this runs // Ctrl+v to paste so we should be conservative about what browsers this runs
// in. // in.
var uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/); const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
if (uaMatch && parseInt(uaMatch[1]) >= 24) { if (uaMatch && parseInt(uaMatch[1]) >= 24) {
self.$().append( Ember.$("<div id='contenteditable' contenteditable='true' style='height: 0; width: 0; overflow: hidden'></div>") ); self.$().append( Ember.$("<div id='contenteditable' contenteditable='true' style='height: 0; width: 0; overflow: hidden'></div>") );
self.$("textarea").off('keydown.contenteditable'); self.$("textarea").off('keydown.contenteditable');
@ -395,18 +399,18 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// after we switch focus, probably because it is being executed too late. // after we switch focus, probably because it is being executed too late.
if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) { if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) {
// Save the current textarea selection. // Save the current textarea selection.
var textarea = self.$("textarea")[0], const textarea = self.$("textarea")[0],
selectionStart = textarea.selectionStart, selectionStart = textarea.selectionStart,
selectionEnd = textarea.selectionEnd; selectionEnd = textarea.selectionEnd;
// Focus the contenteditable div. // Focus the contenteditable div.
var contentEditableDiv = self.$('#contenteditable'); const contentEditableDiv = self.$('#contenteditable');
contentEditableDiv.focus(); contentEditableDiv.focus();
// The paste doesn't finish immediately and we don't have any onpaste // The paste doesn't finish immediately and we don't have any onpaste
// event, so wait for 100ms which _should_ be enough time. // event, so wait for 100ms which _should_ be enough time.
setTimeout(function() { setTimeout(function() {
var pastedImg = contentEditableDiv.find('img'); const pastedImg = contentEditableDiv.find('img');
if ( pastedImg.length === 1 ) { if ( pastedImg.length === 1 ) {
pastedImg.remove(); pastedImg.remove();
@ -414,11 +418,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// For restoring the selection. // For restoring the selection.
textarea.focus(); textarea.focus();
var textareaContent = $(textarea).val(), const textareaContent = $(textarea).val(),
startContent = textareaContent.substring(0, selectionStart), startContent = textareaContent.substring(0, selectionStart),
endContent = textareaContent.substring(selectionEnd); endContent = textareaContent.substring(selectionEnd);
var restoreSelection = function(pastedText) { const restoreSelection = function(pastedText) {
$(textarea).val( startContent + pastedText + endContent ); $(textarea).val( startContent + pastedText + endContent );
textarea.selectionStart = selectionStart + pastedText.length; textarea.selectionStart = selectionStart + pastedText.length;
textarea.selectionEnd = textarea.selectionStart; textarea.selectionEnd = textarea.selectionStart;
@ -435,20 +439,20 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// to a Blob and upload that, but if it is a regular URL that // to a Blob and upload that, but if it is a regular URL that
// operation is prevented for security purposes. When we get a regular // operation is prevented for security purposes. When we get a regular
// URL let's just create an <img> tag for the image. // URL let's just create an <img> tag for the image.
var imageSrc = pastedImg.attr('src'); const imageSrc = pastedImg.attr('src');
if (imageSrc.match(/^data:image/)) { if (imageSrc.match(/^data:image/)) {
// Restore the cursor position, and remove any selected text. // Restore the cursor position, and remove any selected text.
restoreSelection(""); restoreSelection("");
// Create a Blob to upload. // Create a Blob to upload.
var image = new Image(); const image = new Image();
image.onload = function() { image.onload = function() {
// Create a new canvas. // Create a new canvas.
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
canvas.height = image.height; canvas.height = image.height;
canvas.width = image.width; canvas.width = image.width;
var ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0); ctx.drawImage(image, 0, 0);
canvas.toBlob(function(blob) { canvas.toBlob(function(blob) {
@ -488,8 +492,8 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, 400); }, 400);
}, },
addMarkdown: function(text) { addMarkdown(text) {
var ctrl = $('#wmd-input').get(0), const ctrl = $('#wmd-input').get(0),
caretPosition = Discourse.Utilities.caretPosition(ctrl), caretPosition = Discourse.Utilities.caretPosition(ctrl),
current = this.get('model.reply'); current = this.get('model.reply');
this.set('model.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length)); this.set('model.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length));
@ -500,10 +504,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, },
// Uses javascript to get the image sizes from the preview, if present // Uses javascript to get the image sizes from the preview, if present
imageSizes: function() { imageSizes() {
var result = {}; const result = {};
$('#wmd-preview img').each(function(i, e) { $('#wmd-preview img').each(function(i, e) {
var $img = $(e), const $img = $(e),
src = $img.prop('src'); src = $img.prop('src');
if (src && src.length) { if (src && src.length) {
@ -513,12 +517,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return result; return result;
}, },
childDidInsertElement: function() { childDidInsertElement() {
return this.initEditor(); return this.initEditor();
}, },
childWillDestroyElement: function() { childWillDestroyElement() {
var self = this; const self = this;
this._unbindUploadTarget(); this._unbindUploadTarget();
@ -532,9 +536,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, },
titleValidation: function() { titleValidation: function() {
var titleLength = this.get('model.titleLength'), const titleLength = this.get('model.titleLength'),
missingChars = this.get('model.missingTitleCharacters'), missingChars = this.get('model.missingTitleCharacters');
reason; let reason;
if( titleLength < 1 ){ if( titleLength < 1 ){
reason = I18n.t('composer.error.title_missing'); reason = I18n.t('composer.error.title_missing');
} else if( missingChars > 0 ) { } else if( missingChars > 0 ) {
@ -555,9 +559,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}.property('model.categoryId'), }.property('model.categoryId'),
replyValidation: function() { replyValidation: function() {
var replyLength = this.get('model.replyLength'), const replyLength = this.get('model.replyLength'),
missingChars = this.get('model.missingReplyCharacters'), missingChars = this.get('model.missingReplyCharacters');
reason; let reason;
if( replyLength < 1 ){ if( replyLength < 1 ){
reason = I18n.t('composer.error.post_missing'); reason = I18n.t('composer.error.post_missing');
} else if( missingChars > 0 ) { } else if( missingChars > 0 ) {
@ -569,8 +573,8 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
} }
}.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'), }.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'),
_unbindUploadTarget: function() { _unbindUploadTarget() {
var $uploadTarget = $('#reply-control'); const $uploadTarget = $('#reply-control');
try { $uploadTarget.fileupload('destroy'); } try { $uploadTarget.fileupload('destroy'); }
catch (e) { /* wasn't initialized yet */ } catch (e) { /* wasn't initialized yet */ }
$uploadTarget.off(); $uploadTarget.off();