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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/app/assets/config/active_analytics_manifest.js +1 -0
- data/app/assets/javascripts/active_analytics/application.js +3 -0
- data/app/assets/javascripts/active_analytics/ariato.js +746 -0
- data/app/assets/stylesheets/active_analytics/application.css +33 -0
- data/app/assets/stylesheets/active_analytics/ariato.css +3548 -0
- data/app/assets/stylesheets/active_analytics/charts.css +2606 -0
- data/app/controllers/active_analytics/application_controller.rb +4 -0
- data/app/controllers/active_analytics/pages_controller.rb +22 -0
- data/app/controllers/active_analytics/referrers_controller.rb +18 -0
- data/app/controllers/active_analytics/sites_controller.rb +16 -0
- data/app/helpers/active_analytics/application_helper.rb +4 -0
- data/app/helpers/active_analytics/pages_helper.rb +15 -0
- data/app/helpers/active_analytics/referrers_helper.rb +4 -0
- data/app/helpers/active_analytics/sites_helper.rb +4 -0
- data/app/jobs/active_analytics/application_job.rb +4 -0
- data/app/mailers/active_analytics/application_mailer.rb +6 -0
- data/app/models/active_analytics/application_record.rb +5 -0
- data/app/models/active_analytics/views_per_day.rb +100 -0
- data/app/views/active_analytics/pages/_table.html.erb +21 -0
- data/app/views/active_analytics/pages/index.html.erb +12 -0
- data/app/views/active_analytics/pages/show.html.erb +23 -0
- data/app/views/active_analytics/referrers/_table.html.erb +14 -0
- data/app/views/active_analytics/referrers/index.html.erb +8 -0
- data/app/views/active_analytics/referrers/show.html.erb +14 -0
- data/app/views/active_analytics/sites/_histogram.html.erb +19 -0
- data/app/views/active_analytics/sites/index.html.erb +7 -0
- data/app/views/active_analytics/sites/show.html.erb +16 -0
- data/app/views/layouts/active_analytics/_footer.html.erb +6 -0
- data/app/views/layouts/active_analytics/_header.html.erb +25 -0
- data/app/views/layouts/active_analytics/application.html.erb +19 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210303094108_create_active_analytics_views_per_days.rb +20 -0
- data/lib/active_analytics.rb +22 -0
- data/lib/active_analytics/engine.rb +5 -0
- data/lib/active_analytics/version.rb +3 -0
- data/lib/tasks/active_analytics_tasks.rake +4 -0
- 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,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
|
+
}
|