diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js
index 688a28d10a2..b0940bea272 100644
--- a/app/assets/javascripts/discourse/controllers/composer_controller.js
+++ b/app/assets/javascripts/discourse/controllers/composer_controller.js
@@ -39,6 +39,13 @@ Discourse.ComposerController = Discourse.Controller.extend({
buttons;
composer = this.get('content');
+
+ if( composer.get('cantSubmitPost') ) {
+ this.set('view.showTitleTip', true);
+ this.set('view.showReplyTip', true);
+ return;
+ }
+
composer.set('disableDrafts', true);
// for now handle a very narrow use case
@@ -328,6 +335,8 @@ Discourse.ComposerController = Discourse.Controller.extend({
close: function() {
this.set('content', null);
this.set('view.content', null);
+ this.set('view.showTitleTip', false);
+ this.set('view.showReplyTip', false);
},
closeIfCollapsed: function() {
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js
index 44d86537eac..dd8caec7b15 100644
--- a/app/assets/javascripts/discourse/helpers/application_helpers.js
+++ b/app/assets/javascripts/discourse/helpers/application_helpers.js
@@ -98,6 +98,21 @@ Ember.Handlebars.registerHelper('inputTip', function(options) {
return Ember.Handlebars.helpers.view.call(this, Discourse.InputTipView, options);
});
+/**
+ Inserts a Discourse.PopupInputTipView
+
+ @method popupInputTip
+ @for Handlebars
+**/
+Ember.Handlebars.registerHelper('popupInputTip', function(options) {
+ var hash = options.hash,
+ types = options.hashTypes;
+
+ normalizeHash(hash, types);
+
+ return Ember.Handlebars.helpers.view.call(this, Discourse.PopupInputTipView, options);
+});
+
/**
Produces a bound link to a category
diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
index c4a287379b6..68ceff6f64b 100644
--- a/app/assets/javascripts/discourse/models/composer.js
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -276,7 +276,9 @@ Discourse.Composer = Discourse.Model.extend({
},
save: function(opts) {
- return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts);
+ if( !this.get('cantSubmitPost') ) {
+ return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts);
+ }
},
// When you edit a post
diff --git a/app/assets/javascripts/discourse/templates/composer.js.handlebars b/app/assets/javascripts/discourse/templates/composer.js.handlebars
index 54361f62760..aab1e64fed9 100644
--- a/app/assets/javascripts/discourse/templates/composer.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/composer.js.handlebars
@@ -32,7 +32,12 @@
{{#if content.creatingPrivateMessage}}
{{view Discourse.UserSelector topicIdBinding="controller.controllers.topic.content.id" excludeCurrentUser="true" id="private-message-users" class="span8" placeholderKey="composer.users_placeholder" tabindex="1" usernamesBinding="content.targetUsernames"}}
{{/if}}
- {{textField value=content.title tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
+
+
+ {{textField value=content.title tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
+ {{popupInputTip validation=view.titleValidation show=view.showTitleTip}}
+
+
{{#unless content.creatingPrivateMessage}}
{{view Discourse.ComboboxViewCategory valueAttribute="name" contentBinding="categories" valueBinding="content.categoryName"}}
{{#if content.archetype.hasOptions}}
@@ -53,6 +58,7 @@
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="content.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
+ {{popupInputTip validation=view.replyValidation show=view.showReplyTip}}
@@ -70,7 +76,7 @@
{{#if Discourse.currentUser}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/popup_input_tip.js.handlebars b/app/assets/javascripts/discourse/templates/popup_input_tip.js.handlebars
new file mode 100644
index 00000000000..8fa9bc38f55
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/popup_input_tip.js.handlebars
@@ -0,0 +1,2 @@
+
+{{view.validation.reason}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/views/composer_view.js b/app/assets/javascripts/discourse/views/composer_view.js
index f2154519bc7..2d8b7d0b5db 100644
--- a/app/assets/javascripts/discourse/views/composer_view.js
+++ b/app/assets/javascripts/discourse/views/composer_view.js
@@ -369,7 +369,33 @@ Discourse.ComposerView = Discourse.View.extend({
$adminOpts.show();
$wmd.css('top', wmdTop + parseInt($adminOpts.css('height'),10) + 'px' );
}
- }
+ },
+
+ titleValidation: function() {
+ var title = this.get('content.title'), reason;
+ if( !title || title.length < 1 ){
+ reason = Em.String.i18n('composer.error.title_missing');
+ } else if( title.length < Discourse.SiteSettings.min_topic_title_length || title.length > Discourse.SiteSettings.max_topic_title_length ) {
+ reason = Em.String.i18n('composer.error.title_length', {min: Discourse.SiteSettings.min_topic_title_length, max: Discourse.SiteSettings.max_topic_title_length})
+ }
+
+ if( reason ) {
+ return Discourse.InputValidation.create({ failed: true, reason: reason });
+ }
+ }.property('content.title'),
+
+ replyValidation: function() {
+ var reply = this.get('content.reply'), reason;
+ if( !reply || reply.length < 1 ){
+ reason = Em.String.i18n('composer.error.post_missing');
+ } else if( reply.length < Discourse.SiteSettings.min_post_length ) {
+ reason = Em.String.i18n('composer.error.post_length', {min: Discourse.SiteSettings.min_post_length})
+ }
+
+ if( reason ) {
+ return Discourse.InputValidation.create({ failed: true, reason: reason });
+ }
+ }.property('content.reply')
});
// not sure if this is the right way, keeping here for now, we could use a mixin perhaps
diff --git a/app/assets/javascripts/discourse/views/dismissable_input_tip_view.js b/app/assets/javascripts/discourse/views/dismissable_input_tip_view.js
new file mode 100644
index 00000000000..a4c5be876c3
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/dismissable_input_tip_view.js
@@ -0,0 +1,51 @@
+/**
+ This view extends the functionality of InputTipView with these extra features:
+ * it can be dismissed
+ * it bounces when it's shown
+ * it's absolutely positioned beside the input element, with the help of
+ extra css you'll need to write to line it up correctly.
+
+ @class PopupInputTipView
+ @extends Discourse.View
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.PopupInputTipView = Discourse.View.extend({
+ templateName: 'popup_input_tip',
+ classNameBindings: [':popup-tip', 'good', 'bad', 'show::hide'],
+ animateAttribute: null,
+ bouncePixels: 6,
+ bounceDelay: 100,
+
+ good: function() {
+ return !this.get('validation.failed');
+ }.property('validation'),
+
+ bad: function() {
+ return this.get('validation.failed');
+ }.property('validation'),
+
+ hide: function() {
+ this.set('show', false);
+ },
+
+ bounce: function() {
+ var $elem = this.$()
+ if( !this.animateAttribute ) {
+ this.animateAttribute = $elem.css('left') == 'auto' ? 'right' : 'left';
+ }
+ this.animateAttribute == 'left' ? this.bounceLeft($elem) : this.bounceRight($elem);
+ }.observes('show'),
+
+ bounceLeft: function($elem) {
+ for( var i = 0; i < 5; i++ ) {
+ $elem.animate({ left: '+=' + this.bouncePixels }, this.bounceDelay).animate({ left: '-=' + this.bouncePixels }, this.bounceDelay);
+ }
+ },
+
+ bounceRight: function($elem) {
+ for( var i = 0; i < 5; i++ ) {
+ $elem.animate({ right: '-=' + this.bouncePixels }, this.bounceDelay).animate({ right: '+=' + this.bouncePixels }, this.bounceDelay);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/views/input_tip_view.js b/app/assets/javascripts/discourse/views/input_tip_view.js
index 108cf0c1d59..f2f5eca3737 100644
--- a/app/assets/javascripts/discourse/views/input_tip_view.js
+++ b/app/assets/javascripts/discourse/views/input_tip_view.js
@@ -7,7 +7,6 @@
@module Discourse
**/
Discourse.InputTipView = Discourse.View.extend({
- templateName: 'input_tip',
classNameBindings: [':tip', 'good', 'bad'],
good: function() {
diff --git a/app/assets/stylesheets/application/compose.css.scss b/app/assets/stylesheets/application/compose.css.scss
index 4d3e113c5cd..a1477934b0b 100644
--- a/app/assets/stylesheets/application/compose.css.scss
+++ b/app/assets/stylesheets/application/compose.css.scss
@@ -104,9 +104,6 @@
}
#reply-control {
- .requirements-not-met {
- background-color: rgba(255, 0, 0, 0.12);
- }
.toggle-preview, #draft-status, #image-uploading {
position: absolute;
bottom: -31px;
@@ -325,6 +322,15 @@
bottom: 8px;
}
}
+ .title-input {
+ position: relative;
+ display: inline;
+ .popup-tip {
+ width: 300px;
+ left: -8px;
+ margin-top: 8px;
+ }
+ }
}
.reply-to {
@@ -450,6 +456,10 @@ div.ac-wrap {
.textarea-wrapper {
padding-right: 5px;
float: left;
+ .popup-tip {
+ margin-top: 3px;
+ right: 4px;
+ }
}
.preview-wrapper {
padding-left: 5px;
diff --git a/app/assets/stylesheets/application/input_tip.css.scss b/app/assets/stylesheets/application/input_tip.css.scss
new file mode 100644
index 00000000000..fabfdaba7ae
--- /dev/null
+++ b/app/assets/stylesheets/application/input_tip.css.scss
@@ -0,0 +1,29 @@
+@import "foundation/variables";
+@import "foundation/mixins";
+
+.popup-tip {
+ position: absolute;
+ display: block;
+ padding: 5px 10px;
+ z-index: 101;
+ @include border-radius-all(2px);
+ border: solid 1px #955;
+ &.bad {
+ background-color: #b66;
+ color: white;
+ box-shadow: 1px 1px 5px #777, inset 0 0 9px #b55;
+ }
+ &.hide, &.good {
+ display: none;
+ }
+ a.close {
+ float: right;
+ color: $black;
+ opacity: 0.5;
+ font-size: 15px;
+ margin-left: 4px;
+ }
+ a.close:hover {
+ opacity: 1.0;
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/components/buttons.css.scss b/app/assets/stylesheets/components/buttons.css.scss
index d65cd88dcbc..21bcefbf2c7 100644
--- a/app/assets/stylesheets/components/buttons.css.scss
+++ b/app/assets/stylesheets/components/buttons.css.scss
@@ -22,7 +22,7 @@
&:active {
text-shadow: none;
}
- &[disabled] {
+ &[disabled], &.disabled {
cursor: default;
opacity: 0.4;
}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 56a34ba9d5b..b116b51a42f 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -338,6 +338,12 @@ en:
need_more_for_title: "{{n}} to go for the title"
need_more_for_reply: "{{n}} to go for the reply"
+ error:
+ title_missing: "Title is required."
+ title_length: "Title needs between {{min}} and {{max}} characters."
+ post_missing: "Post can't be empty."
+ post_length: "Post must be at least {{min}} characters long."
+
save_edit: "Save Edit"
reply_original: "Reply on Original Topic"
reply_here: "Reply Here"