hootstrap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +54 -0
- data/Rakefile +33 -0
- data/assets/javascripts/hootstrap/modal.js +795 -0
- data/assets/javascripts/hootstrap/rails.js +28 -0
- data/assets/javascripts/hootstrap/toast.js +166 -0
- data/assets/javascripts/hootstrap/utils/dynamicListener.js +80 -0
- data/assets/javascripts/hootstrap.js +3 -0
- data/assets/stylesheets/hootstrap/base/_base.scss +5 -0
- data/assets/stylesheets/hootstrap/base/_variables.scss +868 -0
- data/assets/stylesheets/hootstrap/base/mixins/_background.scss +8 -0
- data/assets/stylesheets/hootstrap/base/mixins/_button.scss +42 -0
- data/assets/stylesheets/hootstrap/base/mixins/_toast.scss +16 -0
- data/assets/stylesheets/hootstrap/base/mixins/_tooltip.scss +29 -0
- data/assets/stylesheets/hootstrap/components/_alert.scss +7 -0
- data/assets/stylesheets/hootstrap/components/_badge.scss +3 -0
- data/assets/stylesheets/hootstrap/components/_base.scss +6 -0
- data/assets/stylesheets/hootstrap/components/_button.scss +9 -0
- data/assets/stylesheets/hootstrap/components/_forms.scss +64 -0
- data/assets/stylesheets/hootstrap/components/_loader.scss +27 -0
- data/assets/stylesheets/hootstrap/components/_tooltip.scss +274 -0
- data/assets/stylesheets/hootstrap/patterns/_base.scss +5 -0
- data/assets/stylesheets/hootstrap/patterns/_card.scss +8 -0
- data/assets/stylesheets/hootstrap/patterns/_list-group.scss +8 -0
- data/assets/stylesheets/hootstrap/patterns/_navbar.scss +3 -0
- data/assets/stylesheets/hootstrap/patterns/_search.scss +45 -0
- data/assets/stylesheets/hootstrap/patterns/_toast.scss +34 -0
- data/assets/stylesheets/hootstrap/reset/_base.scss +1 -0
- data/assets/stylesheets/hootstrap/reset/_html.scss +4 -0
- data/assets/stylesheets/hootstrap/utils/_background.scss +3 -0
- data/assets/stylesheets/hootstrap/utils/_base.scss +5 -0
- data/assets/stylesheets/hootstrap/utils/_flex.scss +3 -0
- data/assets/stylesheets/hootstrap/utils/_opacity.scss +3 -0
- data/assets/stylesheets/hootstrap/utils/_sizing.scss +16 -0
- data/assets/stylesheets/hootstrap/utils/_transition.scss +0 -0
- data/assets/stylesheets/hootstrap/utils/_typography.scss +49 -0
- data/assets/stylesheets/hootstrap.scss +9 -0
- data/lib/hootstrap/engine.rb +9 -0
- data/lib/hootstrap/railtie.rb +10 -0
- data/lib/hootstrap/version.rb +3 -0
- data/lib/hootstrap.rb +20 -0
- metadata +141 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
//= require ./utils/dynamicListener
|
2
|
+
|
3
|
+
document.addEventListener('turbolinks:load', () => {
|
4
|
+
let confirmed = false;
|
5
|
+
|
6
|
+
addDynamicEventListener(document.body, 'click', '[data-prompt]', handleClick);
|
7
|
+
|
8
|
+
function handleClick(event) {
|
9
|
+
if (confirmed) {
|
10
|
+
confirmed = false;
|
11
|
+
return;
|
12
|
+
}
|
13
|
+
|
14
|
+
Rails.stopEverything(event);
|
15
|
+
|
16
|
+
const target = event.target;
|
17
|
+
|
18
|
+
new Toast({
|
19
|
+
message: target.getAttribute('data-prompt'),
|
20
|
+
type: 'danger',
|
21
|
+
action: 'OK',
|
22
|
+
onClick: () => {
|
23
|
+
confirmed = true;
|
24
|
+
target.click({ something: 'hello' });
|
25
|
+
}
|
26
|
+
});
|
27
|
+
}
|
28
|
+
});
|
@@ -0,0 +1,166 @@
|
|
1
|
+
/**
|
2
|
+
* Toast
|
3
|
+
*
|
4
|
+
* API
|
5
|
+
*
|
6
|
+
* @param {string} message - The text to be displayed inside the Toast.
|
7
|
+
* @param {string} type - The context: success, danger, primary, or warning.
|
8
|
+
* @param {string} [action] - The text inside the actionable button.
|
9
|
+
* @param {function} [onClick] - The callback function after actionable button is clicked.
|
10
|
+
*
|
11
|
+
*/
|
12
|
+
|
13
|
+
function Toast(options) {
|
14
|
+
if (!options.message) {
|
15
|
+
throw new Error('You need a message to use a Toast');
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
if (!options.type) {
|
20
|
+
throw new Error('You need a type to use Toast');
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
|
24
|
+
const types = ['success', 'danger', 'primary', 'warning'];
|
25
|
+
if (!types.includes(options.type)) {
|
26
|
+
throw new Error(
|
27
|
+
'You need a valid type to use Toast: `success`, `danger`, `primary`, `warning`'
|
28
|
+
);
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
if (options.action && !options.onClick) {
|
33
|
+
throw new Error('You need an onClick function to use `action`');
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
this.options = options;
|
38
|
+
this.containerEl = document.querySelector('.toast-container');
|
39
|
+
this.el = document.querySelector('.toast');
|
40
|
+
this.handleKeyup = this.handleKeyup.bind(this);
|
41
|
+
|
42
|
+
this.init();
|
43
|
+
}
|
44
|
+
|
45
|
+
Toast.prototype._createElements = function() {
|
46
|
+
return new Promise((resolve, reject) => {
|
47
|
+
this.containerEl = document.createElement('div');
|
48
|
+
this.containerEl.classList.add('toast-container');
|
49
|
+
this.containerEl.setAttribute('role', 'alert');
|
50
|
+
this.containerEl.setAttribute('aria-hidden', true);
|
51
|
+
|
52
|
+
this.el = document.createElement('div');
|
53
|
+
this.el.classList.add('toast');
|
54
|
+
|
55
|
+
this.containerEl.appendChild(this.el);
|
56
|
+
document.body.appendChild(this.containerEl);
|
57
|
+
|
58
|
+
resolve();
|
59
|
+
});
|
60
|
+
};
|
61
|
+
|
62
|
+
Toast.prototype.bindEventListeners = function() {
|
63
|
+
const dismissEl = document.querySelector('[data-dismiss="toast"]');
|
64
|
+
|
65
|
+
dismissEl.addEventListener('click', () => {
|
66
|
+
this.close();
|
67
|
+
});
|
68
|
+
|
69
|
+
if (this.options.action) {
|
70
|
+
const actionButton = document.querySelector(
|
71
|
+
'[data-behavior="toast-action"]'
|
72
|
+
);
|
73
|
+
|
74
|
+
actionButton.addEventListener('click', event => {
|
75
|
+
this.options.onClick(event);
|
76
|
+
this.close();
|
77
|
+
});
|
78
|
+
}
|
79
|
+
|
80
|
+
document.addEventListener('keyup', this.handleKeyup);
|
81
|
+
};
|
82
|
+
|
83
|
+
Toast.prototype.handleKeyup = function(event) {
|
84
|
+
if (event.keyCode == 27) {
|
85
|
+
this.close();
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
Toast.prototype.close = function() {
|
90
|
+
return new Promise((resolve, reject) => {
|
91
|
+
this.containerEl.setAttribute('aria-hidden', true);
|
92
|
+
|
93
|
+
this.el.innerHTML = '';
|
94
|
+
this.el.classList.remove(
|
95
|
+
'toast-success',
|
96
|
+
'toast-danger',
|
97
|
+
'toast-primary',
|
98
|
+
'toast-warning'
|
99
|
+
);
|
100
|
+
|
101
|
+
if (this.focusedElBeforeOpen) {
|
102
|
+
this.focusedElBeforeOpen.focus();
|
103
|
+
}
|
104
|
+
|
105
|
+
document.removeEventListener('keyup', this.handleKeyup);
|
106
|
+
|
107
|
+
resolve();
|
108
|
+
});
|
109
|
+
};
|
110
|
+
|
111
|
+
Toast.prototype._open = function() {
|
112
|
+
this.el.classList.add(`toast-${this.options.type}`);
|
113
|
+
this.containerEl.setAttribute('aria-hidden', false);
|
114
|
+
|
115
|
+
let actionButton = '';
|
116
|
+
if (this.options.action) {
|
117
|
+
actionButton = `
|
118
|
+
<button type="button"
|
119
|
+
class="btn btn-${this.options.type} btn-sm"
|
120
|
+
data-behavior="toast-action">
|
121
|
+
${this.options.action}
|
122
|
+
</button>
|
123
|
+
`;
|
124
|
+
}
|
125
|
+
|
126
|
+
const textClass = this.options.action ? '' : 'mb-0';
|
127
|
+
|
128
|
+
this.el.innerHTML = `
|
129
|
+
<button type="button" class="close" data-dismiss="toast" aria-label="Close">
|
130
|
+
<span aria-hidden="true">×</span>
|
131
|
+
</button>
|
132
|
+
|
133
|
+
<p class="${textClass}">${this.options.message}</p>
|
134
|
+
|
135
|
+
${actionButton}
|
136
|
+
`;
|
137
|
+
|
138
|
+
this.focusedElBeforeOpen = document.activeElement;
|
139
|
+
|
140
|
+
if (this.options.action) {
|
141
|
+
document.querySelector('[data-behavior="toast-action"]').focus();
|
142
|
+
} else {
|
143
|
+
document.querySelector('[data-dismiss="toast"]').focus();
|
144
|
+
}
|
145
|
+
};
|
146
|
+
|
147
|
+
Toast.prototype.init = function() {
|
148
|
+
Promise.resolve()
|
149
|
+
.then(() => {
|
150
|
+
if (this.containerEl) {
|
151
|
+
return Promise.resolve();
|
152
|
+
}
|
153
|
+
|
154
|
+
return this._createElements();
|
155
|
+
})
|
156
|
+
.then(() => {
|
157
|
+
if (this.containerEl.getAttribute('aria-hidden') == 'false') {
|
158
|
+
return this.close();
|
159
|
+
}
|
160
|
+
return Promise.resolve();
|
161
|
+
})
|
162
|
+
.then(() => {
|
163
|
+
this._open();
|
164
|
+
this.bindEventListeners();
|
165
|
+
});
|
166
|
+
};
|
@@ -0,0 +1,80 @@
|
|
1
|
+
// Credits
|
2
|
+
// https://github.com/Cristy94/dynamic-listener
|
3
|
+
//
|
4
|
+
// Usage
|
5
|
+
//
|
6
|
+
// addDynamicEventListener(document.body, 'click', '[data-prompt]', event => {
|
7
|
+
// console.log(event);
|
8
|
+
// });
|
9
|
+
(function(globalSope) {
|
10
|
+
'use strict';
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Including this file adds the `addDynamicListener` to the ELement prototype.
|
14
|
+
*
|
15
|
+
* The dynamic listener gets an extra `selector` parameter that only calls the callback
|
16
|
+
* if the target element matches the selector.
|
17
|
+
*
|
18
|
+
* The listener has to be added to the container/root element and the selector should match
|
19
|
+
* the elements that should trigger the event.
|
20
|
+
*
|
21
|
+
* Browser support: IE9+
|
22
|
+
*/
|
23
|
+
|
24
|
+
// Polyfil Element.matches
|
25
|
+
// https://developer.mozilla.org/en/docs/Web/API/Element/matches#Polyfill
|
26
|
+
if (!Element.prototype.matches) {
|
27
|
+
Element.prototype.matches =
|
28
|
+
Element.prototype.matchesSelector ||
|
29
|
+
Element.prototype.mozMatchesSelector ||
|
30
|
+
Element.prototype.msMatchesSelector ||
|
31
|
+
Element.prototype.oMatchesSelector ||
|
32
|
+
Element.prototype.webkitMatchesSelector ||
|
33
|
+
function(s) {
|
34
|
+
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
|
35
|
+
i = matches.length;
|
36
|
+
while (--i >= 0 && matches.item(i) !== this) {}
|
37
|
+
return i > -1;
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Returns a modified callback function that calls the
|
43
|
+
* initial callback function only if the target element matches the given selector
|
44
|
+
*
|
45
|
+
* @param {string} selector
|
46
|
+
* @param {function} callback
|
47
|
+
*/
|
48
|
+
function getConditionalCallback(selector, callback) {
|
49
|
+
return function(e) {
|
50
|
+
if (!e.target) return;
|
51
|
+
if (!e.target.matches(selector)) return;
|
52
|
+
callback.apply(this, arguments);
|
53
|
+
};
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
*
|
58
|
+
*
|
59
|
+
* @param {Element} rootElement The root element to add the linster too.
|
60
|
+
* @param {string} eventType The event type to listen for.
|
61
|
+
* @param {string} selector The selector that should match the dynamic elements.
|
62
|
+
* @param {function} callback The function to call when an event occurs on the given selector.
|
63
|
+
* @param {boolean|object} options Passed as the regular `options` parameter to the addEventListener function
|
64
|
+
* Set to `true` to use capture.
|
65
|
+
* Usually used as an object to add the listener as `passive`
|
66
|
+
*/
|
67
|
+
globalSope.addDynamicEventListener = function(
|
68
|
+
rootElement,
|
69
|
+
eventType,
|
70
|
+
selector,
|
71
|
+
callback,
|
72
|
+
options
|
73
|
+
) {
|
74
|
+
rootElement.addEventListener(
|
75
|
+
eventType,
|
76
|
+
getConditionalCallback(selector, callback),
|
77
|
+
options
|
78
|
+
);
|
79
|
+
};
|
80
|
+
})(this);
|