nail_polish 0.1.1 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +2 -4
- data/app/assets/images/nail_polish/elements/link_action.png +0 -0
- data/app/assets/images/nail_polish/elements/white_down_arrow.png +0 -0
- data/app/assets/javascripts/nail_polish.js +6 -1
- data/app/assets/javascripts/nail_polish/app.js +32 -18
- data/app/assets/javascripts/nail_polish/events.js +2 -0
- data/app/assets/javascripts/nail_polish/model.js +49 -0
- data/app/assets/javascripts/nail_polish/models/dropdown.js +9 -0
- data/app/assets/javascripts/nail_polish/models/menu.js +1 -0
- data/app/assets/javascripts/nail_polish/nail_polish.js +2 -0
- data/app/assets/javascripts/nail_polish/presenter.js +34 -7
- data/app/assets/javascripts/nail_polish/presenter_with_errors.js +61 -0
- data/app/assets/javascripts/nail_polish/presenters/dropdown.js +7 -12
- data/app/assets/javascripts/nail_polish/presenters/menu.js +7 -0
- data/app/assets/javascripts/nail_polish/router.js +23 -13
- data/app/assets/javascripts/nail_polish/templates/dropdown.mustache +14 -0
- data/app/assets/javascripts/nail_polish/templates/menu.mustache +22 -0
- data/app/assets/javascripts/nail_polish/templates/modal.mustache +5 -0
- data/app/assets/javascripts/nail_polish/utils/subview_manager.js +28 -0
- data/app/assets/javascripts/nail_polish/validator.js +147 -0
- data/app/assets/javascripts/nail_polish/view.js +17 -8
- data/app/assets/javascripts/nail_polish/views/form.js +44 -0
- data/app/assets/javascripts/nail_polish/widget/dropdown.js +37 -9
- data/app/assets/javascripts/nail_polish/widget/flash.js +8 -8
- data/app/assets/javascripts/nail_polish/widget/menu.js +61 -0
- data/app/assets/javascripts/nail_polish/widget/modal.js +35 -5
- data/app/assets/javascripts/nail_polish_widgets.js +1 -0
- data/app/assets/stylesheets/nail_polish/base.scss +1 -1
- data/app/assets/stylesheets/nail_polish/base/buttons.scss +5 -0
- data/app/assets/stylesheets/nail_polish/base/form_elements.scss +15 -1
- data/app/assets/stylesheets/nail_polish/base/grid.scss +8 -6
- data/app/assets/stylesheets/nail_polish/base/layout.scss +3 -2
- data/app/assets/stylesheets/nail_polish/base/oo_styles.scss +37 -2
- data/app/assets/stylesheets/nail_polish/base/reset.scss +5 -0
- data/app/assets/stylesheets/nail_polish/base/typography_and_tags.scss +4 -0
- data/app/assets/stylesheets/nail_polish/desktop.scss +1 -1
- data/app/assets/stylesheets/nail_polish/desktop/grid.scss +6 -1
- data/app/assets/stylesheets/nail_polish/desktop/widgets/menu.scss +61 -0
- data/app/assets/stylesheets/nail_polish/tablet.scss +0 -1
- data/app/assets/stylesheets/nail_polish/tablet/grid.scss +3 -0
- data/lib/nail_polish/version.rb +1 -1
- metadata +43 -14
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="row dropdown-toggle {{name}}-dropdown-toggle">
|
2
|
+
<div class="unit">
|
3
|
+
<div id="{{name}}" class="dropdown-selected-option" data-option='{{selected_key}}'>
|
4
|
+
{{selected_value}}
|
5
|
+
</div>
|
6
|
+
</div>
|
7
|
+
<div class="unit-right caret"></div>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<ul class="dropdown-menu {{name}}-dropdown-menu hidden">
|
11
|
+
{{#selectable_items}}
|
12
|
+
<li class="menu-item" data-option={{key}}>{{value}}</li>
|
13
|
+
{{/selectable_items}}
|
14
|
+
</ul>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<div class='inner-right menu-item-wrapper'>
|
2
|
+
<div class='menu-name'>
|
3
|
+
<a href='javascript:void(0);' class="small menu-trigger button">
|
4
|
+
<span class="white-down-arrow">{{trigger_link_text}}</span>
|
5
|
+
</a>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<div class='menu-items hidden'>
|
9
|
+
<div class='menu-caret'></div>
|
10
|
+
{{#items}}
|
11
|
+
<div class='row menu-item'>
|
12
|
+
<div class='inner half'>
|
13
|
+
<a href="{{path}}">
|
14
|
+
<h3 class="icon {{icon_class}}">
|
15
|
+
{{link_text}}
|
16
|
+
</h3>
|
17
|
+
</a>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
{{/items}}
|
21
|
+
</div>
|
22
|
+
</div>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
NailPolish.SubviewManager = {
|
2
|
+
renderEach: function (subviews) {
|
3
|
+
this._subviews = subviews;
|
4
|
+
|
5
|
+
_.each(subviews, function (view) {
|
6
|
+
if(view) {
|
7
|
+
view.parent = view.parent || this.defaultParent();
|
8
|
+
view.repository = view.repository || this.repository;
|
9
|
+
view.render();
|
10
|
+
}
|
11
|
+
}.bind(this));
|
12
|
+
},
|
13
|
+
|
14
|
+
remove: function() {
|
15
|
+
this.removeSelf();
|
16
|
+
|
17
|
+
_.each(this._subviews, function(subview) {
|
18
|
+
if (subview){
|
19
|
+
subview.remove();
|
20
|
+
}
|
21
|
+
});
|
22
|
+
},
|
23
|
+
|
24
|
+
removeSelf: function() { /* override me */ },
|
25
|
+
defaultParent: function() {
|
26
|
+
return this;
|
27
|
+
}
|
28
|
+
};
|
@@ -0,0 +1,147 @@
|
|
1
|
+
/*
|
2
|
+
Provides errors object builder for Backbone.js models.
|
3
|
+
Follows Backbone.js validation conventions.
|
4
|
+
*/
|
5
|
+
|
6
|
+
NailPolish.Validator = function() {
|
7
|
+
this._attributeRuleSets = {};
|
8
|
+
};
|
9
|
+
|
10
|
+
/*
|
11
|
+
Starts the validations chain for a particular attribute(s).
|
12
|
+
See documentation for `NailPolish.Validator.RuleSet.prototype.addRule` below.
|
13
|
+
*/
|
14
|
+
NailPolish.Validator.prototype.attribute = function() {
|
15
|
+
var ruleSet = new NailPolish.Validator.RuleSet();
|
16
|
+
|
17
|
+
_.each(arguments, function(attributeName) {
|
18
|
+
var attributeRuleSet = this._attributeRuleSets[attributeName] ||
|
19
|
+
new NailPolish.Validator.RuleSet();
|
20
|
+
attributeRuleSet.chain(ruleSet);
|
21
|
+
this._attributeRuleSets[attributeName] = attributeRuleSet;
|
22
|
+
}, this);
|
23
|
+
|
24
|
+
return ruleSet;
|
25
|
+
};
|
26
|
+
|
27
|
+
/*
|
28
|
+
Returns an object containing errors or `undefined` if there were none.
|
29
|
+
|
30
|
+
Example:
|
31
|
+
|
32
|
+
validate.validate()
|
33
|
+
|
34
|
+
// {
|
35
|
+
// "username" : ["is too short", "must not contain whitespace"],
|
36
|
+
// "age": ["is not a number"]
|
37
|
+
// }
|
38
|
+
*/
|
39
|
+
NailPolish.Validator.prototype.validate = function(attributes) {
|
40
|
+
var errors = {};
|
41
|
+
|
42
|
+
_.each(attributes, function(attributeValue, attributeName) {
|
43
|
+
var ruleSet = this._attributeRuleSets[attributeName];
|
44
|
+
if (!ruleSet) return;
|
45
|
+
|
46
|
+
_.each(ruleSet.rules(), function(rule) {
|
47
|
+
if (rule(attributeValue, attributeName)) {
|
48
|
+
errors[attributeName] = errors[attributeName] || [];
|
49
|
+
errors[attributeName].push(rule.message);
|
50
|
+
}
|
51
|
+
}, this);
|
52
|
+
}, this);
|
53
|
+
|
54
|
+
return _.isEmpty(errors) ? undefined : errors;
|
55
|
+
};
|
56
|
+
|
57
|
+
/*
|
58
|
+
Built-in validations.
|
59
|
+
|
60
|
+
Usage:
|
61
|
+
|
62
|
+
var validate = new NailPolish.Validator();
|
63
|
+
validate.
|
64
|
+
attribute("username").
|
65
|
+
addRule("is too short", validate.is.tooShort(5));
|
66
|
+
|
67
|
+
Extending:
|
68
|
+
|
69
|
+
_.extend(NailPolish.Validator.prototype.is, {
|
70
|
+
whitespaceFree: function() {
|
71
|
+
return function(value) { return undefined !== value && value.match(/\s/); }
|
72
|
+
}
|
73
|
+
});
|
74
|
+
|
75
|
+
...
|
76
|
+
|
77
|
+
validate.
|
78
|
+
attribute("username").
|
79
|
+
addRule("must not contain whitespace", validate.is.whitespaceFree());
|
80
|
+
*/
|
81
|
+
(function() {
|
82
|
+
var notEmpty = function(value) {
|
83
|
+
return null != value && "" !== value;
|
84
|
+
};
|
85
|
+
|
86
|
+
NailPolish.Validator.prototype.is = {
|
87
|
+
required: function() {
|
88
|
+
return function(value) { return !notEmpty(value); };
|
89
|
+
},
|
90
|
+
|
91
|
+
tooShort: function(minLength) {
|
92
|
+
return function(value) { return notEmpty(value) && value.length < minLength; };
|
93
|
+
},
|
94
|
+
|
95
|
+
tooLong: function(maxLength) {
|
96
|
+
return function(value) { return notEmpty(value) && value.length > maxLength; };
|
97
|
+
},
|
98
|
+
|
99
|
+
notMatching: function(pattern) {
|
100
|
+
return function(value) { return notEmpty(value) && !String(value).match(pattern); }
|
101
|
+
}
|
102
|
+
};
|
103
|
+
})();
|
104
|
+
|
105
|
+
NailPolish.Validator.RuleSet = function() {
|
106
|
+
this._rules = [];
|
107
|
+
this._chainedRuleSets = [];
|
108
|
+
};
|
109
|
+
|
110
|
+
/*
|
111
|
+
Defines validation rule and an error message for a particular attribute(s).
|
112
|
+
|
113
|
+
Attribute is considered to be *invalid* if the validation function returns a truthy value.
|
114
|
+
|
115
|
+
Multiple calls to `addRule` can be chained to add as many validations as needed.
|
116
|
+
|
117
|
+
In case of custom validation functions, it's up to you to make sure that
|
118
|
+
validation functions don't depend on each other and are capable of handling
|
119
|
+
any *possible* input, be it `null`, `undefined`, or an actual value.
|
120
|
+
|
121
|
+
Usage:
|
122
|
+
|
123
|
+
validate.
|
124
|
+
attribute("username", "email").
|
125
|
+
addRule("is already in use", function(attrValue, attrName) {
|
126
|
+
return this.collection.detect(function(user) {
|
127
|
+
return user.get(attrName) == attrValue;
|
128
|
+
});
|
129
|
+
}.bind(this));
|
130
|
+
*/
|
131
|
+
NailPolish.Validator.RuleSet.prototype.addRule = function(errorMessage, validatingFunc) {
|
132
|
+
validatingFunc.message = errorMessage;
|
133
|
+
this._rules.push(validatingFunc);
|
134
|
+
return this;
|
135
|
+
}
|
136
|
+
|
137
|
+
/* private */
|
138
|
+
NailPolish.Validator.RuleSet.prototype.chain = function(ruleSet) {
|
139
|
+
this._chainedRuleSets.push(ruleSet);
|
140
|
+
}
|
141
|
+
|
142
|
+
/* private */
|
143
|
+
NailPolish.Validator.RuleSet.prototype.rules = function() {
|
144
|
+
return _.reduce(this._chainedRuleSets, function(memo, ruleSet) {
|
145
|
+
return memo.concat(ruleSet.rules());
|
146
|
+
}, this._rules);
|
147
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
NailPolish.View = Backbone.View.extend({
|
1
|
+
NailPolish.View = Backbone.View.extend(_.extend(_.clone(NailPolish.SubviewManager), {
|
2
2
|
addListeners: {},
|
3
3
|
presenterClass: function () {
|
4
4
|
return NailPolish.Presenter;
|
@@ -8,7 +8,14 @@ NailPolish.View = Backbone.View.extend({
|
|
8
8
|
opts = opts || {};
|
9
9
|
this.parent = (this.parent && $(this.parent)) || opts.parent;
|
10
10
|
this.repository = opts.repository;
|
11
|
-
this.attachmentMethod = this.attachmentMethod || 'append';
|
11
|
+
this.attachmentMethod = opts.attachmentMethod || this.attachmentMethod || 'append';
|
12
|
+
this.parentSelector = opts.parentSelector || this.parentSelector;
|
13
|
+
this.templateName = opts.templateName || this.templateName;
|
14
|
+
if (opts.presenterClass) {
|
15
|
+
this.presenterClass = function() {
|
16
|
+
return opts.presenterClass;
|
17
|
+
};
|
18
|
+
}
|
12
19
|
this.init(opts);
|
13
20
|
},
|
14
21
|
|
@@ -27,11 +34,8 @@ NailPolish.View = Backbone.View.extend({
|
|
27
34
|
return [];
|
28
35
|
},
|
29
36
|
|
30
|
-
renderSubviews: function
|
31
|
-
|
32
|
-
view.parent = view.parent || this;
|
33
|
-
view.render();
|
34
|
-
}.bind(this));
|
37
|
+
renderSubviews: function() {
|
38
|
+
this.renderEach(this.subviews());
|
35
39
|
},
|
36
40
|
|
37
41
|
attachToParent: function () {
|
@@ -70,11 +74,16 @@ NailPolish.View = Backbone.View.extend({
|
|
70
74
|
presenter: function () {
|
71
75
|
var presented = this.model || this.collection || {};
|
72
76
|
var presenter = new (this.presenterClass())(presented);
|
77
|
+
presenter.repository = presenter.repository || this.repository;
|
73
78
|
return presenter.toJSON();
|
74
79
|
},
|
75
80
|
|
76
81
|
partials: function () {
|
77
82
|
// override with own partials if necessary
|
78
83
|
return {};
|
84
|
+
},
|
85
|
+
|
86
|
+
removeSelf: function() {
|
87
|
+
Backbone.View.prototype.remove.apply(this);
|
79
88
|
}
|
80
|
-
});
|
89
|
+
}));
|
@@ -0,0 +1,44 @@
|
|
1
|
+
NailPolish.Views.Form = NailPolish.View.extend({
|
2
|
+
formAttrs: [], /* implement in subclass */
|
3
|
+
|
4
|
+
init: function(){
|
5
|
+
this.listenTo(this.model, 'invalid', this.addErrors);
|
6
|
+
},
|
7
|
+
|
8
|
+
readAttributes: function() {
|
9
|
+
var attrs = {};
|
10
|
+
|
11
|
+
_.each(this.formAttrs, function(name) {
|
12
|
+
attrs[name] = this.readAttribute(name);
|
13
|
+
}.bind(this));
|
14
|
+
|
15
|
+
_.extend(attrs, this.readAdditionalAttributes());
|
16
|
+
|
17
|
+
if(this.model.id) { _.extend(attrs, {'_method' : 'put'}); }
|
18
|
+
return attrs;
|
19
|
+
},
|
20
|
+
|
21
|
+
readAttribute: function(attr) {
|
22
|
+
return this.$('[name=' + attr + ']').val();
|
23
|
+
},
|
24
|
+
|
25
|
+
readAdditionalAttributes: function() {
|
26
|
+
return {}; /* override for additional form stuff */
|
27
|
+
},
|
28
|
+
|
29
|
+
addErrors: function() {
|
30
|
+
var errorKeys = _.keys(this.model.validationError);
|
31
|
+
var selector = _.map(errorKeys, function(key) {
|
32
|
+
return ".input-row." + key;
|
33
|
+
}).join(', ');
|
34
|
+
this.$(selector).addClass('error');
|
35
|
+
},
|
36
|
+
|
37
|
+
clearErrors: function() {
|
38
|
+
_.each(this.formAttrs, function(key) {
|
39
|
+
this.model.set(key + '_error', undefined);
|
40
|
+
}.bind(this));
|
41
|
+
|
42
|
+
this.$('.input-row').removeClass('error');
|
43
|
+
}
|
44
|
+
});
|
@@ -1,22 +1,48 @@
|
|
1
1
|
NailPolish.Widget.Dropdown = NailPolish.View.extend({
|
2
|
-
templateName: 'templates/dropdown',
|
2
|
+
templateName: 'nail_polish/templates/dropdown',
|
3
3
|
|
4
4
|
events: {
|
5
5
|
'click .dropdown-toggle': 'toggle',
|
6
|
-
'click .menu-item': 'setSelected'
|
6
|
+
'click .menu-item': 'setSelected',
|
7
|
+
'click .dropdown-menu': 'stopPropagation'
|
7
8
|
},
|
8
9
|
|
10
|
+
hiddenClass: 'hidden',
|
11
|
+
|
9
12
|
init: function(){
|
10
|
-
this.className = this.model.name + '-drop-down';
|
11
|
-
this.menuSelector = '.' + this.model.name + '-dropdown-menu';
|
13
|
+
this.className = this.model.get('name') + '-drop-down';
|
14
|
+
this.menuSelector = '.' + this.model.get('name') + '-dropdown-menu';
|
12
15
|
},
|
13
16
|
|
14
17
|
presenterClass: function () {
|
15
18
|
return NailPolish.Presenter.Dropdown;
|
16
19
|
},
|
17
20
|
|
18
|
-
toggle: function() {
|
19
|
-
|
21
|
+
toggle: function(e) {
|
22
|
+
if (this.menuIsVisible()) {
|
23
|
+
this.hideMenu.bind(this)();
|
24
|
+
} else {
|
25
|
+
this.showMenu();
|
26
|
+
e.stopPropagation();
|
27
|
+
}
|
28
|
+
},
|
29
|
+
|
30
|
+
menuIsVisible: function() {
|
31
|
+
return this.$(this.menuSelector).is(':visible');
|
32
|
+
},
|
33
|
+
|
34
|
+
hideMenu: function() {
|
35
|
+
this.$el.find(this.menuSelector).addClass(this.hiddenClass);
|
36
|
+
this.removeBodyListener();
|
37
|
+
},
|
38
|
+
|
39
|
+
showMenu: function() {
|
40
|
+
this.$el.find(this.menuSelector).removeClass(this.hiddenClass);
|
41
|
+
$('body').on(NailPolish.Events.click, this.hideMenu.bind(this));
|
42
|
+
},
|
43
|
+
|
44
|
+
removeBodyListener: function() {
|
45
|
+
$('body').off(NailPolish.Events.click, this.hideMenu.bind(this));
|
20
46
|
},
|
21
47
|
|
22
48
|
renderTemplate: function () {
|
@@ -26,15 +52,17 @@ NailPolish.Widget.Dropdown = NailPolish.View.extend({
|
|
26
52
|
this.$el.html(rendered);
|
27
53
|
},
|
28
54
|
|
29
|
-
hiddenClass: 'hidden',
|
30
|
-
|
31
55
|
setSelected: function(e){
|
32
56
|
var $target = $(e.target);
|
33
|
-
this.model.
|
57
|
+
this.model.set('selected_key', $target.attr("data-option"));
|
34
58
|
this.render();
|
35
59
|
this.afterSelect();
|
36
60
|
},
|
37
61
|
|
62
|
+
stopPropagation: function(e) {
|
63
|
+
e.stopPropagation();
|
64
|
+
},
|
65
|
+
|
38
66
|
afterSelect: function() {
|
39
67
|
//overwrite me in subclass if you need to do stuffs
|
40
68
|
},
|
@@ -3,7 +3,7 @@ NailPolish.Widget.Flash = NailPolish.View.extend({
|
|
3
3
|
className: 'flash',
|
4
4
|
parentSelector: '#flash-container',
|
5
5
|
attachmentMethod: 'html',
|
6
|
-
animationLength:
|
6
|
+
animationLength: 1000,
|
7
7
|
showFor: 2000,
|
8
8
|
|
9
9
|
addListeners: {
|
@@ -20,17 +20,17 @@ NailPolish.Widget.Flash = NailPolish.View.extend({
|
|
20
20
|
},
|
21
21
|
|
22
22
|
show: function () {
|
23
|
-
window.scrollTo(0, 0);
|
24
|
-
this.$el.css('opacity', 0);
|
25
23
|
this.$el.show();
|
26
|
-
this.$el.
|
27
|
-
|
24
|
+
this.$el.find('.flash').css('opacity', 1);
|
28
25
|
setTimeout(this.hide.bind(this), this.showFor);
|
29
26
|
},
|
30
27
|
|
31
28
|
hide: function () {
|
32
|
-
this.$el.
|
33
|
-
|
34
|
-
|
29
|
+
this.$el.find('.flash').css('opacity', 0);
|
30
|
+
setTimeout(this.removeFlash.bind(this), this.animationLength);
|
31
|
+
},
|
32
|
+
|
33
|
+
removeFlash: function() {
|
34
|
+
this.$el.hide();
|
35
35
|
}
|
36
36
|
});
|
@@ -0,0 +1,61 @@
|
|
1
|
+
NailPolish.Widget.Menu = NailPolish.View.extend({
|
2
|
+
parentSelector: '.menu-container',
|
3
|
+
templateName: 'nail_polish/templates/menu',
|
4
|
+
addListeners: {
|
5
|
+
"click .menu-trigger": 'toggleMenu',
|
6
|
+
"click .menu-item a": 'menuItemClick'
|
7
|
+
},
|
8
|
+
|
9
|
+
onMenuItemClick: function (e) {
|
10
|
+
// hook in here
|
11
|
+
},
|
12
|
+
|
13
|
+
presenterClass: function() {
|
14
|
+
return NailPolish.Presenter.Menu;
|
15
|
+
},
|
16
|
+
|
17
|
+
toggleMenu: function(e) {
|
18
|
+
if (this.menuIsVisible()) {
|
19
|
+
this.hideMenu();
|
20
|
+
} else {
|
21
|
+
this.hideAllMenus();
|
22
|
+
this.showMenu(e);
|
23
|
+
}
|
24
|
+
},
|
25
|
+
|
26
|
+
menuIsVisible: function() {
|
27
|
+
return this.$('.menu-items').is(':visible');
|
28
|
+
},
|
29
|
+
|
30
|
+
showMenu: function(e) {
|
31
|
+
this.$('.menu-trigger').addClass("selected");
|
32
|
+
this.$('.menu-items').css("display", "block");
|
33
|
+
this.onBodyClick = this.hideMenu.bind(this);
|
34
|
+
$('body').on(NailPolish.Events.click, this.onBodyClick);
|
35
|
+
e.stopPropagation();
|
36
|
+
e.preventDefault();
|
37
|
+
},
|
38
|
+
|
39
|
+
hideMenu: function() {
|
40
|
+
this.$('.menu-trigger').removeClass("selected");
|
41
|
+
this.$('.menu-items').css("display", "none");
|
42
|
+
this.removeBodyListener();
|
43
|
+
},
|
44
|
+
|
45
|
+
hideAllMenus: function() {
|
46
|
+
$('.menu-trigger').removeClass("selected");
|
47
|
+
$('.menu-items').css("display", "none");
|
48
|
+
},
|
49
|
+
|
50
|
+
removeBodyListener: function() {
|
51
|
+
if(this.onBodyClick){
|
52
|
+
$('body').off(NailPolish.Events.click, this.onBodyClick);
|
53
|
+
this.onBodyClick = undefined;
|
54
|
+
}
|
55
|
+
},
|
56
|
+
|
57
|
+
menuItemClick: function(e) {
|
58
|
+
this.onMenuItemClick(e);
|
59
|
+
this.hideMenu();
|
60
|
+
}
|
61
|
+
});
|