tips 0.0.2
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/.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: []
|