recurrence_ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +208 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/recurrence_ui_manifest.js +1 -0
  6. data/app/assets/stylesheets/recurrence_ui/application.css +15 -0
  7. data/app/assets/stylesheets/recurrence_ui/components.css +295 -0
  8. data/app/assets/stylesheets/recurrence_ui/tokens.css +102 -0
  9. data/app/components/recurrence_ui/recurrence_picker_component.html.erb +234 -0
  10. data/app/components/recurrence_ui/recurrence_picker_component.rb +204 -0
  11. data/app/controllers/recurrence_ui/application_controller.rb +32 -0
  12. data/app/helpers/recurrence_ui/application_helper.rb +23 -0
  13. data/app/javascript/recurrence_ui/index.js +22 -0
  14. data/app/javascript/recurrence_ui/recurrence_ui_controller.js +954 -0
  15. data/app/jobs/recurrence_ui/application_job.rb +4 -0
  16. data/app/mailers/recurrence_ui/application_mailer.rb +6 -0
  17. data/app/models/concerns/recurrence_ui/has_recurrence.rb +351 -0
  18. data/app/models/recurrence_ui/application_record.rb +5 -0
  19. data/app/services/recurrence_ui/recurrence_formatter_service.rb +536 -0
  20. data/app/views/layouts/recurrence_ui/application.html.erb +15 -0
  21. data/app/views/recurrence_ui/_daily.html.erb +22 -0
  22. data/app/views/recurrence_ui/_exceptions.html.erb +50 -0
  23. data/app/views/recurrence_ui/_footer_actions.html.erb +17 -0
  24. data/app/views/recurrence_ui/_monthly_by_day_of_month.html.erb +47 -0
  25. data/app/views/recurrence_ui/_monthly_by_weekday.html.erb +55 -0
  26. data/app/views/recurrence_ui/_rdates.html.erb +50 -0
  27. data/app/views/recurrence_ui/_rule_card.html.erb +61 -0
  28. data/app/views/recurrence_ui/_summary_link.html.erb +25 -0
  29. data/app/views/recurrence_ui/_termination.html.erb +44 -0
  30. data/app/views/recurrence_ui/_weekly.html.erb +44 -0
  31. data/app/views/recurrence_ui/_yearly_by_day_of_month.html.erb +63 -0
  32. data/app/views/recurrence_ui/_yearly_by_weekday.html.erb +72 -0
  33. data/config/i18n-tasks.yml +212 -0
  34. data/config/importmap.rb +3 -0
  35. data/config/locales/recurrence_ui.be.yml +245 -0
  36. data/config/locales/recurrence_ui.en.yml +210 -0
  37. data/config/locales/recurrence_ui.ja.yml +201 -0
  38. data/config/locales/recurrence_ui.ru.yml +245 -0
  39. data/config/locales/recurrence_ui.uk.yml +245 -0
  40. data/config/routes.rb +5 -0
  41. data/lib/generators/recurrence_ui/views_generator.rb +26 -0
  42. data/lib/recurrence_ui/configuration.rb +116 -0
  43. data/lib/recurrence_ui/engine.rb +57 -0
  44. data/lib/recurrence_ui/version.rb +3 -0
  45. data/lib/recurrence_ui.rb +25 -0
  46. data/lib/tasks/recurrence_ui_tasks.rake +4 -0
  47. metadata +195 -0
@@ -0,0 +1,234 @@
1
+ <%# app/components/recurrence_ui/recurrence_picker_component.html.erb %>
2
+ <% I18n.with_locale(locale) do %>
3
+ <div data-controller="<%= identifier %>"
4
+ data-<%= identifier %>-time-zone-value="<%= time_zone %>"
5
+ data-<%= identifier %>-translations-value="<%= t('recurrence_ui', locale: locale).to_json %>"
6
+ data-<%= identifier %>-locale-value="<%= locale %>"
7
+ class="rui-container"
8
+ data-rui-theme="<%= RecurrenceUi.configuration.theme_mode %>">
9
+
10
+ <%# 1. Секция полей времени, окончания и часового пояса %>
11
+ <div class="space-y-4 pb-4 border-b border-[var(--rui-border-color)]">
12
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
13
+
14
+ <!-- 1. Дата и время начала (всегда на одной строке, не тянутся) -->
15
+ <div>
16
+ <%= form.label :start_at, t('recurrence_ui.labels.start_at'), class: "rui-label block" %>
17
+ <div class="flex items-center space-x-2 mt-1">
18
+ <%= form.date_field :form_start_date,
19
+ value: initial_start_at&.to_date || Date.current,
20
+ class: "rui-input w-36 shrink-0",
21
+ data: { "#{identifier}_target": "startDate", action: "change->#{identifier}#calculateEndDateFromDuration" } %>
22
+ <%= form.time_field :form_start_time,
23
+ value: initial_start_at&.strftime('%H:%M') || '09:00',
24
+ class: "rui-input w-24 shrink-0",
25
+ data: { "#{identifier}_target": "startTime", action: "change->#{identifier}#calculateEndDateFromDuration" } %>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Часовой пояс -->
30
+ <div>
31
+ <%= form.label :time_zone, t('recurrence_ui.labels.time_zone'), class: "rui-label block" %>
32
+ <%= form.time_zone_select :time_zone,
33
+ ActiveSupport::TimeZone.all,
34
+ { default: record.public_send(time_zone_field).presence || Time.zone.name },
35
+ { class: "rui-input rui-select cursor-pointer w-full mt-1" } %>
36
+ </div>
37
+
38
+ </div>
39
+ </div>
40
+
41
+ <!-- Блок выбора: Длительность или Окончание -->
42
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-4">
43
+
44
+ <!-- Вариант 4.1: Длительность -->
45
+ <label class="flex items-start space-x-3 p-2 rounded-lg transition-colors cursor-pointer hover:bg-[var(--rui-hover-bg-soft)]">
46
+ <%= form.radio_button :form_duration_mode, :duration,
47
+ checked: (initial_form_duration_mode == :duration),
48
+ class: "mt-1 shrink-0",
49
+ data: { "#{identifier}_target": "modeDuration", action: "change->#{identifier}#toggleDurationEndMode" } %>
50
+
51
+ <div class="flex-1 min-w-0">
52
+ <span class="rui-label font-medium select-none">
53
+ <%= t('recurrence_ui.labels.duration') %>
54
+ </span>
55
+
56
+ <div class="flex flex-nowrap items-start gap-3 mt-2"
57
+ data-<%= identifier %>-target="durationContainer">
58
+
59
+ <%# Дни %>
60
+ <div class="flex flex-col items-center gap-1 shrink-0"
61
+ onclick="event.stopPropagation()">
62
+ <span class="rui-duration-unit">
63
+ <%= t('recurrence_ui.duration.form_days') %>
64
+ </span>
65
+
66
+ <%= form.number_field :form_duration_days,
67
+ value: initial_form_duration_days,
68
+ min: 0,
69
+ class: "rui-input w-16 text-center",
70
+ data: { "#{identifier}_target": "daysField", action: "change->#{identifier}#calculateEndDateFromDuration" } %>
71
+ </div>
72
+
73
+ <%# Часы %>
74
+ <div class="flex flex-col items-center gap-1 shrink-0"
75
+ onclick="event.stopPropagation()">
76
+ <span class="rui-duration-unit">
77
+ <%= t('recurrence_ui.duration.form_hours') %>
78
+ </span>
79
+
80
+ <%= form.number_field :form_duration_hours,
81
+ value: initial_form_duration_hours,
82
+ min: 0,
83
+ max: 23,
84
+ class: "rui-input w-16 text-center",
85
+ data: { "#{identifier}_target": "hoursField", action: "change->#{identifier}#calculateEndDateFromDuration" } %>
86
+ </div>
87
+
88
+ <%# Минуты %>
89
+ <div class="flex flex-col items-center gap-1 shrink-0"
90
+ onclick="event.stopPropagation()">
91
+ <span class="rui-duration-unit">
92
+ <%= t('recurrence_ui.duration.form_minutes') %>
93
+ </span>
94
+
95
+ <%= form.number_field :form_duration_minutes,
96
+ value: initial_form_duration_minutes,
97
+ min: 0,
98
+ max: 59,
99
+ class: "rui-input w-16 text-center",
100
+ data: { "#{identifier}_target": "minutesField", action: "change->#{identifier}#calculateEndDateFromDuration" } %>
101
+ </div>
102
+
103
+ </div>
104
+ </div>
105
+ </label>
106
+
107
+ <!-- Вариант 4.2: Дата и время окончания -->
108
+ <label class="flex items-start space-x-3 p-2 rounded-lg transition-colors cursor-pointer hover:bg-[var(--rui-hover-bg-soft)]">
109
+ <%= form.radio_button :form_duration_mode, :end_at,
110
+ checked: (initial_form_duration_mode == :end_at),
111
+ class: "mt-1 shrink-0",
112
+ data: { "#{identifier}_target": "modeEnd", action: "change->#{identifier}#toggleDurationEndMode" } %>
113
+
114
+ <div class="flex-1 min-w-0">
115
+ <span class="rui-label"><%= t('recurrence_ui.labels.end_at') %></span>
116
+
117
+ <!-- Поля даты и времени окончания -->
118
+ <div class="flex items-center space-x-2 mt-1" onclick="event.stopPropagation()">
119
+ <%= form.date_field :form_end_date,
120
+ value: initial_end_at&.to_date || Date.current,
121
+ class: "rui-input w-36 shrink-0 disabled:opacity-50 disabled:--rui-input-disabled-bg disabled:cursor-not-allowed",
122
+ disabled: (initial_form_duration_mode != :end_at),
123
+ data: { "#{identifier}_target": "endDateField"} %>
124
+ <%= form.time_field :form_end_time,
125
+ value: initial_end_at&.strftime('%H:%M') || '09:00',
126
+ class: ["rui-input w-24 shrink-0 disabled:opacity-50 disabled:--rui-input-disabled-bg disabled:cursor-not-allowed", ("rui-input-error" if record.errors[:form_end_time].any?)],
127
+ disabled: (initial_form_duration_mode != :end_at),
128
+ data: { "#{identifier}_target": "endTimeField" } %>
129
+ </div>
130
+ </div>
131
+ </label>
132
+
133
+ </div>
134
+
135
+ <!-- 4. При показе саммари: только заголовок Повторение и саммари под ним -->
136
+ <div data-<%= identifier %>-target="displayMode" class="space-y-1">
137
+ <div class="rui-panel-header">
138
+ <span class="rui-label">
139
+ <%= t('recurrence_ui.labels.recurrence') %>
140
+ </span>
141
+ </div>
142
+ <div>
143
+ <%= render "recurrence_ui/summary_link", form: form, identifier: identifier, rec_ui: self %>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- 5. Режим редактирования: заголовок Повторение + кнопка кода напротив, все поля под ними -->
148
+ <div data-<%= identifier %>-target="editMode" class="hidden space-y-3 pt-2">
149
+
150
+ <!-- Шапка с кнопкой кода справа напротив заголовка Повторение -->
151
+ <div class="rui-panel-header flex items-center justify-between">
152
+ <span class="rui-header-title">
153
+ <%= t('recurrence_ui.labels.recurrence') %>
154
+ </span>
155
+ <button type="button"
156
+ data-action="click-><%= identifier %>#toggleCodeEditor"
157
+ title="<%= t('recurrence_ui.actions.code_mode') %>"
158
+ class="rui-btn-secondary font-mono text-xs px-2 py-1">
159
+ &lt;/&gt;
160
+ </button>
161
+ </div>
162
+
163
+ <%# Visual Rule Editor Section %>
164
+ <div data-<%= identifier %>-target="visualEditor" class="space-y-3">
165
+ <div class="space-y-1">
166
+ <%# Dynamic Rule Cards Container %>
167
+ <div data-<%= identifier %>-target="rulesContainer" class="space-y-2"></div>
168
+
169
+ <%# Add Rule Action Button %>
170
+ <div class="pt-1">
171
+ <button type="button"
172
+ data-action="click-><%= identifier %>#addRule"
173
+ class="rui-link-action">
174
+ <span class="font-bold">+</span> <%= t('recurrence_ui.actions.add_rule', default: 'Add rule') %>
175
+ </button>
176
+ </div>
177
+ </div>
178
+
179
+ <%# Exceptions & Additional Dates Grid %>
180
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 rui-divider">
181
+ <div data-<%= identifier %>-target="rdatesBlock" class="hidden">
182
+ <%= render "recurrence_ui/rdates", form: form, identifier: identifier, rec_ui: self, rdates: rules[:rdates] || [] %>
183
+ </div>
184
+ <div data-<%= identifier %>-target="exceptionsBlock" class="hidden">
185
+ <%= render "recurrence_ui/exceptions", form: form, identifier: identifier, rec_ui: self, exdates: rules[:exdates] || [] %>
186
+ </div>
187
+ </div>
188
+
189
+ <%# Bottom Action Bar %>
190
+ <div class="pt-2">
191
+ <%= render "recurrence_ui/footer_actions", form: form, identifier: identifier, rec_ui: self %>
192
+ </div>
193
+ </div>
194
+
195
+ <%# Raw JSON / Code Editor Panel %>
196
+ <div data-<%= identifier %>-target="codeEditor" class="hidden space-y-3">
197
+ <div>
198
+ <label class="rui-label">
199
+ <%= t('recurrence_ui.labels.raw_json', default: 'Raw JSON') %>
200
+ </label>
201
+ <textarea data-<%= identifier %>-target="iceCubeField"
202
+ rows="6"
203
+ class="rui-textarea font-mono text-xs w-full"></textarea>
204
+ </div>
205
+ <div class="flex items-center justify-end space-x-2">
206
+ <button type="button"
207
+ data-action="click-><%= identifier %>#toggleCodeEditor"
208
+ class="rui-btn-secondary">
209
+ <%= t('recurrence_ui.actions.cancel') %>
210
+ </button>
211
+ <button type="button"
212
+ data-action="click-><%= identifier %>#applyCodeChanges"
213
+ class="rui-btn-primary">
214
+ <%= t('recurrence_ui.actions.apply_code') %>
215
+ </button>
216
+ </div>
217
+ </div>
218
+
219
+ </div>
220
+
221
+ <%# Rule Card Template %>
222
+ <template data-<%= identifier %>-target="ruleCardTemplate">
223
+ <%= render "recurrence_ui/rule_card",
224
+ form: form,
225
+ identifier: identifier,
226
+ days_for_ui: days_for_ui,
227
+ rec_ui: self,
228
+ rule_options: rule_options %>
229
+ </template>
230
+
231
+ <%# Hidden Form Field %>
232
+ <%= form.hidden_field field_name, value: rules_json, data: { "#{identifier}_target": "hiddenField" } %>
233
+ </div>
234
+ <% end %>
@@ -0,0 +1,204 @@
1
+ # app/components/recurrence_ui/recurrence_picker_component.rb
2
+ module RecurrenceUi
3
+ class RecurrencePickerComponent < ViewComponent::Base
4
+ include ActionView::Helpers::TranslationHelper
5
+ attr_reader :record, :i18n_prefix, :identifier, :locale, :time_zone, :week_start, :enabled_rule_types
6
+
7
+ def initialize(
8
+ record:,
9
+ form: nil,
10
+ i18n_prefix: 'recurrence_ui',
11
+ locale: nil,
12
+ time_zone: nil,
13
+ week_start: nil,
14
+ enabled_rule_types: nil
15
+ )
16
+ @raw_form = form
17
+ @record = record
18
+ @i18n_prefix = i18n_prefix
19
+ @identifier = "recurrence-ui"
20
+
21
+ # Resolve enabled_rule_types considering allowed values and order preservation
22
+ @enabled_rule_types = RecurrenceUi.config.resolved_enabled_rule_types(enabled_rule_types)
23
+
24
+ # Resolve environment configuration fallbacks
25
+ @locale = RecurrenceUi.config.resolved_locale(locale)
26
+ @time_zone = RecurrenceUi.config.resolved_time_zone(time_zone)
27
+ @week_start = RecurrenceUi.config.resolved_week_start(week_start)
28
+
29
+ log_configuration_details if Rails.env.development?
30
+ end
31
+ def field_name
32
+ @field_name ||= if record.class.respond_to?(:recurrence_rules_field)
33
+ record.class.recurrence_rules_field
34
+ else
35
+ RecurrenceUi.config.default_recurring_rules_field
36
+ end
37
+ end
38
+ def form
39
+ @form ||= @raw_form || ActionView::Helpers::FormBuilder.new(
40
+ record_name,
41
+ record,
42
+ helpers,
43
+ {}
44
+ )
45
+ end
46
+ # --- Динамические хелперы для вычисления начальных значений длительности ---
47
+ # Вычисляет количество дней длительности из записи
48
+ def initial_form_duration_days
49
+ dur = record.public_send(duration_field).to_i
50
+ return 0 if dur <= 0
51
+ (dur / 1.day).to_i
52
+ end
53
+
54
+ # Вычисляет оставшиеся часы длительности
55
+ def initial_form_duration_hours
56
+ dur = record.public_send(duration_field).to_i
57
+ return 1 if dur <= 0 # дефолтное значение, если пусто
58
+ ((dur % 1.day) / 1.hour).to_i
59
+ end
60
+
61
+ # Вычисляет оставшиеся минуты длительности
62
+ def initial_form_duration_minutes
63
+ dur = record.public_send(duration_field).to_i
64
+ return 0 if dur <= 0
65
+ ((dur % 1.hour) / 1.minute).to_i
66
+ end
67
+
68
+ # Определяет, какая радиокнопка должна быть выбрана при открытии формы (:duration или :end_at)
69
+ def initial_form_duration_mode
70
+ saved_mode = rules[:form_duration_mode]
71
+ return saved_mode.to_sym if saved_mode.present?
72
+
73
+ :duration # дефолт
74
+ end
75
+
76
+
77
+ # --- Динамические хелперы для имён полей из модели ---
78
+
79
+ def start_time_field
80
+ @start_time_field ||= record.class.respond_to?(:recurrence_start_time_field) ? record.class.recurrence_start_time_field : :start_at
81
+ end
82
+
83
+ def end_time_field
84
+ @end_time_field ||= record.class.respond_to?(:recurrence_end_time_field) ? record.class.recurrence_end_time_field : :end_at
85
+ end
86
+ # Возвращает начало события в часовом поясе самого события.
87
+ def initial_start_at
88
+ value = record.public_send(start_time_field)
89
+ return if value.blank?
90
+
91
+ value.in_time_zone(event_time_zone)
92
+ end
93
+
94
+ # Возвращает окончание события в часовом поясе самого события.
95
+ def initial_end_at
96
+ value = record.public_send(end_time_field)
97
+ return if value.blank?
98
+
99
+ value.in_time_zone(event_time_zone)
100
+ end
101
+
102
+ # Определяет часовой пояс события.
103
+ def event_time_zone
104
+ if record.respond_to?(:event_time_zone) && record.event_time_zone.present?
105
+ record.event_time_zone
106
+ else
107
+ zone_name = record.respond_to?(time_zone_field) ? record.public_send(time_zone_field) : nil
108
+
109
+ ActiveSupport::TimeZone[zone_name.presence] ||
110
+ Time.zone ||
111
+ ActiveSupport::TimeZone[Rails.application.config.time_zone]
112
+ end
113
+ end
114
+ def duration_field
115
+ @duration_field ||= record.class.respond_to?(:recurrence_duration_field) ? record.class.recurrence_duration_field : :duration
116
+ end
117
+ def time_zone_field
118
+ @time_zone_field ||= record.class.respond_to?(:recurrence_time_zone_field) ? record.class.recurrence_time_zone_field : :time_zone
119
+ end
120
+
121
+ def rules
122
+ @rules ||= begin
123
+ raw = record.public_send(field_name)
124
+ parsed = raw.is_a?(String) ? (JSON.parse(raw) rescue {}) : raw
125
+ (parsed || {}).with_indifferent_access
126
+ end
127
+ end
128
+
129
+ def rules_json
130
+ rules.to_json
131
+ end
132
+
133
+ def current_rule_type
134
+ raw_type = rules[:rule_type].to_s.demodulize.underscore.gsub('_rule', '')
135
+ return 'never' if raw_type.blank? || raw_type == 'never'
136
+
137
+ if %w[monthly yearly].include?(raw_type)
138
+ sub_type = validations[:day_of_week].present? ? 'by_weekday' : 'by_day_of_month'
139
+ "#{raw_type}_#{sub_type}"
140
+ else
141
+ raw_type
142
+ end
143
+ end
144
+
145
+ def validations = rules[:validations] || {}
146
+ def initial_interval = rules[:interval] || 1
147
+ def selected_weekly_days = (validations[:day] || []).map(&:to_i)
148
+ def selected_monthly_days = (validations[:day_of_month] || []).map(&:to_i)
149
+ def selected_nth_days = validations[:day_of_week] || {}
150
+
151
+ # Uses the pre-resolved @enabled_rule_types directly without re-resolving
152
+ def rule_options
153
+ enabled_rule_types.reject { |type| type == 'never' }.map do |type|
154
+ [I18n.t("#{i18n_prefix}.types.#{type}", locale: locale), type]
155
+ end
156
+ end
157
+
158
+ def days_for_ui
159
+ shift = (week_start == 1) ? 1 : 0
160
+ (0..6).to_a.rotate(shift)
161
+ end
162
+
163
+ def months_for_ui
164
+ I18n.with_locale(locale) do
165
+ (1..12).map { |m| [I18n.t("#{i18n_prefix}.date.month_names", default: I18n.t("recurrence_ui.date.month_names"))[m], m] }
166
+ end
167
+ end
168
+
169
+ def nth_weeks
170
+ I18n.with_locale(locale) do
171
+ [1, 2, 3, 4, 5, -1].map do |n|
172
+ label = if n == -1
173
+ I18n.t("#{i18n_prefix}.last", default: "last")
174
+ else
175
+ I18n.t("#{i18n_prefix}.nth_weeks.#{n}", default: "#{n}th")
176
+ end
177
+ [n, label]
178
+ end
179
+ end
180
+ end
181
+
182
+ def recurrence_summary
183
+ rule_hash = rules
184
+ return "" if rule_hash.blank?
185
+
186
+ RecurrenceUi::RecurrenceFormatterService.new(
187
+ rule_hash,
188
+ locale: locale,
189
+ time_zone: event_time_zone,
190
+ week_start: week_start
191
+ ).to_s
192
+ end
193
+
194
+ private
195
+
196
+ def record_name
197
+ record.respond_to?(:model_name) ? record.model_name.param_key : :record
198
+ end
199
+
200
+ def log_configuration_details
201
+ Rails.logger.debug("[RecurrenceUi] Locale: #{locale} | TimeZone: #{time_zone} | WeekStart: #{week_start}")
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,32 @@
1
+ # app/controllers/recurrence_ui/application_controller.rb
2
+ module RecurrenceUi
3
+ class ApplicationController < ActionController::Base
4
+ protect_from_forgery with: :exception
5
+
6
+ around_action :with_recurrence_context
7
+
8
+ private
9
+
10
+ def with_recurrence_context(&action)
11
+ # Resolve the locale, time zone, and week start based on the provided params or defaults
12
+ resolved_locale = RecurrenceUi.config.resolved_locale(params[:locale])
13
+ resolved_time_zone = RecurrenceUi.config.resolved_time_zone(params[:time_zone])
14
+
15
+ raw_week_start = RecurrenceUi.config.resolved_week_start(params[:week_start])
16
+ resolved_week_start = (raw_week_start == 1) ? :monday : :sunday
17
+
18
+ # Force the environment to the time of HTTP request execution
19
+ I18n.with_locale(resolved_locale) do
20
+ Time.use_zone(resolved_time_zone) do
21
+ old_week_start = Date.beginning_of_week
22
+ Date.beginning_of_week = resolved_week_start
23
+ begin
24
+ action.call
25
+ ensure
26
+ Date.beginning_of_week = old_week_start
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # app/helpers/recurrence_ui/application_helper.rb
2
+ module RecurrenceUi
3
+ module ApplicationHelper
4
+ def recurrence_ui_tag(options = {})
5
+ # Ensure that the locale is set to the current locale if not provided in options
6
+ # This is important for the translations to work correctly, especially when the locale is changed dynamically in the application.
7
+ # Priority: local options -> gem config -> defaults
8
+ locale = options[:locale] || RecurrenceUi.config.resolved_locale
9
+ time_zone = options[:time_zone] || RecurrenceUi.config.resolved_time_zone
10
+ week_start = options[:week_start] || RecurrenceUi.config.resolved_week_start
11
+
12
+ content_tag :div,
13
+ "",
14
+ data: {
15
+ controller: "recurrence-ui",
16
+ "recurrence-ui-locale-value": locale,
17
+ "recurrence-ui-time-zone-value": time_zone,
18
+ "recurrence-ui-week-start-value": week_start,
19
+ "recurrence-ui-translations-value": I18n.t("recurrence_ui", locale: locale).to_json
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ import RecurrenceUiController from "./recurrence_ui_controller"
2
+
3
+ export function registerRecurrenceUiController(application) {
4
+ if (application) {
5
+ application.register("recurrence-ui", RecurrenceUiController)
6
+ }
7
+ }
8
+
9
+ // Automatically register the controller if Stimulus is already initialized
10
+ if (typeof window !== "undefined") {
11
+ if (window.Stimulus) {
12
+ window.Stimulus.register("recurrence-ui", RecurrenceUiController)
13
+ } else {
14
+ document.addEventListener("DOMContentLoaded", () => {
15
+ if (window.Stimulus) {
16
+ window.Stimulus.register("recurrence-ui", RecurrenceUiController)
17
+ }
18
+ })
19
+ }
20
+ }
21
+
22
+ export { RecurrenceUiController }