tips 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/app/assets/images/tip_throbber.png +0 -0
- data/app/assets/javascripts/tips.js +582 -0
- data/app/assets/stylesheets/tips.css +279 -0
- data/lib/tips/engine.rb +4 -0
- data/lib/tips/version.rb +3 -0
- data/lib/tips.rb +6 -0
- data/tips.gemspec +23 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9684df1fded8d68ca41ee1ce6f427df5af471b34
|
4
|
+
data.tar.gz: dfbf087e71d2d7d8d9553bf560e00e1254b45081
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 405cd0a5dcb1b15ee664b066a23149bb893dc4aff4d18c3b6e5635c20c2911a3339b6d7fe1a588a520af4f19443dfcb293a1d501e32c0fcdb013bd176e8204d3
|
7
|
+
data.tar.gz: a1f9c2b17f0230b56b385db8d3a756689eb4cc819688405fd86d51f4efbdd63143ac1ee771ee5f6ad9b90274b1e4ba6d4a55c3adc5b875b47a3fed0dddcf052d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ryan Guerrettaz
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Tips
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'tips'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install tips
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( http://github.com/<my-github-username>/tips/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
Binary file
|
@@ -0,0 +1,582 @@
|
|
1
|
+
;(function setup(window){
|
2
|
+
|
3
|
+
function initTips(){
|
4
|
+
function Validation(config, settableAttrs, acceptableValues) {
|
5
|
+
|
6
|
+
function logInvalidKeyError(key){
|
7
|
+
console.log(key + ' is not a valid config key.')
|
8
|
+
}
|
9
|
+
|
10
|
+
function logInvalidValueError(attrKey, attrVal){
|
11
|
+
console.log(attrVal + ' is not a valid value for ' + attrKey)
|
12
|
+
}
|
13
|
+
|
14
|
+
Object.keys(config).forEach(function(key){
|
15
|
+
if (settableAttrs.indexOf(key) > -1) {
|
16
|
+
var value = config[key]
|
17
|
+
|
18
|
+
if (acceptableValues[key].indexOf(config[key]) === -1) {
|
19
|
+
logInvalidValueError(key, value)
|
20
|
+
|
21
|
+
// Remove the invalid key from the config hash
|
22
|
+
config[key] = undefined
|
23
|
+
}
|
24
|
+
} else logInvalidKeyError(key)
|
25
|
+
})
|
26
|
+
}
|
27
|
+
|
28
|
+
//
|
29
|
+
//
|
30
|
+
// STORAGE DEVICE
|
31
|
+
//
|
32
|
+
//
|
33
|
+
var tipsCustomStorageDevice;
|
34
|
+
var storageDevice;
|
35
|
+
var defaultStorageDevice = {
|
36
|
+
optOutKey: 'tips-opted-out',
|
37
|
+
|
38
|
+
addTip: function(tipName){
|
39
|
+
localStorage.setItem(tipName, true);
|
40
|
+
},
|
41
|
+
|
42
|
+
tipHasBeenSeen: function(tipName){
|
43
|
+
return !!localStorage.getItem(tipName);
|
44
|
+
},
|
45
|
+
|
46
|
+
optOut: function(){
|
47
|
+
localStorage.setItem(this.optOutKey, true);
|
48
|
+
},
|
49
|
+
|
50
|
+
userHasOptedOut: function(){
|
51
|
+
return !!localStorage.getItem(this.optOutKey);
|
52
|
+
},
|
53
|
+
|
54
|
+
removeAll: function(tipNames){
|
55
|
+
tipNames.push(this.optOutKey)
|
56
|
+
tipNames.forEach(function(key){
|
57
|
+
localStorage.removeItem(key)
|
58
|
+
})
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
62
|
+
if (tipsCustomStorageDevice == undefined) storageDevice = defaultStorageDevice;
|
63
|
+
else storageDevice = tipsCustomStorageDevice();
|
64
|
+
|
65
|
+
//
|
66
|
+
//
|
67
|
+
// TIPS
|
68
|
+
//
|
69
|
+
//
|
70
|
+
var Tips = {
|
71
|
+
list: [],
|
72
|
+
|
73
|
+
userOptedOut: function(){ return storageDevice.userHasOptedOut() },
|
74
|
+
|
75
|
+
reset: function(){ storageDevice.removeAll(this.tipNames()) },
|
76
|
+
|
77
|
+
tipNames: function(){
|
78
|
+
var names = [];
|
79
|
+
this.list.forEach( function(tip){ names.push(tip.name) } )
|
80
|
+
return names
|
81
|
+
},
|
82
|
+
|
83
|
+
showNextTip: function(){
|
84
|
+
// Guard clause
|
85
|
+
if (this.userOptedOut()) return
|
86
|
+
var unseen = Tips.unseen()
|
87
|
+
if (unseen.length != 0) unseen[0].hotSpot.show()
|
88
|
+
},
|
89
|
+
|
90
|
+
unseen: function() {
|
91
|
+
var unseen = [];
|
92
|
+
this.list.forEach(function(tip){
|
93
|
+
if (!tip.hasBeenSeen()) unseen.push(tip)
|
94
|
+
})
|
95
|
+
return unseen
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
//
|
100
|
+
//
|
101
|
+
// TIP
|
102
|
+
//
|
103
|
+
//
|
104
|
+
function Tip(el) {
|
105
|
+
this.name = 'tips:' + el.getAttribute('data-tips-name');
|
106
|
+
this.priority = el.getAttribute('data-tips-priority') || 1;
|
107
|
+
this.pages = el.getAttribute('data-tips-pages') || 1;
|
108
|
+
this.tipNode = el;
|
109
|
+
this.height = 246;
|
110
|
+
this.width = 485;
|
111
|
+
this.hotspotPosition = el.getAttribute('data-tips-hot-spot-position') || 'right';
|
112
|
+
this.cardPosition = el.getAttribute('data-tips-card-position') || 'right';
|
113
|
+
this.tipCard = new TipCard(el, this);
|
114
|
+
this.hotSpot = new HotSpot(this);
|
115
|
+
}
|
116
|
+
|
117
|
+
Tip.prototype.hasBeenSeen = function() {
|
118
|
+
//Look in storage device for tip seen
|
119
|
+
return !!storageDevice.tipHasBeenSeen(this.name)
|
120
|
+
}
|
121
|
+
|
122
|
+
//
|
123
|
+
//
|
124
|
+
// HOT SPOT
|
125
|
+
//
|
126
|
+
//
|
127
|
+
function HotSpot(tip) {
|
128
|
+
var hotSpot = {};
|
129
|
+
var hotSpotSpecs = {
|
130
|
+
width: 56,
|
131
|
+
height: 56,
|
132
|
+
middle: 56/2
|
133
|
+
};
|
134
|
+
|
135
|
+
hotSpot.tipCard = tip.tipCard;
|
136
|
+
hotSpot.tip = tip;
|
137
|
+
hotSpot.position = tip.hotspotPosition;
|
138
|
+
hotSpot.tipNode = tip.tipNode;
|
139
|
+
hotSpot.tipName = tip.name;
|
140
|
+
hotSpot.hotSpotNode = createHotSpotNode();
|
141
|
+
|
142
|
+
hotSpot.show = function(){
|
143
|
+
this.hotSpotNode.style.display = '';
|
144
|
+
}
|
145
|
+
|
146
|
+
hotSpot.hide = function(){
|
147
|
+
this.hotSpotNode.style.display = 'none';
|
148
|
+
}
|
149
|
+
|
150
|
+
// Private functions
|
151
|
+
|
152
|
+
function createHotSpotNode(){
|
153
|
+
var bounds = hotSpot.tipNode.getBoundingClientRect();
|
154
|
+
var hotSpotNode = document.createElement('div');
|
155
|
+
var offsetLeft = calcualteHotSpotOffsetLeft(bounds);
|
156
|
+
var offsetTop = calculateHotSpotOffsetTop(bounds);
|
157
|
+
|
158
|
+
hotSpotNode.className = 'tips-throbber';
|
159
|
+
hotSpotNode.style.position = 'absolute';
|
160
|
+
hotSpotNode.style.top = offsetTop + 'px';
|
161
|
+
hotSpotNode.style.left = offsetLeft + 'px';
|
162
|
+
hotSpotNode.style.display = 'none';
|
163
|
+
|
164
|
+
addHotSpotListener(hotSpotNode);
|
165
|
+
|
166
|
+
document.body.appendChild(hotSpotNode);
|
167
|
+
|
168
|
+
return hotSpotNode
|
169
|
+
}
|
170
|
+
|
171
|
+
function calculateHotSpotOffsetTop(bounds) {
|
172
|
+
var topPxLength = 0;
|
173
|
+
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset :
|
174
|
+
(document.documentElement ||
|
175
|
+
document.body.parentNode || document.body).scrollTop;
|
176
|
+
var topPositions = ['top-left', 'top', 'top-right'];
|
177
|
+
var bottomPositions = ['bottom-left', 'bottom', 'bottom-right'];
|
178
|
+
var middlePositions = ['middle', 'left', 'right'];
|
179
|
+
|
180
|
+
if (topPositions.indexOf(hotSpot.position) !== -1) {
|
181
|
+
topPxLength = bounds.top - hotSpotSpecs.middle + scrollTop;
|
182
|
+
} else if (bottomPositions.indexOf(hotSpot.position) !== -1) {
|
183
|
+
topPxLength = bounds.top + bounds.height - hotSpotSpecs.middle +
|
184
|
+
scrollTop;
|
185
|
+
} else if (middlePositions.indexOf(hotSpot.position) !== -1) {
|
186
|
+
topPxLength = bounds.top + bounds.height/2 - hotSpotSpecs.middle +
|
187
|
+
scrollTop;
|
188
|
+
} else { console.log(hotSpot.position + ' is an invalid position hotSpot position') };
|
189
|
+
|
190
|
+
return topPxLength;
|
191
|
+
}
|
192
|
+
|
193
|
+
function calcualteHotSpotOffsetLeft(bounds) {
|
194
|
+
var leftPositions = ['top-left', 'left', 'bottom-left'];
|
195
|
+
var rightPositions = ['top-right', 'right', 'bottom-right'];
|
196
|
+
var middlePositions = ['middle', 'top', 'bottom'];
|
197
|
+
var scrollLeft = (window.pageXOffset !== undefined) ?
|
198
|
+
window.pageXOffset : (document.documentElement ||
|
199
|
+
document.body.parentNode ||
|
200
|
+
document.body).scrollLeft;
|
201
|
+
|
202
|
+
if (leftPositions.indexOf(hotSpot.position) !== -1) {
|
203
|
+
return bounds.left - hotSpotSpecs.middle + scrollLeft;
|
204
|
+
} else if (rightPositions.indexOf(hotSpot.position) !== -1) {
|
205
|
+
return bounds.left + bounds.width - hotSpotSpecs.middle + scrollLeft;
|
206
|
+
} else if (middlePositions.indexOf(hotSpot.position) !== -1) {
|
207
|
+
return bounds.left + bounds.width/2 - hotSpotSpecs.middle + scrollLeft;
|
208
|
+
} else { console.log(hotSpot.position + ' is an invalid hotSpot position') };
|
209
|
+
}
|
210
|
+
|
211
|
+
function addHotSpotListener(hotSpotNode){
|
212
|
+
hotSpotNode.addEventListener('click', function(){
|
213
|
+
storeThatTipHasBeenSeen();
|
214
|
+
showTipCard();
|
215
|
+
hotSpot.hide();
|
216
|
+
})
|
217
|
+
}
|
218
|
+
|
219
|
+
function storeThatTipHasBeenSeen(){
|
220
|
+
storageDevice.addTip(hotSpot.tipName);
|
221
|
+
}
|
222
|
+
|
223
|
+
function showTipCard(){
|
224
|
+
hotSpot.tipCard.show();
|
225
|
+
}
|
226
|
+
|
227
|
+
return hotSpot
|
228
|
+
}
|
229
|
+
|
230
|
+
//
|
231
|
+
//
|
232
|
+
// TIP CARD
|
233
|
+
//
|
234
|
+
//
|
235
|
+
function TipCard(tipNode, tip) {
|
236
|
+
var tipCard = {};
|
237
|
+
tipCard.tip = tip;
|
238
|
+
tipCard.tipNode = tipNode;
|
239
|
+
tipCard.content = JSON.parse(tipNode.getAttribute('data-tips-content'));
|
240
|
+
tipCard.position = tip.cardPosition;
|
241
|
+
tipCard.cardNode = createCardNode();
|
242
|
+
tipCard.scritNode = createScritNode();
|
243
|
+
|
244
|
+
tipCard.show = function(){
|
245
|
+
this.cardNode.style.display = '';
|
246
|
+
this.scritNode.style.display = '';
|
247
|
+
|
248
|
+
// Highlight the tip node
|
249
|
+
// For z-index to work the tipNode must have a positon in the following:
|
250
|
+
// ['absolute', 'relative', 'fixed']
|
251
|
+
this.tipNode.style.zIndex = 9999;
|
252
|
+
};
|
253
|
+
|
254
|
+
tipCard.hide = function(){
|
255
|
+
this.cardNode.style.display = 'none';
|
256
|
+
this.scritNode.style.display = 'none';
|
257
|
+
this.tipNode.style.zIndex = 0;
|
258
|
+
Tips.showNextTip();
|
259
|
+
}
|
260
|
+
|
261
|
+
// Private functions
|
262
|
+
|
263
|
+
function createCardNode(){
|
264
|
+
var bounds = tipCard.tipNode.getBoundingClientRect();
|
265
|
+
var cardNode = document.createElement('div');
|
266
|
+
cardNode.className = 'tips-card tips-card-' + tipCard.position;
|
267
|
+
cardNode.appendChild(createContent());
|
268
|
+
cardNode.appendChild(createOptOut())
|
269
|
+
cardNode.style.display = 'none';
|
270
|
+
cardNode.style.position = 'absolute';
|
271
|
+
cardNode.style.width = tip.width + 'px';
|
272
|
+
cardNode.style.left = calculateOffsetLeft(bounds, cardNode);
|
273
|
+
cardNode.style.top = calculateOffsetTop(bounds, cardNode);
|
274
|
+
cardNode.style.zIndex = 9999;
|
275
|
+
|
276
|
+
document.body.appendChild(cardNode);
|
277
|
+
return cardNode;
|
278
|
+
}
|
279
|
+
|
280
|
+
function createContent(){
|
281
|
+
var pageCount = tip.pages
|
282
|
+
var content = document.createElement('div')
|
283
|
+
content.className = 'tips-content-wrapper'
|
284
|
+
|
285
|
+
for(var i = 1; i <= pageCount; i++){
|
286
|
+
var lastPage = false;
|
287
|
+
|
288
|
+
if(i == pageCount) lastPage = true;
|
289
|
+
|
290
|
+
content.appendChild(createPage(i, lastPage))
|
291
|
+
}
|
292
|
+
|
293
|
+
return content
|
294
|
+
}
|
295
|
+
|
296
|
+
function createPage(pageNumber, lastPage){
|
297
|
+
var page = document.createElement('div')
|
298
|
+
|
299
|
+
if (pageNumber === 1) {
|
300
|
+
page.className = 'tips-content-page-' + pageNumber +
|
301
|
+
' tips-last-page-' + lastPage + ' tips-active-tip' +
|
302
|
+
' tips-page';
|
303
|
+
} else {
|
304
|
+
page.className = 'tips-content-page-' + pageNumber +
|
305
|
+
' tips-last-page-' + lastPage + ' tips-page';
|
306
|
+
}
|
307
|
+
|
308
|
+
page.innerHTML = headerHtml(pageNumber) + bodyHtml(pageNumber);
|
309
|
+
page.zIndex = 100 - pageNumber;
|
310
|
+
|
311
|
+
if (pageNumber !== 1) page.style.display = 'none';
|
312
|
+
|
313
|
+
page.appendChild(createNavigation(lastPage, pageNumber))
|
314
|
+
|
315
|
+
return page
|
316
|
+
}
|
317
|
+
|
318
|
+
function headerHtml(pageNumber) {
|
319
|
+
return '<h2>' + tipCard.content['page' + pageNumber + 'Header'] +
|
320
|
+
'</h2>'
|
321
|
+
}
|
322
|
+
|
323
|
+
function bodyHtml(pageNumber) {
|
324
|
+
return '<p>' + tipCard.content['page' + pageNumber + 'Body'] +
|
325
|
+
'</p>'
|
326
|
+
}
|
327
|
+
|
328
|
+
function createNavigation(lastPage, pageNumber) {
|
329
|
+
var wrapper = document.createElement('div')
|
330
|
+
wrapper.className = 'tips-navigation-wrapper'
|
331
|
+
|
332
|
+
if (tip.pages > 1) {
|
333
|
+
wrapper.appendChild(createNavigationalDots(pageNumber))
|
334
|
+
}
|
335
|
+
|
336
|
+
if (lastPage) { wrapper.appendChild(createDoneButton()) }
|
337
|
+
else { wrapper.appendChild(createNextButton()) }
|
338
|
+
|
339
|
+
return wrapper
|
340
|
+
}
|
341
|
+
|
342
|
+
function createNextButton(){
|
343
|
+
var wrapper = document.createElement('div');
|
344
|
+
wrapper.className = 'tips-btn-wrapper'
|
345
|
+
var btn = document.createElement('button');
|
346
|
+
btn.className = 'tips-btn tips-next-btn tips-green';
|
347
|
+
btn.textContent = 'Next';
|
348
|
+
addNextButtonListener(btn);
|
349
|
+
wrapper.appendChild(btn)
|
350
|
+
|
351
|
+
return wrapper
|
352
|
+
}
|
353
|
+
|
354
|
+
function addNextButtonListener(btn){
|
355
|
+
btn.addEventListener('click', function(e){
|
356
|
+
e.preventDefault();
|
357
|
+
|
358
|
+
// TODO: traversing DOM seems nasty, must be better way
|
359
|
+
removeClass(this.parentNode.parentNode.parentNode, 'tips-active-tip')
|
360
|
+
this.parentNode.parentNode.parentNode.style.display = 'none';
|
361
|
+
|
362
|
+
this.parentNode.parentNode.parentNode.nextSibling.style.display = '';
|
363
|
+
addClass(this.parentNode.parentNode.parentNode.nextSibling, 'tips-active-tip')
|
364
|
+
})
|
365
|
+
}
|
366
|
+
|
367
|
+
function createDoneButton() {
|
368
|
+
var wrapper = document.createElement('div');
|
369
|
+
wrapper.className = 'tips-btn-wrapper';
|
370
|
+
var btn = document.createElement('button')
|
371
|
+
btn.className = 'tips-btn tips-done-btn tips-red';
|
372
|
+
btn.textContent = 'Done';
|
373
|
+
addDoneButtonListener(btn);
|
374
|
+
wrapper.appendChild(btn)
|
375
|
+
|
376
|
+
return wrapper
|
377
|
+
}
|
378
|
+
|
379
|
+
function addDoneButtonListener(btn){
|
380
|
+
btn.addEventListener('click', function(){
|
381
|
+
tipCard.hide();
|
382
|
+
})
|
383
|
+
}
|
384
|
+
|
385
|
+
function createNavigationalDots(pageNumber){
|
386
|
+
var dots = document.createElement('div');
|
387
|
+
dots.className = 'tips-navigational-dots-wrapper'
|
388
|
+
|
389
|
+
for(var i = 1; i <= tip.pages; i++){
|
390
|
+
dots.appendChild(createDot(pageNumber, i))
|
391
|
+
}
|
392
|
+
|
393
|
+
return dots;
|
394
|
+
}
|
395
|
+
|
396
|
+
function createDot(parentPageNumber, pageNumber){
|
397
|
+
// var dot = document.createElement('div');
|
398
|
+
var dotClass = 'tips-navigational-dot';
|
399
|
+
var activeDotClass = 'tips-active-dot';
|
400
|
+
var dot = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
401
|
+
var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
402
|
+
dot.setAttribute('data-tips-page-number', pageNumber);
|
403
|
+
|
404
|
+
if(parentPageNumber == pageNumber) dot.setAttribute('class', dotClass + ' ' + activeDotClass)
|
405
|
+
else dot.setAttribute('class', dotClass)
|
406
|
+
|
407
|
+
dot.setAttribute('height', '6');
|
408
|
+
dot.setAttribute('width' ,'6');
|
409
|
+
|
410
|
+
circle.setAttribute('cx', '3');
|
411
|
+
circle.setAttribute('cy', '3');
|
412
|
+
circle.setAttribute('r', '3');
|
413
|
+
circle.setAttribute('fill', 'red');
|
414
|
+
|
415
|
+
dot.appendChild(circle);
|
416
|
+
|
417
|
+
addDotListener(dot)
|
418
|
+
|
419
|
+
return dot;
|
420
|
+
}
|
421
|
+
|
422
|
+
function addDotListener(dot){
|
423
|
+
dot.addEventListener('click', function(){
|
424
|
+
hideActiveDotsCard(dot);
|
425
|
+
showClickedDotsCard(dot);
|
426
|
+
})
|
427
|
+
}
|
428
|
+
|
429
|
+
function hideActiveDotsCard(dot) {
|
430
|
+
// TODO: YUCK!!!!! Safari doesn't support .closest
|
431
|
+
// Surely there must be a cleaner/safer way to find this el
|
432
|
+
dot.parentNode.parentNode.parentNode.style.display = 'none';
|
433
|
+
}
|
434
|
+
|
435
|
+
function showClickedDotsCard(dot) {
|
436
|
+
var pageNumber = dot.getAttribute('data-tips-page-number')
|
437
|
+
|
438
|
+
// TODO: find cleaner way to find parent el
|
439
|
+
var pageToShow = dot.parentNode.parentNode.parentNode.parentNode
|
440
|
+
.getElementsByClassName('tips-content-page-' +
|
441
|
+
pageNumber)[0];
|
442
|
+
pageToShow.style.display = '';
|
443
|
+
}
|
444
|
+
|
445
|
+
|
446
|
+
function createOptOut() {
|
447
|
+
var optOutWrapper = document.createElement('span');
|
448
|
+
var optOutLink = document.createElement('a');
|
449
|
+
optOutWrapper.className = 'tips-opt-out';
|
450
|
+
optOutWrapper.textContent = 'Seen this before?'
|
451
|
+
optOutLink.className = 'tips-opt-out-link'
|
452
|
+
optOutLink.textContent = ' Opt out of these tips.'
|
453
|
+
optOutLink.setAttribute('href', '#')
|
454
|
+
optOutWrapper.appendChild(optOutLink)
|
455
|
+
addOptOutListener(optOutLink)
|
456
|
+
return optOutWrapper
|
457
|
+
}
|
458
|
+
|
459
|
+
function addOptOutListener(optOutLink) {
|
460
|
+
optOutLink.addEventListener('click', function(e){
|
461
|
+
e.preventDefault();
|
462
|
+
storageDevice.optOut();
|
463
|
+
tipCard.hide();
|
464
|
+
})
|
465
|
+
};
|
466
|
+
|
467
|
+
// TODO: A tipCard should know the optimal location for its speech
|
468
|
+
// bubble point based on the window boundaries. For now it the speech
|
469
|
+
// bubble point will always be centered on the tipNode
|
470
|
+
function calculateOffsetLeft(bounds, cardNode){
|
471
|
+
// length of speech box tip
|
472
|
+
var pointerLength = 15;
|
473
|
+
var scrollLeft = (window.pageXOffset !== undefined) ?
|
474
|
+
window.pageXOffset : (document.documentElement ||
|
475
|
+
document.body.parentNode ||
|
476
|
+
document.body).scrollLeft;
|
477
|
+
if(tipCard.position === 'right'){
|
478
|
+
return bounds.left + bounds.width + pointerLength +
|
479
|
+
scrollLeft + 'px';
|
480
|
+
} else if(tipCard.position === 'left'){
|
481
|
+
return bounds.left - cardNode.style.width.replace('px', '')/1 -
|
482
|
+
pointerLength + scrollLeft + 'px';
|
483
|
+
} else if (['top', 'bottom'].indexOf(tipCard.position) !== -1){
|
484
|
+
return bounds.left - cardNode.style.width.replace('px', '')/2 +
|
485
|
+
bounds.width/2 + scrollLeft + 'px';
|
486
|
+
} else {
|
487
|
+
console.log(tipCard.position + ' is an invalid tipCard position.')
|
488
|
+
|
489
|
+
// Same position as right as it's the default position, for now
|
490
|
+
return bounds.left + bounds.width + scrollLeft + 'px';
|
491
|
+
}
|
492
|
+
};
|
493
|
+
|
494
|
+
function calculateOffsetTop(bounds, cardNode){
|
495
|
+
var pointerLength = 15;
|
496
|
+
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset :
|
497
|
+
(document.documentElement ||
|
498
|
+
document.body.parentNode || document.body).scrollTop;
|
499
|
+
|
500
|
+
if(['right', 'left'].indexOf(tipCard.position) !== -1){
|
501
|
+
return bounds.top + bounds.height/2 - tip.height/2 + scrollTop + 'px';
|
502
|
+
} else if(tipCard.position === 'top'){
|
503
|
+
return bounds.top - tip.height - pointerLength + scrollTop + 'px';
|
504
|
+
} else if(tipCard.position === 'bottom'){
|
505
|
+
return bounds.top + bounds.height + pointerLength + scrollTop + 'px';
|
506
|
+
} else {
|
507
|
+
// Same position as right as it's the default position, for now
|
508
|
+
return bounds.top - bounds.height/2 +
|
509
|
+
tip.height/2 + scrollTop + 'px';
|
510
|
+
}
|
511
|
+
};
|
512
|
+
|
513
|
+
function createScritNode(){
|
514
|
+
var scritNode = document.createElement('div')
|
515
|
+
scritNode.className = 'shadow';
|
516
|
+
scritNode.style.height = '100%';
|
517
|
+
scritNode.style.width = '100%';
|
518
|
+
scritNode.style.zIndex = 9998;
|
519
|
+
scritNode.style.backgroundColor = 'rgb(95, 95, 95)';
|
520
|
+
scritNode.style.opacity = '0.5';
|
521
|
+
scritNode.style.position = 'fixed';
|
522
|
+
scritNode.style.left = '0px';
|
523
|
+
scritNode.style.top = '0px';
|
524
|
+
scritNode.style.display = 'none';
|
525
|
+
|
526
|
+
addScritNodeListener(scritNode);
|
527
|
+
|
528
|
+
document.body.appendChild(scritNode)
|
529
|
+
return scritNode
|
530
|
+
}
|
531
|
+
|
532
|
+
function addScritNodeListener(scritNode){
|
533
|
+
scritNode.addEventListener('click',function(){
|
534
|
+
tipCard.hide();
|
535
|
+
})
|
536
|
+
}
|
537
|
+
|
538
|
+
return tipCard;
|
539
|
+
}
|
540
|
+
|
541
|
+
// Helper functions
|
542
|
+
|
543
|
+
function addClass(node, className) {
|
544
|
+
node.className = node.className + ' ' + className;
|
545
|
+
}
|
546
|
+
|
547
|
+
function removeClass(node, className) {
|
548
|
+
node.className = node.className.replace(className, '')
|
549
|
+
}
|
550
|
+
|
551
|
+
// Extend String object with capitalize method
|
552
|
+
String.prototype.capitalize = function() {
|
553
|
+
return this.charAt(0).toUpperCase() + this.slice(1);
|
554
|
+
}
|
555
|
+
|
556
|
+
function createTips(){
|
557
|
+
var tipNodes = document.querySelectorAll('[data-tips-single-item]');
|
558
|
+
|
559
|
+
for(i = 0; i < tipNodes.length; ++i) {
|
560
|
+
var tip = new Tip(tipNodes[i])
|
561
|
+
Tips.list.push(tip)
|
562
|
+
}
|
563
|
+
};
|
564
|
+
|
565
|
+
// Sort list to make TipCard showing easier
|
566
|
+
function sortTipsList(list){
|
567
|
+
Tips.list.sort(function(a,b) {return a.priority - b.priority})
|
568
|
+
}
|
569
|
+
|
570
|
+
createTips();
|
571
|
+
sortTipsList();
|
572
|
+
Tips.showNextTip();
|
573
|
+
|
574
|
+
return Tips;
|
575
|
+
}
|
576
|
+
|
577
|
+
if(typeof(Tips) === 'undefined') window.Tips = initTips();
|
578
|
+
else console.log('Tips already defined.');
|
579
|
+
|
580
|
+
window.Tips.reset()
|
581
|
+
|
582
|
+
})(window);
|
@@ -0,0 +1,279 @@
|
|
1
|
+
.tips-throbber {
|
2
|
+
width: 58px;
|
3
|
+
height: 58px;
|
4
|
+
cursor: pointer;
|
5
|
+
z-index: 10;
|
6
|
+
background-image: url('/assets/tip_throbber.png');
|
7
|
+
-webkit-animation: throb .5s steps(12) alternate infinite;
|
8
|
+
-moz-animation: throb .5s steps(12) alternate infinite;
|
9
|
+
animation: throb .5s steps(12) alternate infinite;
|
10
|
+
}
|
11
|
+
|
12
|
+
@keyframes throb{
|
13
|
+
from{background-position:0 0}
|
14
|
+
to{background-position:-696px 0}
|
15
|
+
}
|
16
|
+
|
17
|
+
@-webkit-keyframes throb{
|
18
|
+
from{background-position:0 0}
|
19
|
+
to{background-position:100% 0}
|
20
|
+
}
|
21
|
+
|
22
|
+
@-moz-keyframes throb{
|
23
|
+
from{background-position:0 0}
|
24
|
+
to{background-position:-696px 0}
|
25
|
+
}
|
26
|
+
|
27
|
+
.tips-card {
|
28
|
+
position: relative;
|
29
|
+
-webkit-font-smoothing: antialiased;
|
30
|
+
-webkit-user-select: none;
|
31
|
+
box-sizing: border-box;
|
32
|
+
color: rgb(85, 84, 89);
|
33
|
+
display: block;
|
34
|
+
font-size: 16px;
|
35
|
+
min-height: 246px;
|
36
|
+
line-height: 20px;
|
37
|
+
width: 436px;
|
38
|
+
border: 2px solid #7F7F7F;
|
39
|
+
background: #FFFFFF;
|
40
|
+
-webkit-border-radius: 10px;
|
41
|
+
-moz-border-radius: 10px;
|
42
|
+
border-radius: 10px;
|
43
|
+
padding: 30px;
|
44
|
+
}
|
45
|
+
|
46
|
+
.tips-card, p, h2, a, li {
|
47
|
+
font-family: Lato, sans-serif !important;
|
48
|
+
}
|
49
|
+
|
50
|
+
.tips-card:after {
|
51
|
+
content: '';
|
52
|
+
position: absolute;
|
53
|
+
border-style: solid;
|
54
|
+
border-color: transparent #FFFFFF;
|
55
|
+
display: block;
|
56
|
+
width: 0;
|
57
|
+
z-index: 1;
|
58
|
+
}
|
59
|
+
|
60
|
+
.tips-card-right:after {
|
61
|
+
border-width: 15px 15px 15px 0;
|
62
|
+
left: -15px;
|
63
|
+
top: 118px;
|
64
|
+
}
|
65
|
+
|
66
|
+
.tips-card-left:after {
|
67
|
+
border-width: 15px 0 15px 15px;
|
68
|
+
right: -15px;
|
69
|
+
top: 118px;
|
70
|
+
}
|
71
|
+
|
72
|
+
.tips-card-top:after {
|
73
|
+
content: '';
|
74
|
+
position: absolute;
|
75
|
+
border-style: solid;
|
76
|
+
border-width: 15px 15px 0;
|
77
|
+
border-color: #FFFFFF transparent;
|
78
|
+
display: block;
|
79
|
+
width: 0;
|
80
|
+
z-index: 1;
|
81
|
+
bottom: -15px;
|
82
|
+
left: 228px;
|
83
|
+
}
|
84
|
+
|
85
|
+
.tips-card-bottom:after {
|
86
|
+
content: '';
|
87
|
+
position: absolute;
|
88
|
+
border-style: solid;
|
89
|
+
border-width: 0 15px 15px;
|
90
|
+
border-color: #FFFFFF transparent;
|
91
|
+
display: block;
|
92
|
+
width: 0;
|
93
|
+
z-index: 1;
|
94
|
+
top: -15px;
|
95
|
+
left: 228px;
|
96
|
+
}
|
97
|
+
|
98
|
+
.tips-card:before {
|
99
|
+
content: '';
|
100
|
+
position: absolute;
|
101
|
+
border-style: solid;
|
102
|
+
border-color: transparent #7F7F7F;
|
103
|
+
display: block;
|
104
|
+
width: 0;
|
105
|
+
z-index: 0;
|
106
|
+
}
|
107
|
+
|
108
|
+
.tips-card-right:before{
|
109
|
+
border-width: 16px 16px 16px 0;
|
110
|
+
left: -18px;
|
111
|
+
top: 117px;
|
112
|
+
}
|
113
|
+
|
114
|
+
.tips-card-left:before{
|
115
|
+
border-width: 16px 0 16px 16px;
|
116
|
+
right: -18px;
|
117
|
+
top: 117px;
|
118
|
+
}
|
119
|
+
|
120
|
+
.tips-card-top:before{
|
121
|
+
content: '';
|
122
|
+
position: absolute;
|
123
|
+
border-style: solid;
|
124
|
+
border-width: 16px 16px 0;
|
125
|
+
border-color: #7F7F7F transparent;
|
126
|
+
display: block;
|
127
|
+
width: 0;
|
128
|
+
z-index: 0;
|
129
|
+
bottom: -18px;
|
130
|
+
left: 227px;
|
131
|
+
}
|
132
|
+
|
133
|
+
.tips-card-bottom:before{
|
134
|
+
content: '';
|
135
|
+
position: absolute;
|
136
|
+
border-style: solid;
|
137
|
+
border-width: 0 16px 16px;
|
138
|
+
border-color: #7F7F7F transparent;
|
139
|
+
display: block;
|
140
|
+
width: 0;
|
141
|
+
z-index: 0;
|
142
|
+
top: -18px;
|
143
|
+
left: 227px;
|
144
|
+
}
|
145
|
+
|
146
|
+
.tips-card h2 {
|
147
|
+
margin: 0 0 10px;
|
148
|
+
font-size: 1.3em;
|
149
|
+
}
|
150
|
+
|
151
|
+
.tips-card p {
|
152
|
+
margin: 0px;
|
153
|
+
line-height: 20px;
|
154
|
+
font-size: 15px;
|
155
|
+
letter-spacing: 0.5px;
|
156
|
+
font-width: 400;
|
157
|
+
}
|
158
|
+
|
159
|
+
.tips-opt-out {
|
160
|
+
position: absolute;
|
161
|
+
right: 30px;
|
162
|
+
bottom: 6px;
|
163
|
+
font-size: 11px;
|
164
|
+
color: #aaaaaa
|
165
|
+
}
|
166
|
+
|
167
|
+
.tips-opt-out-link {
|
168
|
+
text-decoration: none;
|
169
|
+
color: #4A90E2
|
170
|
+
}
|
171
|
+
|
172
|
+
.tips-opt-out-link:hover{
|
173
|
+
background-color: inherit;
|
174
|
+
color: #407ABE;
|
175
|
+
}
|
176
|
+
|
177
|
+
.tips-navigational-dots-wrapper {
|
178
|
+
position: relative;
|
179
|
+
display: inline-block;
|
180
|
+
margin: 0 auto;
|
181
|
+
}
|
182
|
+
|
183
|
+
.tips-navigational-dot {
|
184
|
+
padding: 0 5px;
|
185
|
+
height: 8px;
|
186
|
+
width: 8px;
|
187
|
+
margin: 0 auto;
|
188
|
+
display: inline-block;
|
189
|
+
position: relative;
|
190
|
+
cursor: pointer;
|
191
|
+
display: inline-block;
|
192
|
+
}
|
193
|
+
|
194
|
+
.tips-navigational-dot circle {
|
195
|
+
fill: #ccc;
|
196
|
+
}
|
197
|
+
|
198
|
+
.tips-active-dot circle {
|
199
|
+
fill: #777777;
|
200
|
+
}
|
201
|
+
|
202
|
+
.tips-navigation-wrapper {
|
203
|
+
position: absolute;
|
204
|
+
right: 20px;
|
205
|
+
bottom: 40px;
|
206
|
+
left: 12px;
|
207
|
+
text-align: center;
|
208
|
+
}
|
209
|
+
|
210
|
+
.tips-btn-wrapper {
|
211
|
+
display: inline-block;
|
212
|
+
position: relative;
|
213
|
+
float: right;
|
214
|
+
bottom: 6px;
|
215
|
+
height: 20px;
|
216
|
+
width: 20px;
|
217
|
+
}
|
218
|
+
|
219
|
+
/* Ripped these buttons off */
|
220
|
+
.tips-btn {
|
221
|
+
border: 0 none;
|
222
|
+
border-radius: 4px;
|
223
|
+
color: #FFFFFF;
|
224
|
+
cursor: pointer;
|
225
|
+
position: absolute;
|
226
|
+
right: 10px;
|
227
|
+
display: inline-block;
|
228
|
+
font-family: Lato,sans-serif;
|
229
|
+
font-size: 13px;
|
230
|
+
font-weight: bold;
|
231
|
+
line-height: 16px;
|
232
|
+
margin-bottom: 0;
|
233
|
+
padding: 6px 17px;
|
234
|
+
text-transform: none;
|
235
|
+
transition: all 0.3s ease 0s;
|
236
|
+
-moz-transition: all 0.3s ease 0s;
|
237
|
+
-webkit-transition: all 0.3s ease 0s;
|
238
|
+
text-align: center; /* DELETE WHEN WIDTH AUTO */
|
239
|
+
}
|
240
|
+
|
241
|
+
.tips-btn.tips-dark {
|
242
|
+
background: none repeat scroll 0 0 #444444;
|
243
|
+
}
|
244
|
+
|
245
|
+
.tips-btn.tips-dark:hover {
|
246
|
+
background: none repeat scroll 0 0 #2DB6CF;
|
247
|
+
}
|
248
|
+
|
249
|
+
.tips-btn.tips-light {
|
250
|
+
background: none repeat scroll 0 0 #999999;
|
251
|
+
}
|
252
|
+
|
253
|
+
.tips-btn.tips-light:hover {
|
254
|
+
background: none repeat scroll 0 0 #444444;
|
255
|
+
}
|
256
|
+
|
257
|
+
.tips-btn.tips-green {
|
258
|
+
background: none repeat scroll 0 0 #46b98a;
|
259
|
+
}
|
260
|
+
|
261
|
+
.tips-btn.tips-green:hover {
|
262
|
+
background: none repeat scroll 0 0 #444444;
|
263
|
+
}
|
264
|
+
|
265
|
+
.tips-btn.tips-blue {
|
266
|
+
background: none repeat scroll 0 0 #2DB6CF;
|
267
|
+
}
|
268
|
+
|
269
|
+
.tips-btn.tips-blue:hover {
|
270
|
+
background: none repeat scroll 0 0 #444444;
|
271
|
+
}
|
272
|
+
|
273
|
+
.tips-btn.tips-red {
|
274
|
+
background: none repeat scroll 0 0 #E0645C;
|
275
|
+
}
|
276
|
+
|
277
|
+
.tips-btn.tips-red:hover {
|
278
|
+
background: none repeat scroll 0 0 #999999;
|
279
|
+
}
|
data/lib/tips/engine.rb
ADDED
data/lib/tips/version.rb
ADDED
data/lib/tips.rb
ADDED
data/tips.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tips/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tips"
|
8
|
+
spec.version = Tips::VERSION
|
9
|
+
spec.authors = ["Ryan Guerrettaz"]
|
10
|
+
spec.email = ["rguerrettaz@yahoo.com"]
|
11
|
+
spec.summary = "Slack inspired onboarding tips"
|
12
|
+
spec.description = ""
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tips
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Guerrettaz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: ''
|
42
|
+
email:
|
43
|
+
- rguerrettaz@yahoo.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- app/assets/images/tip_throbber.png
|
54
|
+
- app/assets/javascripts/tips.js
|
55
|
+
- app/assets/stylesheets/tips.css
|
56
|
+
- lib/tips.rb
|
57
|
+
- lib/tips/engine.rb
|
58
|
+
- lib/tips/version.rb
|
59
|
+
- tips.gemspec
|
60
|
+
homepage: ''
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.4.1
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Slack inspired onboarding tips
|
84
|
+
test_files: []
|