active_analytics 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }