mirror of
https://github.com/discourse/discourse.git
synced 2025-01-30 23:11:44 +08:00
Composer uses bouncing popup messages beside fields with invalid values when you click the submit button
This commit is contained in:
parent
962f0dd5f9
commit
e600b45155
|
@ -39,6 +39,13 @@ Discourse.ComposerController = Discourse.Controller.extend({
|
||||||
buttons;
|
buttons;
|
||||||
|
|
||||||
composer = this.get('content');
|
composer = this.get('content');
|
||||||
|
|
||||||
|
if( composer.get('cantSubmitPost') ) {
|
||||||
|
this.set('view.showTitleTip', true);
|
||||||
|
this.set('view.showReplyTip', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
composer.set('disableDrafts', true);
|
composer.set('disableDrafts', true);
|
||||||
|
|
||||||
// for now handle a very narrow use case
|
// for now handle a very narrow use case
|
||||||
|
@ -328,6 +335,8 @@ Discourse.ComposerController = Discourse.Controller.extend({
|
||||||
close: function() {
|
close: function() {
|
||||||
this.set('content', null);
|
this.set('content', null);
|
||||||
this.set('view.content', null);
|
this.set('view.content', null);
|
||||||
|
this.set('view.showTitleTip', false);
|
||||||
|
this.set('view.showReplyTip', false);
|
||||||
},
|
},
|
||||||
|
|
||||||
closeIfCollapsed: function() {
|
closeIfCollapsed: function() {
|
||||||
|
|
|
@ -98,6 +98,21 @@ Ember.Handlebars.registerHelper('inputTip', function(options) {
|
||||||
return Ember.Handlebars.helpers.view.call(this, Discourse.InputTipView, 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
|
Produces a bound link to a category
|
||||||
|
|
|
@ -276,7 +276,9 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function(opts) {
|
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
|
// When you edit a post
|
||||||
|
|
|
@ -32,7 +32,12 @@
|
||||||
{{#if content.creatingPrivateMessage}}
|
{{#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"}}
|
{{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}}
|
{{/if}}
|
||||||
{{textField value=content.title tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
|
|
||||||
|
<div class="title-input">
|
||||||
|
{{textField value=content.title tabindex="2" id="reply-title" maxlength="255" class="span8" placeholderKey="composer.title_placeholder"}}
|
||||||
|
{{popupInputTip validation=view.titleValidation show=view.showTitleTip}}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{#unless content.creatingPrivateMessage}}
|
{{#unless content.creatingPrivateMessage}}
|
||||||
{{view Discourse.ComboboxViewCategory valueAttribute="name" contentBinding="categories" valueBinding="content.categoryName"}}
|
{{view Discourse.ComboboxViewCategory valueAttribute="name" contentBinding="categories" valueBinding="content.categoryName"}}
|
||||||
{{#if content.archetype.hasOptions}}
|
{{#if content.archetype.hasOptions}}
|
||||||
|
@ -53,6 +58,7 @@
|
||||||
<div class='textarea-wrapper'>
|
<div class='textarea-wrapper'>
|
||||||
<div class='wmd-button-bar' id='wmd-button-bar'></div>
|
<div class='wmd-button-bar' id='wmd-button-bar'></div>
|
||||||
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="content.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
|
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="content.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
|
||||||
|
{{popupInputTip validation=view.replyValidation show=view.showReplyTip}}
|
||||||
</div>
|
</div>
|
||||||
<div class='preview-wrapper'>
|
<div class='preview-wrapper'>
|
||||||
<div id='wmd-preview' {{bindAttr class="hidePreview:hidden"}}></div>
|
<div id='wmd-preview' {{bindAttr class="hidePreview:hidden"}}></div>
|
||||||
|
@ -70,7 +76,7 @@
|
||||||
|
|
||||||
{{#if Discourse.currentUser}}
|
{{#if Discourse.currentUser}}
|
||||||
<div class='submit-panel'>
|
<div class='submit-panel'>
|
||||||
<button {{action save}} tabindex="4" {{bindAttr disabled="content.cantSubmitPost"}} class='btn btn-primary create'>{{view.content.saveText}}</button>
|
<button {{action save}} tabindex="4" {{bindAttr class=":btn :btn-primary :create content.cantSubmitPost:disabled"}}>{{view.content.saveText}}</button>
|
||||||
<a href='#' {{action cancel}} class='cancel' tabindex="4">{{i18n cancel}}</a>
|
<a href='#' {{action cancel}} class='cancel' tabindex="4">{{i18n cancel}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<a href="#" class="close" {{action hide target="view"}}><i class="icon icon-remove-sign"></i></a>
|
||||||
|
{{view.validation.reason}}
|
|
@ -369,7 +369,33 @@ Discourse.ComposerView = Discourse.View.extend({
|
||||||
$adminOpts.show();
|
$adminOpts.show();
|
||||||
$wmd.css('top', wmdTop + parseInt($adminOpts.css('height'),10) + 'px' );
|
$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
|
// not sure if this is the right way, keeping here for now, we could use a mixin perhaps
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -7,7 +7,6 @@
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.InputTipView = Discourse.View.extend({
|
Discourse.InputTipView = Discourse.View.extend({
|
||||||
templateName: 'input_tip',
|
|
||||||
classNameBindings: [':tip', 'good', 'bad'],
|
classNameBindings: [':tip', 'good', 'bad'],
|
||||||
|
|
||||||
good: function() {
|
good: function() {
|
||||||
|
|
|
@ -104,9 +104,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-control {
|
#reply-control {
|
||||||
.requirements-not-met {
|
|
||||||
background-color: rgba(255, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
.toggle-preview, #draft-status, #image-uploading {
|
.toggle-preview, #draft-status, #image-uploading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -31px;
|
bottom: -31px;
|
||||||
|
@ -325,6 +322,15 @@
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.title-input {
|
||||||
|
position: relative;
|
||||||
|
display: inline;
|
||||||
|
.popup-tip {
|
||||||
|
width: 300px;
|
||||||
|
left: -8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-to {
|
.reply-to {
|
||||||
|
@ -450,6 +456,10 @@ div.ac-wrap {
|
||||||
.textarea-wrapper {
|
.textarea-wrapper {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
float: left;
|
float: left;
|
||||||
|
.popup-tip {
|
||||||
|
margin-top: 3px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.preview-wrapper {
|
.preview-wrapper {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
|
29
app/assets/stylesheets/application/input_tip.css.scss
Normal file
29
app/assets/stylesheets/application/input_tip.css.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@
|
||||||
&:active {
|
&:active {
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
&[disabled] {
|
&[disabled], &.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
|
@ -338,6 +338,12 @@ en:
|
||||||
need_more_for_title: "{{n}} to go for the title"
|
need_more_for_title: "{{n}} to go for the title"
|
||||||
need_more_for_reply: "{{n}} to go for the reply"
|
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"
|
save_edit: "Save Edit"
|
||||||
reply_original: "Reply on Original Topic"
|
reply_original: "Reply on Original Topic"
|
||||||
reply_here: "Reply Here"
|
reply_here: "Reply Here"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user