exception_hunter 0.2.0 → 1.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -6
  3. data/app/assets/stylesheets/exception_hunter/base.css +66 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +188 -25
  5. data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
  6. data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
  7. data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
  8. data/app/controllers/exception_hunter/application_controller.rb +2 -0
  9. data/app/controllers/exception_hunter/errors_controller.rb +29 -4
  10. data/app/controllers/exception_hunter/ignored_errors_controller.rb +25 -0
  11. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  12. data/app/helpers/exception_hunter/application_helper.rb +22 -0
  13. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  14. data/app/jobs/exception_hunter/send_notification_job.rb +15 -0
  15. data/app/models/exception_hunter/application_record.rb +8 -0
  16. data/app/models/exception_hunter/error.rb +24 -7
  17. data/app/models/exception_hunter/error_group.rb +24 -5
  18. data/app/presenters/exception_hunter/dashboard_presenter.rb +56 -0
  19. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  20. data/app/presenters/exception_hunter/error_presenter.rb +3 -2
  21. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  22. data/app/views/exception_hunter/errors/_error_row.erb +52 -0
  23. data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
  24. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  25. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  26. data/app/views/exception_hunter/errors/index.html.erb +84 -29
  27. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  28. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  29. data/app/views/layouts/exception_hunter/application.html.erb +67 -6
  30. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  31. data/config/rails_best_practices.yml +3 -3
  32. data/config/routes.rb +21 -1
  33. data/lib/exception_hunter.rb +44 -2
  34. data/lib/exception_hunter/config.rb +25 -1
  35. data/lib/exception_hunter/data_redacter.rb +27 -0
  36. data/lib/exception_hunter/devise.rb +19 -0
  37. data/lib/exception_hunter/engine.rb +6 -0
  38. data/lib/exception_hunter/error_creator.rb +72 -0
  39. data/lib/exception_hunter/error_reaper.rb +20 -0
  40. data/lib/exception_hunter/middleware/delayed_job_hunter.rb +70 -0
  41. data/lib/exception_hunter/middleware/request_hunter.rb +5 -2
  42. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -1
  43. data/lib/exception_hunter/notifiers/misconfigured_notifiers.rb +10 -0
  44. data/lib/exception_hunter/notifiers/slack_notifier.rb +42 -0
  45. data/lib/exception_hunter/notifiers/slack_notifier_serializer.rb +20 -0
  46. data/lib/exception_hunter/tracking.rb +35 -0
  47. data/lib/exception_hunter/user_attributes_collector.rb +21 -0
  48. data/lib/exception_hunter/version.rb +1 -1
  49. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  50. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  51. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  52. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +52 -0
  53. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  54. metadata +46 -12
  55. data/app/services/exception_hunter/error_creator.rb +0 -41
  56. data/config/initializers/exception_hunter.rb +0 -16
@@ -0,0 +1,25 @@
1
+ module ExceptionHunter
2
+ class ErrorGroupPresenter
3
+ delegate_missing_to :error_group
4
+
5
+ def initialize(error_group)
6
+ @error_group = error_group
7
+ end
8
+
9
+ def self.wrap_collection(collection)
10
+ collection.map { |error_group| new(error_group) }
11
+ end
12
+
13
+ def self.format_occurrence_day(day)
14
+ day.to_date.strftime('%A, %B %d')
15
+ end
16
+
17
+ def show_for_day?(day)
18
+ last_occurrence.in_time_zone.to_date == day.to_date
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :error_group
24
+ end
25
+ end
@@ -1,6 +1,7 @@
1
1
  module ExceptionHunter
2
2
  class ErrorPresenter
3
3
  delegate_missing_to :error
4
+ delegate :tags, to: :error_group
4
5
 
5
6
  BacktraceLine = Struct.new(:path, :file_name, :line_number, :method_call)
6
7
 
@@ -9,13 +10,13 @@ module ExceptionHunter
9
10
  end
10
11
 
11
12
  def backtrace
12
- error.backtrace.map do |line|
13
+ (error.backtrace || []).map do |line|
13
14
  format_backtrace_line(line)
14
15
  end
15
16
  end
16
17
 
17
18
  def environment_data
18
- error.environment_data.except('params')
19
+ error.environment_data&.except('params') || {}
19
20
  end
20
21
 
21
22
  def tracked_params
@@ -0,0 +1,24 @@
1
+ <div class="login_form_container">
2
+ <div class="login_left_container">
3
+ <div class="left_column">Exception Hunter</div>
4
+ </div>
5
+ <div class="login_right_container">
6
+ <%= form_for(resource, as: resource_name, url: exception_hunter_create_session_path) do |f| %>
7
+ <div class="row">
8
+ Sign into your account
9
+ </div>
10
+ <div class="login_row row"></div>
11
+ <div class="login_row row">
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: 'Email' %>
13
+ </div>
14
+ <div class="login_row row">
15
+ <%= f.password_field :password, autocomplete: "current-password", placeholder: 'Password'%>
16
+ </div>
17
+ <div class="login_button row">
18
+ <div class="column column-50 column-offset-50">
19
+ <input class="button-log-in" type="submit" value="Log in">
20
+ </div>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,52 @@
1
+ <div class="row error-row">
2
+ <div class="column column-10 error-cell">
3
+ <% error.tags.each do |tag| %>
4
+ <div class="error-cell__tags">
5
+ <span class="error-tag"><%= tag %></span>
6
+ </div>
7
+ <% end %>
8
+ </div>
9
+ <div class="column column-33 error-cell error-cell__message">
10
+ <%= link_to error.message, error_path(error.id), class: %w[error-message] %>
11
+ </div>
12
+
13
+ <div class="column column-15 error-cell">
14
+ <% if error.first_occurrence.present? %>
15
+ <%= time_ago_in_words(error.first_occurrence) %> ago
16
+ <% else %>
17
+ Never
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="column column-15 error-cell">
22
+ <% if error.last_occurrence.present? %>
23
+ <%= time_ago_in_words(error.last_occurrence) %> ago
24
+ <% else %>
25
+ Never
26
+ <% end %>
27
+ </div>
28
+
29
+ <div class="column column-8 error-cell">
30
+ <%= error.total_occurrences %>
31
+ </div>
32
+
33
+ <div class="column column-18 error-cell">
34
+ <div class="row">
35
+ <% if error.active? %>
36
+ <div class="column mr-5">
37
+ <%= display_action_button('resolve', error) %>
38
+ </div>
39
+ <div class="column">
40
+ <%= display_action_button('ignore', error) %>
41
+ </div>
42
+ <% elsif error.ignored? %>
43
+ <div class="column">
44
+ <%= button_to('Reopen', reopen_path(error_group: { id: error.id }),
45
+ class: %w[button button-outline resolve-button],
46
+ data: { confirm: 'Are you sure you want to reopen this error?' }) %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
@@ -1,9 +1,9 @@
1
1
  <% if error.environment_data.empty? %>
2
- <div class="row error-row-no-border data-title">
2
+ <div class="row error-row data-title">
3
3
  Unfortunately, no environment information has been registered for this error.
4
4
  </div>
5
5
  <% else %>
6
- <div class="row error-row-no-border data-title">
6
+ <div class="row error-row data-title">
7
7
  Environment Data
8
8
  </div>
9
9
  <pre class="tracked-data">
@@ -13,7 +13,7 @@
13
13
  <% end %>
14
14
 
15
15
  <% unless error.tracked_params.nil? %>
16
- <div class="row error-row-no-border data-title">
16
+ <div class="row error-row data-title">
17
17
  Tracked Params
18
18
  </div>
19
19
  <pre class="tracked-data">
@@ -23,11 +23,11 @@
23
23
  <% end %>
24
24
 
25
25
  <% if error.custom_data.nil? %>
26
- <div class="row error-row-no-border data-title">
26
+ <div class="row error-row data-title">
27
27
  No custom data included.
28
28
  </div>
29
29
  <% else %>
30
- <div class="row error-row-no-border data-title">
30
+ <div class="row error-row data-title">
31
31
  Custom Data
32
32
  </div>
33
33
  <pre class="tracked-data">
@@ -0,0 +1 @@
1
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors, as: :error %>
@@ -0,0 +1,12 @@
1
+ <% today_errors = errors.select { |error| error.show_for_day?(Date.current) } %>
2
+ <%= render partial: 'exception_hunter/errors/error_row', collection: today_errors, as: :error %>
3
+
4
+ <% yesterday_errors = errors.select { |error| error.show_for_day?(Date.yesterday) } %>
5
+ <div class="errors-date-group">Yesterday</div>
6
+ <%= render partial: 'exception_hunter/errors/error_row', collection: yesterday_errors, as: :error %>
7
+
8
+ <% (2..6).each do |i| %>
9
+ <% errors_on_day = errors.select { |error| error.show_for_day?(i.days.ago) } %>
10
+ <div class="errors-date-group"><%= ExceptionHunter::ErrorGroupPresenter.format_occurrence_day(i.days.ago) %></div>
11
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors_on_day, as: :error %>
12
+ <% end %>
@@ -1,36 +1,91 @@
1
- <div class="row statistics-row">
2
- <div class="column column-offset-50 column-25">
3
- <div class="statistics__cell">
4
- <%= number_with_delimiter(@errors_count) %> Total errors
1
+ <div class="row">
2
+ <div class="column column-80">
3
+ <div class="errors-tabs">
4
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::LAST_7_DAYS_TAB) %>">
5
+ <%= link_to errors_path(tab: @dashboard.class::LAST_7_DAYS_TAB) do %>
6
+ <div class="errors-tab__content">
7
+ <div class="errors-tab__badge">
8
+ <%= @dashboard.errors_count(@dashboard.class::LAST_7_DAYS_TAB) %>
9
+ </div>
10
+ <div class="errors-tab__description">
11
+ Errors in the last 7 days
12
+ </div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::CURRENT_MONTH_TAB) %>">
18
+ <%= link_to errors_path(tab: @dashboard.class::CURRENT_MONTH_TAB) do %>
19
+ <div class="errors-tab__content">
20
+ <div class="errors-tab__badge">
21
+ <%= @dashboard.errors_count(@dashboard.class::CURRENT_MONTH_TAB) %>
22
+ </div>
23
+ <div class="errors-tab__description">
24
+ Errors this month
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::TOTAL_ERRORS_TAB) %>">
31
+ <%= link_to errors_path(tab: @dashboard.class::TOTAL_ERRORS_TAB) do %>
32
+ <div class="errors-tab__content">
33
+ <div class="errors-tab__badge">
34
+ <%= @dashboard.errors_count(@dashboard.class::TOTAL_ERRORS_TAB) %>
35
+ </div>
36
+ <div class="errors-tab__description">
37
+ Total errors
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="errors-tab errors-tab__resolved <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::RESOLVED_ERRORS_TAB) %>">
44
+ <%= link_to errors_path(tab: @dashboard.class::RESOLVED_ERRORS_TAB) do %>
45
+ <div class="errors-tab__content">
46
+ <div class="errors-tab__badge">
47
+ <%= @dashboard.errors_count(@dashboard.class::RESOLVED_ERRORS_TAB) || '-' %>
48
+ </div>
49
+ <div class="errors-tab__description">
50
+ Resolved
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+ </div>
55
+
56
+ <div class="errors-tab errors-tab__ignored <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::IGNORED_ERRORS_TAB) %>">
57
+ <%= link_to errors_path(tab: @dashboard.class::IGNORED_ERRORS_TAB) do %>
58
+ <div class="errors-tab__content">
59
+ <div class="errors-tab__badge">
60
+ <%= @dashboard.errors_count(@dashboard.class::IGNORED_ERRORS_TAB) || '-' %>
61
+ </div>
62
+ <div class="errors-tab__description">
63
+ Ignored
64
+ </div>
65
+ </div>
66
+ <% end %>
67
+ </div>
5
68
  </div>
6
69
  </div>
7
- <div class="column column-25">
8
- <div class="statistics__cell">
9
- <%= number_with_delimiter(@month_errors) %> Errors this month
10
- </div>
70
+
71
+ <div class="column column-10 column-offset-10">
72
+ <%= button_to 'Purge', purge_errors_path,
73
+ class: %w[button purge-button],
74
+ method: :delete,
75
+ data: { confirm: 'This will delete all stale errors, do you want to continue?' } %>
11
76
  </div>
12
77
  </div>
13
78
 
14
- <div class="row error-row error-row--header">
15
- <div class="column column-75">Message</div>
16
- <div class="column column-15">Last Occurrence</div>
17
- <div class="column column-10">Occurrences</div>
18
- </div>
79
+ <div class="errors__container">
80
+ <div class="row error-row error-row--header">
81
+ <div class="column column-10">Tags</div>
82
+ <div class="column column-33">Message</div>
83
+ <div class="column column-15">First Occurrence</div>
84
+ <div class="column column-15">Last Occurrence</div>
85
+ <div class="column column-8">Total</div>
86
+ <div class="column column-18"></div>
19
87
 
20
- <% @errors.each do |error| %>
21
- <div class="row error-row">
22
- <div class="column column-75 error-cell">
23
- <%= link_to error.message, error_path(error.id) %>
24
- </div>
25
- <div class="column column-15 error-cell error-cell--highlight">
26
- <% if error.last_occurrence.present? %>
27
- <%= time_ago_in_words(error.last_occurrence) %> ago
28
- <% else %>
29
- Never
30
- <% end %>
31
- </div>
32
- <div class="column column-10 error-cell error-cell--highlight">
33
- <%= error.total_occurrences %>
34
- </div>
35
88
  </div>
36
- <% end %>
89
+
90
+ <%= render partial: @dashboard.partial_for_tab, locals: { errors: @errors } %>
91
+ </div>
@@ -1,17 +1,17 @@
1
- <% link = pagy_link_proc(pagy) %>
2
- <nav aria-label="pager" class="pagy_nav pagination" role="navigation">
3
- <% if pagy.prev %>
4
- <span class="page prev"><%== link.call(pagy.prev, '◄', 'aria-label="previous"') %></span>
5
- <% else %>
6
- <span class="page prev disabled">
7
-
8
- </span>
1
+ <div class="error-occurrences__nav">
2
+ <%= link_to pagy_url_for(1, pagy) do %>
3
+ <%= button_tag 'First', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
9
4
  <% end %>
10
- <% if pagy.next %>
11
- <span class="page next"><%== link.call(pagy.next, '►', 'aria-label="next"') %></span>
12
- <% else %>
13
- <span class="page next disabled">
14
-
15
- </span>
5
+ <%= link_to pagy_url_for(pagy.prev, pagy) do %>
6
+ <%= button_tag '<', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
16
7
  <% end %>
17
- </nav>
8
+ <div class="error-occurrences__nav-current">
9
+ <%= occurred_at %> (<%= pagy.page %>/<%= pagy.last %>)
10
+ </div>
11
+ <%= link_to pagy_url_for(pagy.next, pagy) do %>
12
+ <%= button_tag '>', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
13
+ <% end %>
14
+ <%= link_to pagy_url_for(pagy.last, pagy) do %>
15
+ <%= button_tag 'Last', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
16
+ <% end %>
17
+ </div>
@@ -1,33 +1,69 @@
1
- <div class="row">
2
- <div class="column column-offset-80 column-20">
3
- <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy } %>
1
+ <div class="row error-occurrence__header">
2
+ <div class="column column-10">
3
+ <% @error.tags.each do|tag| %>
4
+ <div class="error-cell__tags">
5
+ <span class="error-tag"><%= tag %></span>
6
+ </div>
7
+ <% end %>
8
+ </div>
9
+ <div class="column column-80">
10
+ <div class="error-title">
11
+ <%= @error.class_name %>: <%= @error.message %>
12
+ </div>
13
+ </div>
14
+ <div class="column column-10">
15
+ <%= button_to('Resolve', resolved_errors_path(error_group: { id: @error.error_group_id }),
16
+ method: :post,
17
+ class: %w[button resolve-button],
18
+ data: { confirm: 'Are you sure you want to resolve this error?' }) %>
4
19
  </div>
5
20
  </div>
6
21
 
7
- <div class="error-title">
8
- <%= @error.class_name %>: <%= @error.message %>
9
- </div>
10
- <div class="error-occurred_at">
11
- <%= @error.occurred_at %>
22
+ <div class="row">
23
+ <div class="column column-50">
24
+ <ul data-tabs class="errors-tabs">
25
+ <li class="errors-tab">
26
+ <a data-tabby-default href="#summary">
27
+ <div class="errors-tab__content">
28
+ Summary
29
+ </div>
30
+ </a>
31
+ </li>
32
+ <li class="errors-tab">
33
+ <a href="#backtrace">
34
+ <div class="errors-tab__content">
35
+ Backtrace
36
+ </div>
37
+ </a>
38
+ </li>
39
+ <li class="errors-tab">
40
+ <a href="#user-data">
41
+ <div class="errors-tab__content">
42
+ User Data
43
+ </div>
44
+ </a>
45
+ </li>
46
+ </ul>
47
+ </div>
48
+ <div class="column column-50">
49
+ <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy, occurred_at: @error.occurred_at } %>
50
+ </div>
12
51
  </div>
13
52
 
14
- <ul data-tabs>
15
- <li><a data-tabby-default href="#summary">Summary</a></li>
16
- <li><a href="#backtrace">Backtrace</a></li>
17
- <li><a href="#user-data">User Data</a></li>
18
- </ul>
19
53
 
20
- <div class="tab-content">
21
- <div id="summary">
22
- <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
23
- </div>
54
+ <div class="errors__container">
55
+ <div class="tab-content">
56
+ <div id="summary">
57
+ <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
58
+ </div>
24
59
 
25
- <div id="backtrace">
26
- <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
27
- </div>
60
+ <div id="backtrace">
61
+ <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
62
+ </div>
28
63
 
29
- <div id="user-data">
30
- <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
64
+ <div id="user-data">
65
+ <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
66
+ </div>
31
67
  </div>
32
68
  </div>
33
69
 
@@ -7,12 +7,11 @@
7
7
 
8
8
  <%= favicon_link_tag 'exception_hunter/logo.png' %>
9
9
 
10
- <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
10
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
11
11
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
12
12
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">
13
13
 
14
14
  <!-- Get patch fixes within a minor version -->
15
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/css/tabby-ui.min.css">
16
15
  <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/js/tabby.polyfills.min.js"></script>
17
16
 
18
17
  <%= stylesheet_link_tag "exception_hunter/application", media: "all" %>
@@ -22,18 +21,80 @@
22
21
  <div class="wrapper">
23
22
  <nav class="nav">
24
23
  <div class="container container__nav">
25
- <div class="nav__logo">
26
- <%= link_to errors_path do %>
27
- <%= image_tag 'exception_hunter/logo.png', alt: 'Logo' %>
28
- <% end %>
24
+ <div class="row">
25
+ <div class="column column-40">
26
+ <%= link_to errors_path do %>
27
+ <div class="nav__title">
28
+ Exception Hunter / <%= application_name %>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ <div class="column column-10 column-offset-50">
33
+ <div class="logout">
34
+ <% if current_admin_user? %>
35
+ <div class="sign_out">
36
+ <%= link_to exception_hunter_logout_path do %>
37
+ Sign out
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+ </div>
29
43
  </div>
30
44
  </div>
31
45
  </nav>
32
46
 
33
47
  <div class="container">
48
+ <% if flash[:notice] %>
49
+ <div id="flash-notice" class="flash flash--notice row">
50
+ <div class="column column-80">
51
+ <%= flash[:notice] %>
52
+ </div>
53
+ <div class="column column-20">
54
+ <div class="button button-clear button-dismiss" data-dismiss="#flash-notice">
55
+ <%= ['Cool!', 'Nice!', 'Ok', 'Dismiss', 'Fine. Whatever.', 'I know that'].sample %>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+
34
61
  <%= yield %>
35
62
  </div>
63
+
64
+ <div class="footer">
65
+ Made with ♥ at <a href="https://www.rootstrap.com/" class="text--underline">Rootstrap</a>
66
+ </div>
36
67
  </div>
37
68
 
69
+ <script type="text/javascript" charset="utf-8">
70
+ function confirmable(element) {
71
+ const message = element.getAttribute('data-confirm')
72
+ element.form.addEventListener('submit', confirm(message))
73
+
74
+ function confirm(message) {
75
+ return function (event) {
76
+ if (!window.confirm(message)) {
77
+ event.preventDefault()
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ function dismissible(element) {
84
+ element.addEventListener('click', dismiss(element))
85
+
86
+ function dismiss(element) {
87
+ return function () {
88
+ const selector = element.getAttribute('data-dismiss')
89
+ const elementToDismiss = document.querySelector(selector)
90
+ elementToDismiss.remove()
91
+ }
92
+ }
93
+ }
94
+
95
+ Array.from(document.querySelectorAll('[data-confirm]')).forEach(confirmable)
96
+ Array.from(document.querySelectorAll('[data-dismiss]')).forEach(dismissible)
97
+ </script>
98
+
38
99
  </body>
39
100
  </html>