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/controllers/composer.js.es6
Robin Ward 5667478b4d A common, extensible interface for sending topic columns across the wire
This allows plugins to specify topic columns to serialize and save in
the database via the composer when creating topics and editing their
first posts.
2015-01-06 14:53:12 -05:00

473 lines
13 KiB
JavaScript

import DiscourseController from 'discourse/controllers/controller';
export default DiscourseController.extend({
needs: ['modal', 'topic', 'composer-messages'],
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY),
checkedMessages: false,
showEditReason: false,
editReason: null,
maxTitleLength: Discourse.computed.setting('max_topic_title_length'),
scopedCategoryId: null,
_initializeSimilar: function() {
this.set('similarTopics', []);
}.on('init'),
showWarning: function() {
if (!Discourse.User.currentProp('staff')) { return false; }
var usernames = this.get('model.targetUsernames');
// We need exactly one user to issue a warning
if (Ember.isEmpty(usernames) || usernames.split(',').length !== 1) {
return false;
}
return this.get('model.creatingPrivateMessage');
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
actions: {
// Toggle the reply view
toggle: function() {
this.toggle();
},
togglePreview: function() {
this.get('model').togglePreview();
},
// Import a quote from the post
importQuote: function() {
this.get('model').importQuote();
},
cancel: function() {
this.cancelComposer();
},
save: function() {
this.save();
},
displayEditReason: function() {
this.set("showEditReason", true);
},
hitEsc: function() {
if (this.get('model.viewOpen')) {
this.shrink();
}
},
openIfDraft: function() {
if (this.get('model.viewDraft')) {
this.set('model.composeState', Discourse.Composer.OPEN);
}
},
},
updateDraftStatus: function() {
var c = this.get('model');
if (c) { c.updateDraftStatus(); }
},
appendText: function(text, opts) {
var c = this.get('model');
if (c) {
opts = opts || {};
var wmd = $('#wmd-input');
var val = wmd.val() || '';
var position = opts.position === "cursor" ? wmd.caret() : val.length;
var caret = c.appendText(text, position, opts);
if(wmd[0]){
Em.run.next(function(){
Discourse.Utilities.setCaretPosition(wmd[0], caret);
});
}
}
},
appendTextAtCursor: function(text, opts) {
opts = opts || {};
opts.position = "cursor";
this.appendText(text, opts);
},
appendBlockAtCursor: function(text, opts) {
opts = opts || {};
opts.position = "cursor";
opts.block = true;
this.appendText(text, opts);
},
categories: function() {
return Discourse.Category.list();
}.property(),
toggle: function() {
this.closeAutocomplete();
switch (this.get('model.composeState')) {
case Discourse.Composer.OPEN:
if (this.blank('model.reply') && this.blank('model.title')) {
this.close();
} else {
this.shrink();
}
break;
case Discourse.Composer.DRAFT:
this.set('model.composeState', Discourse.Composer.OPEN);
break;
case Discourse.Composer.SAVING:
this.close();
}
return false;
},
disableSubmit: function() {
return this.get('model.loading');
}.property('model.loading'),
save: function(force) {
var composer = this.get('model'),
self = this;
// Clear the warning state if we're not showing the checkbox anymore
if (!this.get('showWarning')) {
this.set('model.isWarning', false);
}
if(composer.get('cantSubmitPost')) {
var now = Date.now();
this.setProperties({
'view.showTitleTip': now,
'view.showCategoryTip': now,
'view.showReplyTip': now
});
return;
}
composer.set('disableDrafts', true);
// for now handle a very narrow use case
// if we are replying to a topic AND not on the topic pop the window up
if(!force && composer.get('replyingToTopic')) {
var topic = this.get('topic');
if (!topic || topic.get('id') !== composer.get('topic.id'))
{
var message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')});
var buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}];
if(topic) {
buttons.push({
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + topic.get('title') + "</div>",
"class": "btn btn-reply-here",
"callback": function(){
composer.set('topic', topic);
composer.set('post', null);
self.save(true);
}
});
}
buttons.push({
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + this.get('model.topic.title') + "</div>",
"class": "btn-primary btn-reply-on-original",
"callback": function(){
self.save(true);
}
});
bootbox.dialog(message, buttons, {"classes": "reply-where-modal"});
return;
}
}
return composer.save({
imageSizes: this.get('view').imageSizes(),
editReason: this.get("editReason")
}).then(function(opts) {
// If we replied as a new topic successfully, remove the draft.
if (self.get('replyAsNewTopicDraft')) {
self.destroyDraft();
}
opts = opts || {};
self.close();
var currentUser = Discourse.User.current();
if (composer.get('creatingTopic')) {
currentUser.set('topic_count', currentUser.get('topic_count') + 1);
} else {
currentUser.set('reply_count', currentUser.get('reply_count') + 1);
}
if ((!composer.get('replyingToTopic')) || (!Discourse.User.currentProp('disable_jump_reply'))) {
Discourse.URL.routeTo(opts.post.get('url'));
}
}, function(error) {
composer.set('disableDrafts', false);
bootbox.alert(error);
});
},
/**
Checks to see if a reply has been typed. This is signaled by a keyUp
event in a view.
@method checkReplyLength
**/
checkReplyLength: function() {
if (this.present('model.reply')) {
// Notify the composer messages controller that a reply has been typed. Some
// messages only appear after typing.
this.get('controllers.composer-messages').typedReply();
}
},
/**
Fired after a user stops typing. Considers whether to check for similar
topics based on the current composer state.
@method findSimilarTopics
**/
findSimilarTopics: function() {
// We don't care about similar topics unless creating a topic
if (!this.get('model.creatingTopic')) return;
var body = this.get('model.reply'),
title = this.get('model.title'),
self = this,
message;
// Ensure the fields are of the minimum length
if (body.length < Discourse.SiteSettings.min_body_similar_length ||
title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
// TODO pass the 200 in from somewhere
body = body.substr(0, 200);
// Done search over and over
if((title + body) === this.get('lastSimilaritySearch')) { return; }
this.set('lastSimilaritySearch', title + body);
var messageController = this.get('controllers.composer-messages'),
similarTopics = this.get('similarTopics');
Discourse.Topic.findSimilarTo(title, body).then(function (newTopics) {
similarTopics.clear();
similarTopics.pushObjects(newTopics);
if (similarTopics.get('length') > 0) {
message = Discourse.ComposerMessage.create({
templateName: 'composer/similar_topics',
similarTopics: similarTopics,
extraClass: 'similar-topics'
});
self.set('similarTopicsMessage', message);
messageController.popup(message);
} else {
message = self.get('similarTopicsMessage');
if (message) {
messageController.send('hideMessage', message);
}
}
});
},
saveDraft: function() {
var model = this.get('model');
if (model) { model.saveDraft(); }
},
/**
Open the composer view
@method open
@param {Object} opts Options for creating a post
@param {String} opts.action The action we're performing: edit, reply or createTopic
@param {Discourse.Post} [opts.post] The post we're replying to
@param {Discourse.Topic} [opts.topic] The topic we're replying to
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
**/
open: function(opts) {
opts = opts || {};
if (!opts.draftKey) {
alert("composer was opened without a draft key");
throw "composer opened without a proper draft key";
}
// If we show the subcategory list, scope the categories drop down to
// the category we opened the composer with.
if (Discourse.SiteSettings.show_subcategory_list) {
this.set('scopedCategoryId', opts.categoryId);
}
var composerMessages = this.get('controllers.composer-messages'),
self = this,
composerModel = this.get('model');
this.setProperties({ showEditReason: false, editReason: null });
composerMessages.reset();
// If we want a different draft than the current composer, close it and clear our model.
if (composerModel && opts.draftKey !== composerModel.draftKey &&
composerModel.composeState === Discourse.Composer.DRAFT) {
this.close();
composerModel = null;
}
return new Ember.RSVP.Promise(function(resolve, reject) {
if (composerModel && composerModel.get('replyDirty')) {
// If we're already open, we don't have to do anything
if (composerModel.get('composeState') === Discourse.Composer.OPEN &&
composerModel.get('draftKey') === opts.draftKey) {
return resolve();
}
// If it's the same draft, just open it up again.
if (composerModel.get('composeState') === Discourse.Composer.DRAFT &&
composerModel.get('draftKey') === opts.draftKey) {
composerModel.set('composeState', Discourse.Composer.OPEN);
return resolve();
}
// If it's a different draft, cancel it and try opening again.
return self.cancelComposer().then(function() {
return self.open(opts);
}).then(resolve, reject);
}
// we need a draft sequence for the composer to work
if (opts.draftSequence === void 0) {
return Discourse.Draft.get(opts.draftKey).then(function(data) {
opts.draftSequence = data.draft_sequence;
opts.draft = data.draft;
self._setModel(composerModel, opts);
}).then(resolve, reject);
}
self._setModel(composerModel, opts);
resolve();
});
},
// Given a potential instance and options, set the model for this composer.
_setModel: function(composerModel, opts) {
if (opts.draft) {
composerModel = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft);
if (composerModel) {
composerModel.set('topic', opts.topic);
}
} else {
composerModel = composerModel || Discourse.Composer.create();
composerModel.open(opts);
}
this.set('model', composerModel);
composerModel.set('composeState', Discourse.Composer.OPEN);
composerModel.set('isWarning', false);
var composerMessages = this.get('controllers.composer-messages');
composerMessages.queryFor(composerModel);
},
// View a new reply we've made
viewNewReply: function() {
Discourse.URL.routeTo(this.get('createdPost.url'));
this.close();
return false;
},
destroyDraft: function() {
var key = this.get('model.draftKey');
if (key) {
Discourse.Draft.clear(key, this.get('model.draftSequence'));
}
},
cancelComposer: function() {
var self = this;
return new Ember.RSVP.Promise(function (resolve) {
if (self.get('model.hasMetaData') || self.get('model.replyDirty')) {
bootbox.confirm(I18n.t("post.abandon.confirm"), I18n.t("post.abandon.no_value"),
I18n.t("post.abandon.yes_value"), function(result) {
if (result) {
self.destroyDraft();
self.get('model').clearState();
self.close();
resolve();
}
});
} else {
// it is possible there is some sort of crazy draft with no body ... just give up on it
self.destroyDraft();
self.get('model').clearState();
self.close();
resolve();
}
});
},
shrink: function() {
if (this.get('model.replyDirty')) {
this.collapse();
} else {
this.close();
}
},
collapse: function() {
this.saveDraft();
this.set('model.composeState', Discourse.Composer.DRAFT);
},
close: function() {
this.setProperties({
model: null,
'view.showTitleTip': false,
'view.showCategoryTip': false,
'view.showReplyTip': false
});
},
closeAutocomplete: function() {
$('#wmd-input').autocomplete({ cancel: true });
},
showOptions: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
archetype: this.get('model.archetype'),
metaData: this.get('model.metaData')
})) : void 0;
},
canEdit: function() {
return this.get("model.action") === "edit" && Discourse.User.current().get("can_edit");
}.property("model.action"),
visible: function() {
var state = this.get('model.composeState');
return state && state !== 'closed';
}.property('model.composeState')
});