From e600b45155dc4b0594a65c74f85b4556065d27c4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 24 May 2013 12:25:28 -0400 Subject: [PATCH] Composer uses bouncing popup messages beside fields with invalid values when you click the submit button --- .../controllers/composer_controller.js | 9 ++++ .../discourse/helpers/application_helpers.js | 15 ++++++ .../javascripts/discourse/models/composer.js | 4 +- .../templates/composer.js.handlebars | 10 +++- .../templates/popup_input_tip.js.handlebars | 2 + .../discourse/views/composer_view.js | 28 +++++++++- .../views/dismissable_input_tip_view.js | 51 +++++++++++++++++++ .../discourse/views/input_tip_view.js | 1 - .../stylesheets/application/compose.css.scss | 16 ++++-- .../application/input_tip.css.scss | 29 +++++++++++ .../stylesheets/components/buttons.css.scss | 2 +- config/locales/client.en.yml | 6 +++ 12 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/discourse/templates/popup_input_tip.js.handlebars create mode 100644 app/assets/javascripts/discourse/views/dismissable_input_tip_view.js create mode 100644 app/assets/stylesheets/application/input_tip.css.scss diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js index 688a28d10..b0940bea2 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 44d86537e..dd8caec7b 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 c4a287379..68ceff6f6 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 54361f627..aab1e64fe 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}}
- + {{i18n cancel}}
{{/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 000000000..8fa9bc38f --- /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 f2154519b..2d8b7d0b5 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 000000000..a4c5be876 --- /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 108cf0c1d..f2f5eca37 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 4d3e113c5..a1477934b 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 000000000..fabfdaba7 --- /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 d65cd88dc..21bcefbf2 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 56a34ba9d..b116b51a4 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"