shimmer 0.0.18 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de8e99f1e252a60ee4c57442fe9fe67ba7af78e626b55d7c1756eedf3a83e404
4
- data.tar.gz: d28eeba9b1cdf8d9c1fc46151a334dc0993cacfbafe91223f14647e988a3ba23
3
+ metadata.gz: 89571a33faa2d1fd1d8c9624ab42a51f019418efc9c91af68fb3bb0385b5f602
4
+ data.tar.gz: 814bca1dbd13b0e147df173b1b41077c0c585f4bd5b8e1b9e6933051d597f231
5
5
  SHA512:
6
- metadata.gz: e750c73c60fa769a97526a0eaeb13b0f497539d9ae5c5500c581e2b0aafa7dc922c61b6edf6c4514d8946e8526d5789b5e454ce5787f4850cb598bd9450c6f05
7
- data.tar.gz: 62bcf6a9014420997d088bd41e9c491b4257456ec75f5881e252db5b7aea8b107c2d531ff2b10cba44e037d8eeb387a0edfbedcc09618da898dd3267bf84cad2
6
+ metadata.gz: 6d1c9bfa6f442914ca03747cf391ebf52086cb2c92d334641715031539cef826f09e171d3510e20866780ed87a1829182872485f6cd143c833ddebab97e1abf8
7
+ data.tar.gz: 5db21bb6096a9a4a555b17c05d2ba128379f049ed87bae1e9fd9cddf824a05214c9858d8760924ea948d11645f9052b3d503167a098ece2ba97fb5ca498db754
@@ -5,4 +5,5 @@
5
5
  "editor.formatOnSave": false,
6
6
  "editor.defaultFormatter": "castwide.solargraph"
7
7
  },
8
+ "typescript.tsdk": "node_modules/typescript/lib"
8
9
  }
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class Builder < ActionView::Helpers::FormBuilder
6
+ class << self
7
+ def input_registry
8
+ @input_registry ||= {}
9
+ end
10
+
11
+ def register(klass)
12
+ input_registry[klass.type] = klass
13
+ end
14
+ end
15
+
16
+ def input(method, as: guess_type(method), **options)
17
+ as ||= guess_type(method)
18
+ options[:class] ||= "input__input"
19
+ collection = options.delete :collection
20
+ collection_based = !collection.nil? || as == :select
21
+ collection ||= guess_collection(method) if collection_based
22
+ name_method ||= guess_name_method(method) if collection_based
23
+ id_method ||= :id if collection_based
24
+ classes = []
25
+ options[:required] ||= true if options[:required].nil? && required_attributes.include?(method)
26
+ options[:data] ||= {}
27
+ options[:data][:controller] = options.delete(:controller) if options[:controller]
28
+ wrapper_data = {}
29
+ extra = []
30
+ input_class = self.class.input_registry[as]
31
+ raise "Unknown type #{as}" unless input_class
32
+ input = input_class.new(builder: self, method: method, options: options, id_method: id_method, collection: collection, name_method: name_method)
33
+ wrap method: method, content: input.render, classes: classes + ["input--#{as}"], label: options[:label], data: wrapper_data, extra: extra
34
+ end
35
+
36
+ private
37
+
38
+ def required_attributes
39
+ []
40
+ end
41
+
42
+ def guess_type(method)
43
+ self.class.input_registry.values.find { |e| e.can_handle?(method) }&.type || :string
44
+ end
45
+
46
+ def guess_collection(method)
47
+ association_for(method)&.klass&.all || enum_for(method).map { |e| OpenStruct.new(id: e, name: e) } || []
48
+ end
49
+
50
+ def guess_name_method(method)
51
+ klass = association_for(method)&.klass
52
+ return :name unless klass
53
+
54
+ [:display_name, :name, :title].each do |key|
55
+ return key if klass.instance_methods.include?(key)
56
+ end
57
+ end
58
+
59
+ def association_for(method)
60
+ collection_method = method.to_s.delete_suffix("_id")
61
+ object.class.reflect_on_association(collection_method) if object.respond_to?(collection_method)
62
+ end
63
+
64
+ def enum_for(method)
65
+ object.class.types.keys if object.class.respond_to?(method.to_s.pluralize)
66
+ end
67
+
68
+ def wrap(method:, content:, classes:, label:, data: nil, extra: nil)
69
+ if object&.errors&.[](method)&.any?
70
+ classes << "input--error"
71
+ errors = safe_join(object.errors[method].map { |e| content_tag :div, e, class: "input__error" })
72
+ end
73
+ label = label == false ? nil : self.label(method, label, class: "input__label")
74
+ content_tag(:div, safe_join([label, content, errors, extra].compact), class: ["input"] + classes, data: data)
75
+ end
76
+
77
+ def helper
78
+ @template
79
+ end
80
+
81
+ def value_for(method)
82
+ object.public_send(method)
83
+ end
84
+
85
+ delegate :content_tag, :t, :safe_join, :icon, :tag, to: :helper
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class CheckboxesField < Field
6
+ self.type = :checkboxes
7
+
8
+ def render
9
+ builder.collection_check_boxes method, collection, id_method, name_method, {}, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class DateField < Field
6
+ self.type = :date
7
+
8
+ def render
9
+ builder.date_field method, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class EmailField < Field
6
+ self.type = :email
7
+
8
+ class << self
9
+ def can_handle?(method)
10
+ method.to_s.end_with?("email")
11
+ end
12
+ end
13
+
14
+ def render
15
+ builder.email_field method, options
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class Field
6
+ class_attribute :type
7
+
8
+ attr_reader :builder, :method, :collection, :id_method, :name_method, :options
9
+
10
+ class << self
11
+ def can_handle?(method)
12
+ false
13
+ end
14
+ end
15
+
16
+ def initialize(builder:, method:, collection:, id_method:, name_method:, options: {})
17
+ @builder = builder
18
+ @method = method
19
+ @collection = collection
20
+ @id_method = id_method
21
+ @name_method = name_method
22
+ @options = options
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class NumberField < Field
6
+ self.type = :number
7
+
8
+ def render
9
+ builder.number_field method, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class PasswordField < Field
6
+ self.type = :password
7
+
8
+ class << self
9
+ def can_handle?(method)
10
+ method.to_s.end_with?("password")
11
+ end
12
+ end
13
+
14
+ def render
15
+ builder.password_field method, options
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class PdfField < Field
6
+ self.type = :pdf
7
+
8
+ class << self
9
+ def can_handle?(method)
10
+ method.to_s.end_with?("pdf")
11
+ end
12
+ end
13
+
14
+ def render
15
+ builder.file_field method, options.reverse_merge(accept: "application/pdf")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class RadioField < Field
6
+ self.type = :radio
7
+
8
+ def render
9
+ builder.collection_radio_buttons method, collection, id_method, name_method, {}, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class SelectField < Field
6
+ self.type = :select
7
+
8
+ class << self
9
+ def can_handle?(method)
10
+ method.to_s.end_with?("_id")
11
+ end
12
+ end
13
+
14
+ def render
15
+ builder.collection_select method, collection, id_method, name_method, {}, options
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class TextAreaField < Field
6
+ self.type = :text
7
+
8
+ def render
9
+ builder.text_area method, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class TextField < Field
6
+ self.type = :string
7
+
8
+ def render
9
+ builder.text_field method, options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ class TimeField < Field
6
+ self.type = :time
7
+
8
+ def render
9
+ builder.time_field method, options.reverse_merge(value: object.public_send(method))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Form
5
+ end
6
+ end
7
+
8
+ require_relative "./form/builder"
9
+ require_relative "./form/field"
10
+
11
+ Dir["#{File.expand_path("./form", __dir__)}/*"].sort.each do |e|
12
+ require e
13
+ name = e.split("/").last.delete_suffix(".rb")
14
+ next unless name.end_with?("_field")
15
+ Shimmer::Form::Builder.register("Shimmer::Form::#{name.classify}".constantize)
16
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class ConsentSettings
5
+ SETTINGS = [:essential, :targeting, :statistic].freeze
6
+ DEFAULT = [:essential].freeze
7
+
8
+ SETTINGS.each do |setting|
9
+ attr_accessor setting
10
+ alias_method "#{setting}?", setting
11
+ end
12
+
13
+ def initialize(cookies)
14
+ @cookies = cookies
15
+ allowed = @cookies[:consent].to_s.split(",").map(&:strip)
16
+ SETTINGS.each do |setting|
17
+ instance_variable_set "@#{setting}", DEFAULT.include?(setting) || allowed.include?(setting.to_s)
18
+ end
19
+ end
20
+
21
+ def save
22
+ value = SETTINGS.map { |e| e.to_s if instance_variable_get("@#{e}") }.compact.join(",")
23
+ @cookies.permanent[:consent] = {value: value, expires: 2.years.from_now}
24
+ end
25
+
26
+ def given?
27
+ @cookies[:consent].present?
28
+ end
29
+ end
30
+
31
+ module Consent
32
+ extend ActiveSupport::Concern
33
+
34
+ included do
35
+ helper_method :consent_settings
36
+ def consent_settings
37
+ @consent_settings ||= ConsentSettings.new(cookies)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shimmer
4
- VERSION = "0.0.18"
4
+ VERSION = "0.0.21"
5
5
  end
data/lib/shimmer.rb CHANGED
@@ -7,6 +7,7 @@ Dir["#{File.expand_path("../lib/shimmer/controllers", __dir__)}/*"].sort.each {
7
7
  Dir["#{File.expand_path("../lib/shimmer/jobs", __dir__)}/*"].sort.each { |e| require e }
8
8
  Dir["#{File.expand_path("../lib/shimmer/utils", __dir__)}/*"].sort.each { |e| require e }
9
9
  require_relative "shimmer/auth"
10
+ require_relative "shimmer/form"
10
11
 
11
12
  module Shimmer
12
13
  class Error < StandardError; end
data/src/consent.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { getCookie, setCookie } from "./util";
2
+
3
+ const consentCategories = ["essential", "targeting", "statistic"] as const;
4
+ export type ConsentCategory = typeof consentCategories[number];
5
+
6
+ export class Consent {
7
+ #consentListeners: Record<ConsentCategory, Array<() => void>> = {
8
+ essential: [],
9
+ targeting: [],
10
+ statistic: [],
11
+ };
12
+
13
+ get permitted(): ConsentCategory[] {
14
+ return (getCookie("consent") ?? "").split(",") as ConsentCategory[];
15
+ }
16
+
17
+ set permitted(settings: ConsentCategory[]) {
18
+ setCookie("consent", settings.join(","));
19
+ settings.forEach((category) => {
20
+ this.#consentListeners[category].forEach((e) => e());
21
+ });
22
+ }
23
+
24
+ get given(): boolean {
25
+ return getCookie("consent") !== undefined;
26
+ }
27
+
28
+ permitAll(): void {
29
+ this.permitted = consentCategories.slice();
30
+ }
31
+
32
+ denyAll(): void {
33
+ this.permitted = ["essential"];
34
+ }
35
+
36
+ showSettings(): void {
37
+ const element = document.getElementById("personalization-settings");
38
+ if (element) element.hidden = false;
39
+ }
40
+
41
+ consentFor(category: ConsentCategory): Promise<void> {
42
+ return new Promise((res) => {
43
+ if (this.permitted.includes(category)) {
44
+ res();
45
+ } else {
46
+ this.#consentListeners[category].push(res);
47
+ }
48
+ });
49
+ }
50
+
51
+ async enableGoogleAnalytics(
52
+ id: string,
53
+ role: ConsentCategory = "statistic"
54
+ ): Promise<void> {
55
+ await this.consentFor(role);
56
+ window.gtag("config", id);
57
+ const script = document.createElement("script");
58
+ script.setAttribute(
59
+ "src",
60
+ `https://www.googletagmanager.com/gtag/js?id=${id}`
61
+ );
62
+ document.head.appendChild(script);
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ interface DataEvent {
4
+ event: string;
5
+ }
6
+
7
+ declare global {
8
+ interface Window {
9
+ dataLayer?: DataEvent[];
10
+ gtag(...arg): void;
11
+ }
12
+ }
13
+
14
+ const dataLayer = (window.dataLayer = window.dataLayer ?? []);
15
+
16
+ window.gtag = (...arg) => {
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ dataLayer.push(arg as any);
19
+ };
20
+ window.gtag("js", new Date());
21
+
22
+ export default class extends Controller {
23
+ connect(): void {
24
+ this.recordPage();
25
+ }
26
+
27
+ recordAction(event: MouseEvent): void {
28
+ const data = JSON.parse(
29
+ (event.target as HTMLElement).dataset.analytics ?? "{}"
30
+ );
31
+ dataLayer.push(data);
32
+ }
33
+
34
+ recordPage(): void {
35
+ const event = {
36
+ event: "Pageview",
37
+ path: location.pathname + location.search,
38
+ host: location.host,
39
+ };
40
+ dataLayer.push(event);
41
+ }
42
+ }
@@ -0,0 +1,52 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { ConsentCategory } from "../consent";
3
+
4
+ export default class extends Controller {
5
+ static targets = ["check"];
6
+ declare checkTargets: HTMLInputElement[];
7
+
8
+ connect(): void {
9
+ this.checkTargets.forEach((input) => {
10
+ input.checked =
11
+ window.ui?.consent.permitted.includes(input.name as ConsentCategory) ||
12
+ input.name === "essential";
13
+ });
14
+ }
15
+
16
+ permitAll(event: Event): void {
17
+ event.preventDefault();
18
+ window.ui?.consent.permitAll();
19
+ this.closeAll();
20
+ }
21
+
22
+ denyAll(event: Event): void {
23
+ event.preventDefault();
24
+ window.ui?.consent.denyAll();
25
+ this.closeAll();
26
+ }
27
+
28
+ save(event: Event): void {
29
+ event.preventDefault();
30
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
31
+ window.ui!.consent.permitted = this.checkTargets
32
+ .filter((e) => e.checked)
33
+ .map((e) => e.name) as ConsentCategory[];
34
+ this.closeAll();
35
+ }
36
+
37
+ manage(event: Event): void {
38
+ event.preventDefault();
39
+ const div = document.body.querySelector(
40
+ "#personalization-settings"
41
+ ) as HTMLDivElement;
42
+ if (!div) return;
43
+ (this.element as HTMLElement).hidden = true;
44
+ div.hidden = false;
45
+ }
46
+
47
+ closeAll(): void {
48
+ document
49
+ .querySelectorAll("[data-controller='consent']")
50
+ .forEach((e: HTMLElement) => (e.hidden = true));
51
+ }
52
+ }
data/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Application } from "@hotwired/stimulus";
2
2
  import { ModalPresenter } from "./modal";
3
3
  import { PopoverPresenter } from "./popover";
4
+ import { Consent } from "./consent";
4
5
  import RemoteNavigationController from "./controllers/remote-navigation";
6
+ import ConsentController from "./controllers/consent";
7
+ import AnalyticsController from "./controllers/analytics";
5
8
  import "./touch";
6
9
 
7
10
  export { registerServiceWorker } from "./serviceworker";
@@ -12,10 +15,17 @@ declare global {
12
15
  ui?: {
13
16
  modal: ModalPresenter;
14
17
  popover: PopoverPresenter;
18
+ consent: Consent;
15
19
  };
16
20
  }
17
21
  }
18
22
 
23
+ window.ui = {
24
+ modal: new ModalPresenter(),
25
+ popover: new PopoverPresenter(),
26
+ consent: new Consent(),
27
+ };
28
+
19
29
  function createRemoteDestination(): void {
20
30
  if (document.getElementById("shimmer")) {
21
31
  return;
@@ -33,8 +43,8 @@ export async function start({
33
43
  window.addEventListener("turbo:load", createRemoteDestination);
34
44
  createRemoteDestination();
35
45
  application.register("remote-navigation", RemoteNavigationController);
36
- window.ui = {
37
- modal: new ModalPresenter(),
38
- popover: new PopoverPresenter(),
39
- };
46
+ application.register("consent", ConsentController);
47
+ application.register("analytics", AnalyticsController);
40
48
  }
49
+
50
+ export const ui = window.ui;
data/src/util.ts CHANGED
@@ -44,3 +44,22 @@ export function wait(seconds: number): Promise<void> {
44
44
  setTimeout(res, seconds * 1000);
45
45
  });
46
46
  }
47
+
48
+ export function getCookie(key: string): string | null {
49
+ if (!document.cookie) return null;
50
+ return (
51
+ document.cookie
52
+ .split(";")
53
+ .map((v) => v.split("="))
54
+ .reduce((acc, v) => {
55
+ acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
56
+ return acc;
57
+ }, {})[key] ?? null
58
+ );
59
+ }
60
+
61
+ export function setCookie(key: string, value: string): void {
62
+ const date = new Date();
63
+ date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000);
64
+ document.cookie = `${key}=${value}; expires=${date.toUTCString()}`;
65
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shimmer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.18
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Ravens
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-01 00:00:00.000000000 Z
11
+ date: 2022-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -145,6 +145,20 @@ files:
145
145
  - lib/shimmer/auth/user.rb
146
146
  - lib/shimmer/controllers/files_controller.rb
147
147
  - lib/shimmer/controllers/sitemaps_controller.rb
148
+ - lib/shimmer/form.rb
149
+ - lib/shimmer/form/builder.rb
150
+ - lib/shimmer/form/checkboxes_field.rb
151
+ - lib/shimmer/form/date_field.rb
152
+ - lib/shimmer/form/email_field.rb
153
+ - lib/shimmer/form/field.rb
154
+ - lib/shimmer/form/number_field.rb
155
+ - lib/shimmer/form/password_field.rb
156
+ - lib/shimmer/form/pdf_field.rb
157
+ - lib/shimmer/form/radio_field.rb
158
+ - lib/shimmer/form/select_field.rb
159
+ - lib/shimmer/form/text_area_field.rb
160
+ - lib/shimmer/form/text_field.rb
161
+ - lib/shimmer/form/time_field.rb
148
162
  - lib/shimmer/helpers/meta_helper.rb
149
163
  - lib/shimmer/jobs/sitemap_job.rb
150
164
  - lib/shimmer/middlewares/cloudflare.rb
@@ -154,6 +168,7 @@ files:
154
168
  - lib/shimmer/tasks/lint.rake
155
169
  - lib/shimmer/tasks/s3.rake
156
170
  - lib/shimmer/utils/config.rb
171
+ - lib/shimmer/utils/consent_settings.rb
157
172
  - lib/shimmer/utils/file_helper.rb
158
173
  - lib/shimmer/utils/file_proxy.rb
159
174
  - lib/shimmer/utils/localizable.rb
@@ -163,6 +178,9 @@ files:
163
178
  - lib/shimmer/version.rb
164
179
  - package.json
165
180
  - rollup.config.js
181
+ - src/consent.ts
182
+ - src/controllers/analytics.ts
183
+ - src/controllers/consent.ts
166
184
  - src/controllers/remote-navigation.ts
167
185
  - src/index.ts
168
186
  - src/locale.ts