active_analytics 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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +53 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/active_analytics_manifest.js +1 -0
  6. data/app/assets/javascripts/active_analytics/application.js +3 -0
  7. data/app/assets/javascripts/active_analytics/ariato.js +746 -0
  8. data/app/assets/stylesheets/active_analytics/application.css +33 -0
  9. data/app/assets/stylesheets/active_analytics/ariato.css +3548 -0
  10. data/app/assets/stylesheets/active_analytics/charts.css +2606 -0
  11. data/app/controllers/active_analytics/application_controller.rb +4 -0
  12. data/app/controllers/active_analytics/pages_controller.rb +22 -0
  13. data/app/controllers/active_analytics/referrers_controller.rb +18 -0
  14. data/app/controllers/active_analytics/sites_controller.rb +16 -0
  15. data/app/helpers/active_analytics/application_helper.rb +4 -0
  16. data/app/helpers/active_analytics/pages_helper.rb +15 -0
  17. data/app/helpers/active_analytics/referrers_helper.rb +4 -0
  18. data/app/helpers/active_analytics/sites_helper.rb +4 -0
  19. data/app/jobs/active_analytics/application_job.rb +4 -0
  20. data/app/mailers/active_analytics/application_mailer.rb +6 -0
  21. data/app/models/active_analytics/application_record.rb +5 -0
  22. data/app/models/active_analytics/views_per_day.rb +100 -0
  23. data/app/views/active_analytics/pages/_table.html.erb +21 -0
  24. data/app/views/active_analytics/pages/index.html.erb +12 -0
  25. data/app/views/active_analytics/pages/show.html.erb +23 -0
  26. data/app/views/active_analytics/referrers/_table.html.erb +14 -0
  27. data/app/views/active_analytics/referrers/index.html.erb +8 -0
  28. data/app/views/active_analytics/referrers/show.html.erb +14 -0
  29. data/app/views/active_analytics/sites/_histogram.html.erb +19 -0
  30. data/app/views/active_analytics/sites/index.html.erb +7 -0
  31. data/app/views/active_analytics/sites/show.html.erb +16 -0
  32. data/app/views/layouts/active_analytics/_footer.html.erb +6 -0
  33. data/app/views/layouts/active_analytics/_header.html.erb +25 -0
  34. data/app/views/layouts/active_analytics/application.html.erb +19 -0
  35. data/config/routes.rb +13 -0
  36. data/db/migrate/20210303094108_create_active_analytics_views_per_days.rb +20 -0
  37. data/lib/active_analytics.rb +22 -0
  38. data/lib/active_analytics/engine.rb +5 -0
  39. data/lib/active_analytics/version.rb +3 -0
  40. data/lib/tasks/active_analytics_tasks.rake +4 -0
  41. metadata +100 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 19ae6afed3bb78540b74da3b73496b6177cebd46642180b50da341dcaa556028
4
+ data.tar.gz: afd793312a0c7023218e027c933fbd115ceb82cc57e914a0bc16d990c1dbee57
5
+ SHA512:
6
+ metadata.gz: c4394e54e78f78213dc855aedc7abccd5e8909426b637c5f8fdf970c4990eb0835db6f8cf3dc6bc61acbb489c927c003d50efb64dfeb8b590d6674fdd35c8ec2
7
+ data.tar.gz: cc5606021a8320530d85055cf200d4e9270531720d40b7b91c26dca4aa2447bdd2f021e389f4fb2b378a2d8d5d87daece6c3dcfd3c667852d4a15197b35deba7
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Alexis Bernard
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # ActiveAnalytics
2
+
3
+ Trafic analytics for the win of privacy. To achieve this goal there is NO cookies, NO JavaScript, NO third parties and NO bullshit.
4
+
5
+ ActiveAnalytics is a Rails engine directly mountable in your Rails application.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+ ```ruby
10
+ gem 'active_analytics'
11
+ ```
12
+
13
+ Then execute bundle and run the migration:
14
+ ```bash
15
+ bundle
16
+ rails active_analytics:install:migrations
17
+ rails db:migrate
18
+ ```
19
+
20
+ Your controllers have to call `ActiveAnalytics.record_request(request)` to record pages views:
21
+ ```ruby
22
+ class ApplicationController < ActionController::Base
23
+ before_action :record_page_view
24
+
25
+ def record_page_view
26
+ ActiveAnalytics.record_request(request)
27
+ end
28
+ end
29
+ ```
30
+
31
+ This is a basic `before_action`. In case you don't want to record all page views, simply define a `skip_before_action :record_page_view` in the relevant controller.
32
+
33
+ Finally just add the route to ActiveAnylytics at the desired endpoint:
34
+ ```ruby
35
+ mount ActiveAnalytics::Engine, at: "analytics"
36
+ ```
37
+
38
+ ## Authentication and permissions
39
+ ActiveAnalytics cannot guess how you handle user authentication, because it is different for all Rails applications. So you have to inject your own mechanism into `ActiveAnalytics::ApplicationController`. Create a file in `config/initializers/active_analytics.rb`:
40
+
41
+ ```ruby
42
+ require_dependency "active_analytics/application_controller"
43
+
44
+ module ActiveAnalytics
45
+ class ApplicationController
46
+ # include Currentuser # This is an example that you have to change by
47
+ # before_action :require_admin # your own modules and methods
48
+ end
49
+ end
50
+ ```
51
+
52
+ ## License
53
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/active_analytics .css
@@ -0,0 +1,3 @@
1
+ //= require active_analytics/ariato
2
+
3
+ Ariato.launchWhenDomIsReady()
@@ -0,0 +1,746 @@
1
+ Ariato = {}
2
+
3
+ Ariato.launchWhenDomIsReady = function(root) {
4
+ if (document.readyState != "loading") {
5
+ Ariato.launch()
6
+ Ariato.launch(document, "aria-roledescription")
7
+ Ariato.launch(document, "data-ariato")
8
+ }
9
+ else
10
+ document.addEventListener("DOMContentLoaded", function() { Ariato.launchWhenDomIsReady(root) } )
11
+ }
12
+
13
+ Ariato.launch = function(root, attribute, parent) {
14
+ attribute || (attribute = "role")
15
+ var elements = (root || document).querySelectorAll("[" + attribute + "]")
16
+ for (var i = 0; i < elements.length; i++)
17
+ Ariato.start(elements[i], attribute, parent)
18
+ }
19
+
20
+ Ariato.mount = function() {
21
+ }
22
+
23
+ Ariato.start = function(element, attribute, parent) {
24
+ var names = element.getAttribute(attribute).split(" ")
25
+ for (var i = 0; i < names.length; i++) {
26
+ var name = names[i].charAt(0).toUpperCase() + names[i].slice(1) // Capitalize
27
+ var func = Ariato.stringToFunction("Ariato." + name) || Ariato.stringToFunction(name)
28
+ if (func instanceof Function)
29
+ Ariato.instanciate(func, element, parent)
30
+ }
31
+ }
32
+
33
+ Ariato.instanciate = function(func, element, parent) {
34
+ try {
35
+ controller = Object.create(func.prototype)
36
+ controller.parent = parent
37
+ controller.node = element
38
+ Ariato.initialize(controller, element)
39
+ func.call(controller, element)
40
+ } catch (ex) {
41
+ console.error(ex)
42
+ }
43
+ }
44
+
45
+ Ariato.stringToFunction = function(fullName) {
46
+ var func = window, names = fullName.split(".")
47
+ for (var i = 0; i < names.length; i++)
48
+ if (!(func = func[names[i]]))
49
+ return null
50
+ return func
51
+ }
52
+
53
+ Ariato.initialize = function(controller, container) {
54
+ Ariato.listenEvents(container, controller)
55
+ Ariato.assignRoles(container, controller)
56
+ }
57
+
58
+ Ariato.listenEvents = function(root, controller) {
59
+ var elements = root.querySelectorAll("[data-event]")
60
+ for (var i = 0; i < elements.length; i++) {
61
+ elements[i].getAttribute("data-event").split(" ").forEach(function(eventAndAction) {
62
+ var array = eventAndAction.split("->")
63
+ Ariato.listenEvent(controller, elements[i], array[0], array[1])
64
+ })
65
+ }
66
+ }
67
+
68
+ Ariato.listenEvent = function(controller, element, event, action) {
69
+ if (controller[action] instanceof Function)
70
+ element.addEventListener(event, controller[action].bind(controller))
71
+ }
72
+
73
+ Ariato.findRoles = function(container) {
74
+ var roles = {}, elements = container.querySelectorAll("[data-role]")
75
+ for (var i = 0; i < elements.length; i++) {
76
+ var name = elements[i].getAttribute("data-role")
77
+ roles[name] ? roles[name].push(elements[i]) : roles[name] = [elements[i]]
78
+ }
79
+ return roles
80
+ }
81
+
82
+ Ariato.assignRoles = function(container, controller) {
83
+ controller.roles = Ariato.findRoles(container)
84
+ for (var name in controller.roles)
85
+ if (controller.roles[name].length == 1)
86
+ controller[name] = controller.roles[name][0]
87
+ }
88
+
89
+ Ariato.Accordion = function(node) {
90
+ this.node = node
91
+ node.ariaAccordion = this
92
+ this.regions = []
93
+ }
94
+
95
+ Ariato.Accordion.addRegion = function(region) {
96
+ var button = region.labelledBy()
97
+
98
+ if (!button)
99
+ return
100
+
101
+ var accordion = region.node.parentElement.ariaAccordion || new Ariato.Accordion(region.node.parentElement)
102
+ accordion.addRegion(region)
103
+ return accordion
104
+ }
105
+
106
+ Ariato.Accordion.prototype.addRegion = function(region) {
107
+ this.regions.push(region)
108
+ }
109
+
110
+ Ariato.Accordion.prototype.hideRegions = function() {
111
+ for (var i = 0; i < this.regions.length; i++)
112
+ this.regions[i].hide()
113
+ }
114
+
115
+ Ariato.Accordion.prototype.showRegion = function(region) {
116
+ if (this.mutilpleAllowed())
117
+ region.expanded() ? region.hide() : region.show()
118
+ else {
119
+ this.hideRegions()
120
+ region.show()
121
+ }
122
+ }
123
+
124
+ Ariato.Accordion.prototype.mutilpleAllowed = function() {
125
+ return this.node.hasAttribute("data-allow-multiple")
126
+ }
127
+
128
+ Ariato.Carousel = function() {
129
+ this.currentSlide() || this.showSlide(this.slides()[0])
130
+ this.node.addEventListener("keydown", this.keydown.bind(this))
131
+
132
+ var nextButton = this.node.querySelector("[data-carousel=next]")
133
+ nextButton && nextButton.addEventListener("click", this.clicked.bind(this))
134
+
135
+ var previousButton = this.node.querySelector("[data-carousel=previous]")
136
+ previousButton.addEventListener("click", this.clicked.bind(this))
137
+ }
138
+
139
+ Ariato.Carousel.prototype.slides = function() {
140
+ return this.node.querySelectorAll("[aria-roledescription=slide]")
141
+ }
142
+
143
+ Ariato.Carousel.prototype.currentSlide = function() {
144
+ return this.node.querySelector("[aria-current=slide]")
145
+ }
146
+
147
+ Ariato.Carousel.prototype.showSlide = function(slide) {
148
+ var slides = this.slides()
149
+
150
+ for (var i = 0; i < slides.length; i++)
151
+ if (slides[i] == slide)
152
+ slides[i].setAttribute("aria-current", "slide")
153
+ else
154
+ slides[i].removeAttribute("aria-current")
155
+ }
156
+
157
+ Ariato.Carousel.prototype.nextSlide = function(slide) {
158
+ var slides = this.slides()
159
+ this.currentSlide()
160
+ for (var i = 0; i < slides.length; i++) {
161
+ if (slides[i] == slide)
162
+ slides[i].setAttribute("aria-current", "slide")
163
+ else
164
+ slides[i].removeAttribute("aria-current")
165
+ }
166
+ }
167
+
168
+ Ariato.Carousel.prototype.nextSlide = function(slide) {
169
+ var current = this.currentSlide()
170
+ return current && current.nextElementSibling
171
+ }
172
+
173
+ Ariato.Carousel.prototype.previousSlide = function(slide) {
174
+ var current = this.currentSlide()
175
+ return current && current.previousElementSibling
176
+ }
177
+
178
+ Ariato.Carousel.prototype.keydown = function(event) {
179
+ switch(event.key) {
180
+ case "ArrowLeft":
181
+ this.showSlide(this.previousOrLastSlide())
182
+ break
183
+ case "ArrowRight":
184
+ this.showSlide(this.nextOrFirstSlide())
185
+ break
186
+ }
187
+ }
188
+
189
+ Ariato.Carousel.prototype.clicked = function(event) {
190
+ switch(event.currentTarget.getAttribute("data-carousel")) {
191
+ case "next":
192
+ this.showSlide(this.previousOrLastSlide())
193
+ break
194
+ case "previous":
195
+ this.showSlide(this.nextOrFirstSlide())
196
+ break
197
+ }
198
+ }
199
+
200
+ Ariato.Carousel.prototype.previousOrLastSlide = function(event) {
201
+ var slide = this.previousSlide()
202
+ if (slide)
203
+ return slide
204
+ else {
205
+ var slides = this.slides()
206
+ return slides[slides.length-1]
207
+ }
208
+ }
209
+
210
+ Ariato.Carousel.prototype.nextOrFirstSlide = function(event) {
211
+ var slide = this.nextSlide()
212
+ if (slide)
213
+ return slide
214
+ else {
215
+ var slides = this.slides()
216
+ return slides[0]
217
+ }
218
+ }
219
+
220
+ Ariato.Dialog = function(node) {
221
+ node.setAttribute("hidden", true)
222
+ node.addEventListener("open", this.open.bind(this))
223
+ node.addEventListener("close", this.close.bind(this))
224
+ node.addEventListener("keydown", this.keydown.bind(this))
225
+ }
226
+
227
+ Ariato.Dialog.open = function(elementOrId) {
228
+ var dialog = elementOrId instanceof Element ? elementOrId : document.getElementById(elementOrId)
229
+ dialog && dialog.dispatchEvent(new CustomEvent("open"))
230
+ }
231
+
232
+ Ariato.Dialog.close = function(button) {
233
+ var dialog = Ariato.Dialog.current()
234
+ if (dialog && dialog.node.contains(button))
235
+ dialog.close()
236
+ }
237
+
238
+ Ariato.Dialog.closeCurrent = function() {
239
+ var dialog = Ariato.Dialog.current()
240
+ dialog && dialog.close()
241
+ }
242
+
243
+ Ariato.Dialog.replace = function(elementOrId) {
244
+ Ariato.Dialog.closeCurrent()
245
+ Ariato.Dialog.open(elementOrId)
246
+ }
247
+
248
+ Ariato.Dialog.close = function(button) {
249
+ var dialog = Ariato.Dialog.current()
250
+ if (dialog && dialog.node.contains(button))
251
+ dialog.close()
252
+ }
253
+
254
+ Ariato.Dialog.list = []
255
+
256
+ Ariato.Dialog.current = function() {
257
+ return this.list[this.list.length - 1]
258
+ }
259
+
260
+ Ariato.Dialog.prototype.open = function(event) {
261
+ Ariato.Dialog.list.push(this)
262
+ document.addEventListener("focus", this.bindedLimitFocusScope = this.limitFocusScope.bind(this), true)
263
+ this.initiator = document.activeElement
264
+ this.node.removeAttribute("hidden")
265
+
266
+ this.lockScrolling()
267
+ this.createBackdrop()
268
+ this.createFocusStoppers()
269
+ this.focusFirstDescendant(this.node)
270
+ }
271
+
272
+ Ariato.Dialog.prototype.close = function(event) {
273
+ document.removeEventListener("focus", this.bindedLimitFocusScope, true)
274
+ this.node.setAttribute("hidden", true)
275
+ this.removeFocusStoppers()
276
+ this.removeBackdrop()
277
+ this.unlockScrolling()
278
+ this.initiator.focus()
279
+ Ariato.Dialog.list.pop()
280
+ }
281
+
282
+ Ariato.Dialog.prototype.keydown = function(event) {
283
+ if (event.key == "Escape")
284
+ this.close()
285
+ }
286
+
287
+ Ariato.Dialog.prototype.focusFirstDescendant = function(parent) {
288
+ var focusable = ["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA"]
289
+
290
+ for (var i = 0; i < parent.children.length; i++) {
291
+ var child = parent.children[i]
292
+ if (focusable.indexOf(child.nodeName) != -1 && !child.disabled && child.type != "hidden") {
293
+ child.focus()
294
+ return child
295
+ }
296
+ else {
297
+ var focus = this.focusFirstDescendant(child)
298
+ if (focus) return focus
299
+ }
300
+ }
301
+ }
302
+
303
+ Ariato.Dialog.prototype.limitFocusScope = function(event) {
304
+ if (this == Ariato.Dialog.current())
305
+ if (!this.node.contains(event.target))
306
+ this.focusFirstDescendant(this.node)
307
+ }
308
+
309
+ Ariato.Dialog.prototype.lockScrolling = function() {
310
+ document.body.style.position = "fixed";
311
+ document.body.style.top = "-" + window.scrollY + "px";
312
+ }
313
+
314
+ Ariato.Dialog.prototype.unlockScrolling = function() {
315
+ var scrollY = document.body.style.top
316
+ document.body.style.position = ""
317
+ document.body.style.top = ""
318
+ window.scrollTo(0, parseInt(scrollY || "0") * -1)
319
+ }
320
+
321
+ Ariato.Dialog.prototype.createFocusStoppers = function() {
322
+ this.node.parentNode.insertBefore(this.focusStopper1 = document.createElement("div"), this.node)
323
+ this.focusStopper1.tabIndex = 0
324
+
325
+ this.node.parentNode.insertBefore(this.focusStopper2 = document.createElement("div"), this.node.nextSibling)
326
+ this.focusStopper2.tabIndex = 0
327
+ }
328
+
329
+ Ariato.Dialog.prototype.removeFocusStoppers = function() {
330
+ this.focusStopper1 && this.focusStopper1.parentNode.removeChild(this.focusStopper1)
331
+ this.focusStopper2 && this.focusStopper2.parentNode.removeChild(this.focusStopper2)
332
+ }
333
+
334
+ Ariato.Dialog.prototype.createBackdrop = function() {
335
+ this.backdrop = document.createElement("div")
336
+ this.backdrop.classList.add("dialog-backdrop")
337
+ this.node.parentNode.insertBefore(this.backdrop, this.node)
338
+ this.backdrop.appendChild(this.node)
339
+ }
340
+
341
+ Ariato.Dialog.prototype.removeBackdrop = function() {
342
+ this.backdrop.parentNode.insertBefore(this.node, this.backdrop)
343
+ this.backdrop.parentNode.removeChild(this.backdrop)
344
+ this.backdrop = null
345
+ }
346
+
347
+ Ariato.Alertdialog = Ariato.Dialog
348
+
349
+ Ariato.MenuBar = function() {
350
+ this.node.addEventListener("keydown", this.keyDown.bind(this))
351
+ }
352
+
353
+ // Ariato defines role="menubar" but MenuBar in camel case is nicer
354
+ Ariato.Menubar = Ariato.MenuBar
355
+
356
+ Ariato.Menubar.prototype.keyDown = function(event) {
357
+ switch (event.key) {
358
+ case "ArrowDown":
359
+ event.preventDefault()
360
+ if (event.target.hasAttribute("aria-haspopup"))
361
+ this.openItem(event.target)
362
+ else
363
+ this.focusNextItem(event.target)
364
+ break
365
+ case "ArrowUp":
366
+ event.preventDefault()
367
+ if (event.target.hasAttribute("aria-haspopup"))
368
+ this.openItem(event.target)
369
+ else
370
+ this.focusPreviousItem(event.target)
371
+ break
372
+ case "ArrowRight":
373
+ // Open parent next menu
374
+ // Open child menu
375
+ // Focus next item
376
+ this.openNextMenu(event.target)
377
+ break
378
+ if (event.target.hasAttribute("aria-haspopup"))
379
+ this.openItem(event.target)
380
+ else
381
+ this.openNextMenu(this.findParentMenu(event.target))
382
+ case "ArrowLeft":
383
+ this.openPreviousMenu(event.target)
384
+ break
385
+ case "Escape":
386
+ this.closeAllExcept()
387
+ break
388
+ }
389
+ }
390
+
391
+ Ariato.Menubar.prototype.closeAllExcept = function(item) {
392
+ var menus = this.node.querySelectorAll("[role=menu]")
393
+ for (var i = 0; i < menus.length; i++)
394
+ menus[i].style.display = menus[i].contains(item) ? "block" : null
395
+ }
396
+
397
+ Ariato.Menubar.prototype.openItem = function(item) {
398
+ var menu = item.parentElement.querySelector("[role=menu]")
399
+ item.setAttribute("aria-expanded", true)
400
+ var subItem = menu.querySelector("[role=menuitem]")
401
+ if (subItem) {
402
+ this.closeAllExcept(subItem)
403
+ subItem.focus()
404
+ } else {
405
+ this.closeAllExcept(item)
406
+ item.focus()
407
+ }
408
+ }
409
+
410
+ Ariato.Menubar.prototype.openNextMenu = function(item) {
411
+ var menu = this.findNextMenu(item)
412
+ menu && this.openItem(menu.parentElement.querySelector("[role=menuitem]"))
413
+ }
414
+
415
+ Ariato.Menubar.prototype.openPreviousMenu = function(item) {
416
+ var menu = this.findPreviousMenu(item)
417
+ menu && this.openItem(menu.parentElement.querySelector("[role=menuitem]"))
418
+ }
419
+
420
+ Ariato.Menubar.prototype.focusNextItem = function(item) {
421
+ var nextItem = this.findNextItem(item)
422
+ nextItem && nextItem.focus()
423
+ }
424
+
425
+ Ariato.Menubar.prototype.focusPreviousItem = function(item) {
426
+ var previousItem = this.findPreviousItem(item)
427
+ previousItem && previousItem.focus()
428
+ }
429
+
430
+ Ariato.Menubar.prototype.findParentMenu = function(item) {
431
+ var parent = item.parentElement
432
+ var menuRoles = ["menu", "menubar"]
433
+ while (parent && !menuRoles.includes(parent.getAttribute("role")))
434
+ parent = parent.parentElement
435
+ return parent
436
+ }
437
+
438
+ Ariato.Menubar.prototype.findNextItem = function(item) {
439
+ var menu = this.findParentMenu(item)
440
+ var items = menu.querySelectorAll("[role=menuitem]")
441
+ for (var i = 0; i < items.length; i++)
442
+ if (items[i] == item)
443
+ return items[i+1]
444
+ }
445
+
446
+ Ariato.Menubar.prototype.findPreviousItem = function(item) {
447
+ var menu = this.findParentMenu(item)
448
+ var items = menu.querySelectorAll("[role=menuitem]")
449
+ for (var i = 0; i < items.length; i++)
450
+ if (items[i] == item)
451
+ return items[i-1]
452
+ }
453
+
454
+ Ariato.Menubar.prototype.findNextMenu = function(item) {
455
+ var menus = this.rootMenus()
456
+ for (var i = 0; i < menus.length; i++)
457
+ if (menus[i].contains(item))
458
+ return menus[i+1]
459
+
460
+ var parent = item.parentElement
461
+ for (var i = 0; i < menus.length; i++)
462
+ if (parent.contains(menus[i]))
463
+ return menus[i+1]
464
+ }
465
+
466
+ Ariato.Menubar.prototype.findPreviousMenu = function(item) {
467
+ var menus = this.rootMenus()
468
+ for (var i = 0; i < menus.length; i++)
469
+ if (menus[i].contains(item))
470
+ return menus[i-1]
471
+
472
+ var parent = item.parentElement
473
+ for (var i = 0; i < menus.length; i++)
474
+ if (parent.contains(menus[i]))
475
+ return menus[i-1]
476
+ }
477
+
478
+ Ariato.Menubar.prototype.rootMenus = function() {
479
+ return this.node.querySelectorAll("li > [role=menu]")
480
+ }
481
+
482
+ Ariato.MenuButton = function(node) {
483
+ this.node = this.button = node
484
+ this.menu = document.getElementById(this.button.getAttribute("aria-controls"))
485
+
486
+ this.menu.addEventListener("keydown", this.keydown.bind(this))
487
+ this.button.addEventListener("keydown", this.keydown.bind(this))
488
+
489
+ this.button.addEventListener("click", this.clicked.bind(this))
490
+ window.addEventListener("click", this.windowClicked.bind(this), true)
491
+ }
492
+
493
+ Ariato.MenuButton.prototype.clicked = function(event) {
494
+ this.node.getAttribute("aria-expanded") == "true" ? this.close() : this.open()
495
+ }
496
+
497
+ Ariato.MenuButton.prototype.windowClicked = function() {
498
+ if (!this.node.contains(event.target) && this.node.getAttribute("aria-expanded") == "true")
499
+ this.close()
500
+ }
501
+
502
+ Ariato.MenuButton.prototype.open = function() {
503
+ this.button.setAttribute("aria-expanded", "true")
504
+ this.menu.style.display = "block"
505
+ }
506
+
507
+ Ariato.MenuButton.prototype.close = function() {
508
+ this.button.setAttribute("aria-expanded", "false")
509
+ this.menu.style.display = null
510
+ }
511
+
512
+ Ariato.MenuButton.prototype.keydown = function(event) {
513
+ switch(event.key) {
514
+ case "Escape":
515
+ this.close()
516
+ break
517
+ case "ArrowDown":
518
+ event.preventDefault()
519
+ this.focusNextItem()
520
+ break
521
+ case "ArrowUp":
522
+ event.preventDefault()
523
+ this.focusPreviousItem()
524
+ break
525
+ case "Tab":
526
+ this.close()
527
+ case "Home":
528
+ case "PageUp":
529
+ event.preventDefault()
530
+ this.items()[0].focus()
531
+ break
532
+ case "End":
533
+ case "PageDown":
534
+ event.preventDefault()
535
+ var items = this.items()
536
+ items[items.length-1].focus()
537
+ break
538
+ }
539
+ }
540
+
541
+ Ariato.MenuButton.prototype.items = function() {
542
+ return this.menu.querySelectorAll("[role=menuitem]")
543
+ }
544
+
545
+ Ariato.MenuButton.prototype.currentItem = function() {
546
+ return this.menu.querySelector("[role=menuitem]:focus")
547
+ }
548
+
549
+ Ariato.MenuButton.prototype.nextItem = function() {
550
+ var items = this.items()
551
+ var current = this.currentItem()
552
+ if (!current) return items[0]
553
+ for (var i = 0; i < items.length; i++) {
554
+ if (items[i] == current)
555
+ return items[i+1]
556
+ }
557
+ }
558
+
559
+ Ariato.MenuButton.prototype.previousItem = function() {
560
+ var items = this.items()
561
+ var current = this.currentItem()
562
+ if (!current) return items[0]
563
+ for (var i = 0; i < items.length; i++) {
564
+ if (items[i] == current)
565
+ return items[i-1]
566
+ }
567
+ }
568
+
569
+ Ariato.MenuButton.prototype.focusNextItem = function() {
570
+ var item = this.nextItem()
571
+ item && item.focus()
572
+ }
573
+
574
+ Ariato.MenuButton.prototype.focusPreviousItem = function() {
575
+ var item = this.previousItem()
576
+ item && item.focus()
577
+ }
578
+
579
+ Ariato.Menu = function(node) {
580
+ var button = this.labelledBy()
581
+ button && new Ariato.MenuButton(button)
582
+ }
583
+
584
+ Ariato.Menu.prototype.labelledBy = function() {
585
+ return document.getElementById(this.node.getAttribute("aria-labelledby"))
586
+ }
587
+
588
+ /*
589
+ * A region is a role="region" element which represents a panel of an accordion.
590
+ * It is controlled by a button.
591
+ */
592
+ Ariato.Region = function(node) {
593
+ this.node = node
594
+ var labelledBy = this.labelledBy()
595
+
596
+ if (!labelledBy)
597
+ return
598
+
599
+ this.accordion = Ariato.Accordion.addRegion(this)
600
+ labelledBy.addEventListener("click", this.buttonClicked.bind(this))
601
+ }
602
+
603
+ Ariato.Region.prototype.labelledBy = function() {
604
+ return document.getElementById(this.node.getAttribute("aria-labelledby"))
605
+ }
606
+
607
+ Ariato.Region.prototype.buttonClicked = function(event) {
608
+ this.accordion.showRegion(this)
609
+ }
610
+
611
+ Ariato.Region.prototype.show = function(event) {
612
+ this.labelledBy().setAttribute("aria-expanded", true)
613
+ this.node.removeAttribute("hidden")
614
+ }
615
+
616
+ Ariato.Region.prototype.hide = function(event) {
617
+ this.labelledBy().setAttribute("aria-expanded", false)
618
+ this.node.setAttribute("hidden", "")
619
+ }
620
+
621
+ Ariato.Region.prototype.expanded = function() {
622
+ return !this.node.hasAttribute("hidden")
623
+ }
624
+
625
+ Ariato.Tablist = function(node) {
626
+ this.node = node
627
+ var tabs = this.tabs()
628
+ for (var i = 0; i < tabs.length; i++) {
629
+ tabs[i].addEventListener("click", this.click.bind(this))
630
+ tabs[i].addEventListener("keydown", this.keydown.bind(this))
631
+ tabs[i].addEventListener("keyup", this.keyup.bind(this))
632
+ }
633
+ tabs[0] && this.showTab(tabs[0])
634
+ }
635
+
636
+ Ariato.Tablist.prototype.click = function(event) {
637
+ this.showTab(event.currentTarget)
638
+ }
639
+
640
+ Ariato.Tablist.prototype.tabs = function() {
641
+ return this.node.querySelectorAll("[role=tab]")
642
+ }
643
+
644
+ Ariato.Tablist.prototype.activeTab = function() {
645
+ return this.node.querySelector("[aria-selected=true]")
646
+ }
647
+
648
+ Ariato.Tablist.prototype.panels = function() {
649
+ var tabs = this.tabs(), result = []
650
+ for (var i = 0; i < tabs.length; i++)
651
+ result.push(document.getElementById(tabs[i].getAttribute("aria-controls")))
652
+ return result
653
+ }
654
+
655
+ Ariato.Tablist.prototype.showTab = function(tab) {
656
+ this.hidePanels()
657
+ tab.removeAttribute("tabindex")
658
+ tab.setAttribute("aria-selected", "true")
659
+ document.getElementById(tab.getAttribute("aria-controls")).style.display = null
660
+ tab.focus()
661
+ }
662
+
663
+ Ariato.Tablist.prototype.hidePanels = function() {
664
+ var tabs = this.tabs()
665
+ for (var i = 0; i < tabs.length; i++) {
666
+ tabs[i].setAttribute("tabindex", "-1");
667
+ tabs[i].setAttribute("aria-selected", "false");
668
+ }
669
+
670
+ var panels = this.panels()
671
+ for (var i = 0; i < panels.length; i++)
672
+ panels[i].style.display = "none"
673
+ }
674
+
675
+ Ariato.Tablist.prototype.keydown = function(event) {
676
+ switch (event.key) {
677
+ case "End":
678
+ var tabs = this.tabs()
679
+ event.preventDefault()
680
+ this.showTab(this.tabs()[tabs.length - 1])
681
+ break
682
+ case "Home":
683
+ event.preventDefault()
684
+ this.showTab(this.tabs()[0])
685
+ break
686
+ case "ArrowUp":
687
+ event.preventDefault()
688
+ this.showPrevious()
689
+ break
690
+ case "ArrowDown":
691
+ event.preventDefault()
692
+ this.showNext()
693
+ break
694
+ }
695
+ }
696
+
697
+ Ariato.Tablist.prototype.keyup = function(event) {
698
+ if (event.key == "ArrowLeft")
699
+ this.showPrevious()
700
+ else if (event.key == "ArrowRight")
701
+ this.showNext(event)
702
+ // TODO delete
703
+ }
704
+
705
+ Ariato.Tablist.prototype.showNext = function() {
706
+ var tabs = this.tabs()
707
+ var index = Array.prototype.indexOf.call(tabs, this.activeTab())
708
+ tabs[index + 1] && this.showTab(tabs[index + 1])
709
+ }
710
+
711
+ Ariato.Tablist.prototype.showPrevious = function() {
712
+ var tabs = this.tabs()
713
+ var index = Array.prototype.indexOf.call(tabs, this.activeTab())
714
+ tabs[index - 1] && this.showTab(tabs[index - 1])
715
+ }
716
+
717
+ Ariato.ThemeSwitcher = function() {
718
+ Ariato.ThemeSwitcher.initialize()
719
+ this.node.addEventListener("click", this.change.bind(this))
720
+ }
721
+
722
+ Ariato.ThemeSwitcher.initialize = function() {
723
+ if (!this.initialized) {
724
+ console.log("initialize")
725
+ this.initialized = true
726
+ this.update()
727
+ }
728
+ }
729
+
730
+ Ariato.ThemeSwitcher.update = function() {
731
+ var name = localStorage.getItem("ariato-theme")
732
+ document.documentElement.classList.forEach(function(theme) {
733
+ theme.startsWith("theme-") && document.documentElement.classList.remove(theme)
734
+ })
735
+ document.documentElement.classList.add("theme-" + name)
736
+
737
+ var buttons = document.querySelectorAll("[data-ariato='ThemeSwitcher']")
738
+ for (var i = 0; i < buttons.length; i++)
739
+ buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-theme") == name)
740
+ }
741
+
742
+ Ariato.ThemeSwitcher.prototype.change = function(event) {
743
+ var name = event.currentTarget.getAttribute("data-theme")
744
+ localStorage.setItem("ariato-theme", name)
745
+ name && Ariato.ThemeSwitcher.update(name)
746
+ }