yummy-guide-generic-administrate 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/Gemfile +4 -0
- data/README.md +243 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/yummy_guide_administrate/filter_form.js +219 -0
- data/app/assets/javascripts/yummy_guide_administrate/sticky_left_columns.js +141 -0
- data/app/assets/stylesheets/yummy_guide_administrate/components.scss +199 -0
- data/app/controllers/concerns/yummy_guide/administrate/datetime_filter_parameters.rb +40 -0
- data/app/controllers/concerns/yummy_guide/administrate/default_sorting.rb +46 -0
- data/app/dashboards/yummy_guide/administrate/application_dashboard.rb +46 -0
- data/app/fields/yummy_guide/administrate/fields/area/picture_field.rb +106 -0
- data/app/fields/yummy_guide/administrate/fields/json_pretty_field.rb +29 -0
- data/app/fields/yummy_guide/administrate/fields/version_item_field.rb +62 -0
- data/app/fields/yummy_guide/administrate/fields/version_whodunnit_field.rb +65 -0
- data/app/helpers/yummy_guide/administrate/collection_helper.rb +62 -0
- data/app/helpers/yummy_guide/administrate/filter_form_helper.rb +80 -0
- data/app/views/fields/yummy_guide_administrate/area/picture/_form.html.erb +36 -0
- data/app/views/fields/yummy_guide_administrate/area/picture/_index.html.erb +2 -0
- data/app/views/fields/yummy_guide_administrate/area/picture/_show.html.erb +24 -0
- data/app/views/fields/yummy_guide_administrate/json_pretty_field/_index.html.erb +2 -0
- data/app/views/fields/yummy_guide_administrate/json_pretty_field/_show.html.erb +6 -0
- data/app/views/fields/yummy_guide_administrate/version_item_field/_index.html.erb +2 -0
- data/app/views/fields/yummy_guide_administrate/version_item_field/_show.html.erb +6 -0
- data/app/views/fields/yummy_guide_administrate/version_whodunnit_field/_index.html.erb +2 -0
- data/app/views/fields/yummy_guide_administrate/version_whodunnit_field/_show.html.erb +6 -0
- data/app/views/yummy_guide/administrate/administrate/application/_collection.html.erb +62 -0
- data/app/views/yummy_guide/administrate/filter_forms/_checkbox_group.html.erb +30 -0
- data/app/views/yummy_guide/administrate/filter_forms/_datetime_field.html.erb +30 -0
- data/app/views/yummy_guide/administrate/filter_forms/_frame.html.erb +28 -0
- data/lib/generic/administrate.rb +3 -0
- data/lib/yummy_guide/administrate/engine.rb +18 -0
- data/lib/yummy_guide/administrate/version.rb +8 -0
- data/lib/yummy_guide/administrate.rb +13 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/yummy_guide/administrate/application_dashboard_spec.rb +39 -0
- data/spec/yummy_guide/administrate/collection_helper_spec.rb +25 -0
- data/spec/yummy_guide/administrate/datetime_filter_parameters_spec.rb +52 -0
- data/spec/yummy_guide/administrate/fields/json_pretty_field_spec.rb +22 -0
- data/spec/yummy_guide/administrate/fields/version_item_field_spec.rb +30 -0
- data/spec/yummy_guide/administrate/fields/version_whodunnit_field_spec.rb +31 -0
- data/spec/yummy_guide/administrate/filter_form_helper_spec.rb +40 -0
- data/yummy-guide-generic-administrate.gemspec +38 -0
- metadata +156 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b78001f457aaa653797c44c84f1d169f7e870932e5b7ea43ae1d3994ecab9747
|
|
4
|
+
data.tar.gz: 437ffe942299cbda22d3eca1e72ecc625069880862848b94d3da3520cb3ecf12
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8a8b10627365f4b549f43701974d3e8ab0066d6a656bdab19fbe310dce3ed258b1b80e58db16ce72219bda72030f9269fd71a752324a016cc44426464d64d0c4
|
|
7
|
+
data.tar.gz: 589d405d80686573b6fd8efd2a45795861e010203b703f85e1a0c142dec044c8cf603189480f1adfd581c8fc24c61690a56335c1b487d3be869322c7644bdc91
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# yummy-guide-generic-administrate
|
|
2
|
+
|
|
3
|
+
`yummy-guide-generic-administrate` は、Yummy Guide 系 Rails アプリで共通利用する
|
|
4
|
+
[`Administrate`](https://github.com/thoughtbot/administrate) 拡張をまとめた Rails
|
|
5
|
+
Engine です。
|
|
6
|
+
|
|
7
|
+
`Administrate` 本体を置き換えるものではなく、ダッシュボードの共通既定値、
|
|
8
|
+
フィルター UI、一覧表示 helper、共通 field などを再利用しやすくするための補助
|
|
9
|
+
gem です。
|
|
10
|
+
|
|
11
|
+
## 前提
|
|
12
|
+
|
|
13
|
+
- Ruby `>= 3.2.2`
|
|
14
|
+
- Rails `>= 7.0`, `< 7.2`
|
|
15
|
+
- Administrate `>= 0.19`, `< 0.21`
|
|
16
|
+
- `sprockets-rails`
|
|
17
|
+
|
|
18
|
+
## インストール
|
|
19
|
+
|
|
20
|
+
`Gemfile` に追加します。
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "yummy-guide-generic-administrate"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
その後、依存 gem をインストールします。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 提供するもの
|
|
33
|
+
|
|
34
|
+
- `YummyGuide::Administrate::ApplicationDashboard`
|
|
35
|
+
- 一覧画面の既定ソートと固定列数の共通基底クラス
|
|
36
|
+
- `YummyGuide::Administrate::DefaultSorting`
|
|
37
|
+
- dashboard 側の既定ソート設定を controller に反映する concern
|
|
38
|
+
- `YummyGuide::Administrate::DatetimeFilterParameters`
|
|
39
|
+
- 日付、時、分の分割パラメータを 1 つの datetime 文字列へ正規化する concern
|
|
40
|
+
- `YummyGuide::Administrate::CollectionHelper`
|
|
41
|
+
- 一覧テーブルの固定列数、リンク生成、action partial 解決を補助する helper
|
|
42
|
+
- `YummyGuide::Administrate::FilterFormHelper`
|
|
43
|
+
- datetime フィルターや checkbox group の組み立てを補助する helper
|
|
44
|
+
- 共通 partial / assets
|
|
45
|
+
- collection partial
|
|
46
|
+
- filter form partial
|
|
47
|
+
- `filter_form.js`
|
|
48
|
+
- `sticky_left_columns.js`
|
|
49
|
+
- `components.css`
|
|
50
|
+
- 共通 field
|
|
51
|
+
- `YummyGuide::Administrate::Fields::JsonPrettyField`
|
|
52
|
+
- `YummyGuide::Administrate::Fields::VersionItemField`
|
|
53
|
+
- `YummyGuide::Administrate::Fields::VersionWhodunnitField`
|
|
54
|
+
- `YummyGuide::Administrate::Fields::Area::PictureField`
|
|
55
|
+
|
|
56
|
+
## 利用方法
|
|
57
|
+
|
|
58
|
+
### Dashboard の基底クラス
|
|
59
|
+
|
|
60
|
+
共通の既定ソートと固定列数設定を使う場合は、dashboard を
|
|
61
|
+
`YummyGuide::Administrate::ApplicationDashboard` から継承します。
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class ApplicationDashboard < YummyGuide::Administrate::ApplicationDashboard
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
必要に応じて各 dashboard 側で上書きできます。
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class Admin::ArticleDashboard < ApplicationDashboard
|
|
72
|
+
COLLECTION_ATTRIBUTES = %i[id title status created_at].freeze
|
|
73
|
+
COLLECTION_SORTABLE_ATTRIBUTES = %i[id title status created_at].freeze
|
|
74
|
+
INDEX_FIXED_COLUMNS_COUNT = 2
|
|
75
|
+
|
|
76
|
+
def default_sorting_attribute
|
|
77
|
+
:published_at
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def default_sorting_direction
|
|
81
|
+
:desc
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Controller concern / helper
|
|
87
|
+
|
|
88
|
+
管理画面の基底 controller で共通 concern と helper を読み込みます。
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
class Admin::ApplicationController < Administrate::ApplicationController
|
|
92
|
+
include YummyGuide::Administrate::DefaultSorting
|
|
93
|
+
include YummyGuide::Administrate::DatetimeFilterParameters
|
|
94
|
+
|
|
95
|
+
helper YummyGuide::Administrate::CollectionHelper
|
|
96
|
+
helper YummyGuide::Administrate::FilterFormHelper
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Collection partial
|
|
101
|
+
|
|
102
|
+
ホストアプリ側の `app/views/administrate/application/_collection.html.erb` から、
|
|
103
|
+
engine の共通 partial に委譲します。
|
|
104
|
+
|
|
105
|
+
```erb
|
|
106
|
+
<%= render "yummy_guide/administrate/administrate/application/collection",
|
|
107
|
+
collection_presenter: collection_presenter,
|
|
108
|
+
page: page,
|
|
109
|
+
resources: resources,
|
|
110
|
+
table_title: table_title,
|
|
111
|
+
namespace: namespace,
|
|
112
|
+
resource_class: resource_class,
|
|
113
|
+
collection_field_name: collection_field_name %>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Datetime filter
|
|
117
|
+
|
|
118
|
+
filter form の枠と datetime 入力 partial を組み合わせて利用できます。
|
|
119
|
+
|
|
120
|
+
```erb
|
|
121
|
+
<%= render "yummy_guide/administrate/filter_forms/frame",
|
|
122
|
+
path: path,
|
|
123
|
+
form: form,
|
|
124
|
+
method: method,
|
|
125
|
+
current_values: search_options do |f, values| %>
|
|
126
|
+
<tr>
|
|
127
|
+
<td><%= f.label :start_at, "開始日時" %></td>
|
|
128
|
+
<td>
|
|
129
|
+
<%= render "yummy_guide/administrate/filter_forms/datetime_field",
|
|
130
|
+
form_scope: form,
|
|
131
|
+
field_name: :start_at,
|
|
132
|
+
current_value: values["start_at"],
|
|
133
|
+
css_class: "#{form}_start_at" %>
|
|
134
|
+
</td>
|
|
135
|
+
</tr>
|
|
136
|
+
<% end %>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
controller 側では、必要な filter key を指定して datetime パラメータを正規化します。
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
def search_term
|
|
143
|
+
resource_params = params[resource_name] || params[resource_name.to_sym] || {}
|
|
144
|
+
raw_filters = resource_params[:search] || resource_params["search"] || {}
|
|
145
|
+
|
|
146
|
+
normalized_filters = normalize_datetime_filter_params(
|
|
147
|
+
raw_filters,
|
|
148
|
+
keys: %i[start_at end_at]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
normalized_filters
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Asset の読み込み
|
|
156
|
+
|
|
157
|
+
この engine の asset はホストアプリ側で明示的に読み込んでください。
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
//= require yummy_guide_administrate/filter_form
|
|
161
|
+
//= require yummy_guide_administrate/sticky_left_columns
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```scss
|
|
165
|
+
*= require yummy_guide_administrate/components
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Custom field
|
|
169
|
+
|
|
170
|
+
dashboard から共通 field を利用できます。
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
ATTRIBUTE_TYPES = {
|
|
174
|
+
metadata: YummyGuide::Administrate::Fields::JsonPrettyField,
|
|
175
|
+
item: YummyGuide::Administrate::Fields::VersionItemField.with_options(namespace: :admin),
|
|
176
|
+
whodunnit: YummyGuide::Administrate::Fields::VersionWhodunnitField.with_options(
|
|
177
|
+
namespace: :admin,
|
|
178
|
+
user_class: "User"
|
|
179
|
+
),
|
|
180
|
+
pictures: YummyGuide::Administrate::Fields::Area::PictureField
|
|
181
|
+
}.freeze
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## 各 field の用途
|
|
185
|
+
|
|
186
|
+
### JsonPrettyField
|
|
187
|
+
|
|
188
|
+
JSON 文字列または JSON 互換オブジェクトを、整形済みの文字列として表示します。
|
|
189
|
+
|
|
190
|
+
### VersionItemField
|
|
191
|
+
|
|
192
|
+
PaperTrail の `item` や `reify` 結果をもとに、対象 resource のラベルと詳細画面への
|
|
193
|
+
リンクを表示します。`namespace` は `:admin` が既定です。
|
|
194
|
+
|
|
195
|
+
### VersionWhodunnitField
|
|
196
|
+
|
|
197
|
+
PaperTrail の `whodunnit` から操作ユーザーを解決して表示します。
|
|
198
|
+
|
|
199
|
+
- 既定では `User` クラスを参照します
|
|
200
|
+
- `user_class` で別クラスを指定できます
|
|
201
|
+
- `user_label` に proc を渡すと表示ラベルを上書きできます
|
|
202
|
+
|
|
203
|
+
### Area::PictureField
|
|
204
|
+
|
|
205
|
+
画像添付用の field です。Active Storage の添付オブジェクト、または `attachments`
|
|
206
|
+
を返すオブジェクトを前提としています。
|
|
207
|
+
|
|
208
|
+
必要に応じて以下の option を指定できます。
|
|
209
|
+
|
|
210
|
+
- `max_uploads`
|
|
211
|
+
- `input_name`
|
|
212
|
+
- `purge_input_name`
|
|
213
|
+
- `attachment_url`
|
|
214
|
+
- `preview_url`
|
|
215
|
+
|
|
216
|
+
## 参照
|
|
217
|
+
|
|
218
|
+
- Administrate 公式リポジトリ
|
|
219
|
+
- <https://github.com/thoughtbot/administrate>
|
|
220
|
+
- Administrate gem ページ
|
|
221
|
+
- <https://rubygems.org/gems/administrate>
|
|
222
|
+
|
|
223
|
+
## リリース手順
|
|
224
|
+
|
|
225
|
+
このリポジトリは `bundler/gem_tasks` を利用しているため、標準の gem リリースタスク
|
|
226
|
+
で公開できます。
|
|
227
|
+
|
|
228
|
+
1. `lib/yummy_guide/administrate/version.rb` の `VERSION` を更新する
|
|
229
|
+
2. 必要な変更を commit 済みの状態にする
|
|
230
|
+
3. 必要に応じて `bundle exec rake spec` で確認する
|
|
231
|
+
4. `bundle exec rake release` を実行する
|
|
232
|
+
|
|
233
|
+
`bundle exec rake release` を実行すると、現在の `VERSION` をもとに `v<version>` の
|
|
234
|
+
git tag を作成し、gem を build して `rubygems.org` へ push します。
|
|
235
|
+
|
|
236
|
+
事前に RubyGems への push 権限があること、ローカル環境で `gem push` が利用できる
|
|
237
|
+
ことを確認してください。
|
|
238
|
+
|
|
239
|
+
## 注意点
|
|
240
|
+
|
|
241
|
+
- asset は precompile 対象に追加されますが、ホストアプリ側での読み込み設定は別途必要です
|
|
242
|
+
- `VersionWhodunnitField` は対象ユーザー class や表示ラベルをアプリ事情に合わせて調整してください
|
|
243
|
+
- `Area::PictureField` の URL 解決は、必要なら option で明示的に上書きしてください
|
data/Rakefile
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var FORM_SELECTOR = "[data-yummy-guide-administrate-filter-form]";
|
|
3
|
+
|
|
4
|
+
function initializeForms(root) {
|
|
5
|
+
var forms = [];
|
|
6
|
+
|
|
7
|
+
if (root.matches && root.matches(FORM_SELECTOR)) {
|
|
8
|
+
forms.push(root);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (root.querySelectorAll) {
|
|
12
|
+
root.querySelectorAll(FORM_SELECTOR).forEach(function(formEl) {
|
|
13
|
+
forms.push(formEl);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
forms.forEach(initializeForm);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function initializeForm(formEl) {
|
|
21
|
+
if (formEl.dataset.ygAdministrateFilterFormInitialized === "true") {
|
|
22
|
+
syncDatetimeFilterFields(formEl);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
formEl.dataset.ygAdministrateFilterFormInitialized = "true";
|
|
27
|
+
formEl.addEventListener("submit", function() {
|
|
28
|
+
syncDatetimeFilterFields(formEl);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
formEl.querySelectorAll("[data-datetime-filter]").forEach(function(groupEl) {
|
|
32
|
+
groupEl.addEventListener("change", function(event) {
|
|
33
|
+
handleDatetimeFilterChange(formEl, groupEl, event);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
syncBlankMinuteOptionState(groupEl);
|
|
37
|
+
syncDatetimeTimeDisabledState(groupEl);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
formEl.querySelectorAll('[data-behavior="filter-form-clear"]').forEach(function(buttonEl) {
|
|
41
|
+
buttonEl.addEventListener("click", function() {
|
|
42
|
+
clearFormFields(formEl);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
formEl.querySelectorAll('[data-behavior="checkbox-group-select-all"]').forEach(function(buttonEl) {
|
|
47
|
+
buttonEl.addEventListener("click", function() {
|
|
48
|
+
setCheckboxGroupState(formEl, buttonEl.dataset.target, true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
formEl.querySelectorAll('[data-behavior="checkbox-group-clear-all"]').forEach(function(buttonEl) {
|
|
53
|
+
buttonEl.addEventListener("click", function() {
|
|
54
|
+
setCheckboxGroupState(formEl, buttonEl.dataset.target, false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
syncDatetimeFilterFields(formEl);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function clearFormFields(formEl) {
|
|
62
|
+
formEl.querySelectorAll("input, select, textarea").forEach(function(fieldEl) {
|
|
63
|
+
if (fieldEl.disabled || fieldEl.type === "hidden" || fieldEl.type === "submit") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fieldEl.tagName === "SELECT") {
|
|
68
|
+
fieldEl.value = "";
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (fieldEl.type === "checkbox" || fieldEl.type === "radio") {
|
|
73
|
+
fieldEl.checked = false;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fieldEl.value = "";
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
formEl.querySelectorAll("[data-datetime-filter]").forEach(function(groupEl) {
|
|
81
|
+
clearDatetimeTimeParts(groupEl);
|
|
82
|
+
syncDatetimeTimeDisabledState(groupEl);
|
|
83
|
+
syncBlankMinuteOptionState(groupEl);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
syncDatetimeFilterFields(formEl);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setCheckboxGroupState(formEl, groupName, checked) {
|
|
90
|
+
if (!groupName) return;
|
|
91
|
+
|
|
92
|
+
formEl.querySelectorAll('[data-checkbox-group-item="' + groupName + '"]').forEach(function(checkboxEl) {
|
|
93
|
+
if (!checkboxEl.disabled) {
|
|
94
|
+
checkboxEl.checked = checked;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleDatetimeFilterChange(formEl, groupEl, event) {
|
|
100
|
+
if (event.target && event.target.dataset.datetimePart === "date") {
|
|
101
|
+
if (event.target.value) {
|
|
102
|
+
syncRangeEndDate(formEl, groupEl);
|
|
103
|
+
} else {
|
|
104
|
+
clearDatetimeTimeParts(groupEl);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (event.target && event.target.dataset.datetimePart === "hour") {
|
|
109
|
+
syncDatetimeMinuteOnHourChange(groupEl);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (event.target && event.target.dataset.datetimePart === "minute") {
|
|
113
|
+
preventBlankDatetimeMinute(groupEl);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
syncDatetimeTimeDisabledState(groupEl);
|
|
117
|
+
syncBlankMinuteOptionState(groupEl);
|
|
118
|
+
syncDatetimeFilterFields(formEl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function syncRangeEndDate(formEl, groupEl) {
|
|
122
|
+
var endTarget = groupEl.dataset.datetimeEndTarget;
|
|
123
|
+
if (!endTarget) return;
|
|
124
|
+
|
|
125
|
+
var startDateEl = groupEl.querySelector('[data-datetime-part="date"]');
|
|
126
|
+
if (!startDateEl || !startDateEl.value) return;
|
|
127
|
+
|
|
128
|
+
var endGroupEl = formEl.querySelector('[data-datetime-field="' + endTarget + '"]');
|
|
129
|
+
if (!endGroupEl) return;
|
|
130
|
+
|
|
131
|
+
var endDateEl = endGroupEl.querySelector('[data-datetime-part="date"]');
|
|
132
|
+
if (!endDateEl || endDateEl.value) return;
|
|
133
|
+
|
|
134
|
+
endDateEl.value = startDateEl.value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function clearDatetimeTimeParts(groupEl) {
|
|
138
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
139
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
140
|
+
|
|
141
|
+
if (hourEl) hourEl.value = "";
|
|
142
|
+
if (minuteEl) minuteEl.value = "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function syncDatetimeMinuteOnHourChange(groupEl) {
|
|
146
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
147
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
148
|
+
if (!hourEl || !minuteEl) return;
|
|
149
|
+
|
|
150
|
+
minuteEl.value = hourEl.value ? "00" : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function preventBlankDatetimeMinute(groupEl) {
|
|
154
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
155
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
156
|
+
if (!hourEl || !minuteEl) return;
|
|
157
|
+
|
|
158
|
+
if (hourEl.value && minuteEl.value === "") {
|
|
159
|
+
minuteEl.value = "00";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function syncBlankMinuteOptionState(groupEl) {
|
|
164
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
165
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
166
|
+
if (!hourEl || !minuteEl) return;
|
|
167
|
+
|
|
168
|
+
var blankOptionEl = minuteEl.querySelector('option[value=""]');
|
|
169
|
+
if (blankOptionEl) {
|
|
170
|
+
blankOptionEl.disabled = !!hourEl.value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function syncDatetimeTimeDisabledState(groupEl) {
|
|
175
|
+
var dateEl = groupEl.querySelector('[data-datetime-part="date"]');
|
|
176
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
177
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
178
|
+
var dateDisabled = !dateEl || !dateEl.value;
|
|
179
|
+
var minuteDisabled = dateDisabled || !hourEl || !hourEl.value;
|
|
180
|
+
|
|
181
|
+
if (hourEl) hourEl.disabled = dateDisabled;
|
|
182
|
+
if (minuteEl) minuteEl.disabled = minuteDisabled;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function syncDatetimeFilterFields(formEl) {
|
|
186
|
+
formEl.querySelectorAll("[data-datetime-filter]").forEach(function(groupEl) {
|
|
187
|
+
var combinedEl = groupEl.querySelector('[data-datetime-part="combined"]');
|
|
188
|
+
var dateEl = groupEl.querySelector('[data-datetime-part="date"]');
|
|
189
|
+
var hourEl = groupEl.querySelector('[data-datetime-part="hour"]');
|
|
190
|
+
var minuteEl = groupEl.querySelector('[data-datetime-part="minute"]');
|
|
191
|
+
|
|
192
|
+
if (!combinedEl || !dateEl || !hourEl || !minuteEl) return;
|
|
193
|
+
|
|
194
|
+
syncDatetimeTimeDisabledState(groupEl);
|
|
195
|
+
|
|
196
|
+
if (!dateEl.value) {
|
|
197
|
+
combinedEl.value = "";
|
|
198
|
+
combinedEl.setAttribute("value", "");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
combinedEl.value = hourEl.value && minuteEl.value ? dateEl.value + "T" + hourEl.value + ":" + minuteEl.value : dateEl.value;
|
|
203
|
+
combinedEl.setAttribute("value", combinedEl.value);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initializeFromDocument() {
|
|
208
|
+
initializeForms(document);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (document.readyState === "loading") {
|
|
212
|
+
document.addEventListener("DOMContentLoaded", initializeFromDocument, { once: true });
|
|
213
|
+
} else {
|
|
214
|
+
initializeFromDocument();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
document.addEventListener("turbo:load", initializeFromDocument);
|
|
218
|
+
})();
|
|
219
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var TABLE_SELECTOR = "table[data-fixed-columns-count]";
|
|
3
|
+
var resizeObservers = new WeakMap();
|
|
4
|
+
|
|
5
|
+
function directCells(row) {
|
|
6
|
+
return Array.from(row.children).filter(function(cell) {
|
|
7
|
+
return cell.tagName === "TH" || cell.tagName === "TD";
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function preciseNumber(value) {
|
|
12
|
+
return Math.round(value * 1000) / 1000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function measuredWidth(element) {
|
|
16
|
+
if (!element) return 0;
|
|
17
|
+
|
|
18
|
+
var rectWidth = element.getBoundingClientRect().width;
|
|
19
|
+
return preciseNumber(rectWidth || element.offsetWidth || 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cssPixelValue(value) {
|
|
23
|
+
return preciseNumber(value) + "px";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resetStickyColumns(table) {
|
|
27
|
+
table.querySelectorAll(".sticky-left").forEach(function(cell) {
|
|
28
|
+
cell.classList.remove("sticky-left", "sticky-left--last");
|
|
29
|
+
cell.style.removeProperty("left");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fixedColumnsCount(table) {
|
|
34
|
+
var parsedCount = parseInt(table.dataset.fixedColumnsCount || "0", 10);
|
|
35
|
+
return Number.isNaN(parsedCount) ? 0 : Math.max(parsedCount, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stickyOffsets(table, count) {
|
|
39
|
+
var headerRow = table.querySelector("thead tr");
|
|
40
|
+
if (!headerRow) return [];
|
|
41
|
+
|
|
42
|
+
var widths = directCells(headerRow).slice(0, count).map(function(cell) {
|
|
43
|
+
return measuredWidth(cell);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!widths.some(Boolean)) {
|
|
47
|
+
Array.from(table.querySelectorAll("tbody tr, tfoot tr")).some(function(row) {
|
|
48
|
+
var cells = directCells(row);
|
|
49
|
+
if (cells.length < count || cells.some(function(cell) { return cell.colSpan > 1; })) return false;
|
|
50
|
+
|
|
51
|
+
widths = cells.slice(0, count).map(function(cell) {
|
|
52
|
+
return measuredWidth(cell);
|
|
53
|
+
});
|
|
54
|
+
return widths.some(Boolean);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var offsets = [];
|
|
59
|
+
var currentLeft = 0;
|
|
60
|
+
|
|
61
|
+
widths.forEach(function(width) {
|
|
62
|
+
offsets.push(currentLeft);
|
|
63
|
+
currentLeft += width;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return offsets;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyStickyColumns(table) {
|
|
70
|
+
resetStickyColumns(table);
|
|
71
|
+
|
|
72
|
+
var count = fixedColumnsCount(table);
|
|
73
|
+
if (count === 0) return;
|
|
74
|
+
|
|
75
|
+
var offsets = stickyOffsets(table, count);
|
|
76
|
+
if (offsets.length === 0) return;
|
|
77
|
+
|
|
78
|
+
table.querySelectorAll("thead tr, tbody tr, tfoot tr").forEach(function(row) {
|
|
79
|
+
var cells = directCells(row);
|
|
80
|
+
if (cells.length === 0 || cells.some(function(cell) { return cell.colSpan > 1; })) return;
|
|
81
|
+
|
|
82
|
+
cells.slice(0, offsets.length).forEach(function(cell, index) {
|
|
83
|
+
cell.classList.add("sticky-left");
|
|
84
|
+
if (index === offsets.length - 1) {
|
|
85
|
+
cell.classList.add("sticky-left--last");
|
|
86
|
+
}
|
|
87
|
+
cell.style.left = cssPixelValue(offsets[index]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function observeStickyColumns(table) {
|
|
93
|
+
if (!window.ResizeObserver || resizeObservers.has(table)) return;
|
|
94
|
+
|
|
95
|
+
var observer = new ResizeObserver(function() {
|
|
96
|
+
window.requestAnimationFrame(function() {
|
|
97
|
+
applyStickyColumns(table);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
observer.observe(table);
|
|
102
|
+
if (table.parentElement) {
|
|
103
|
+
observer.observe(table.parentElement);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
resizeObservers.set(table, observer);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function initializeStickyColumns(root) {
|
|
110
|
+
var tables = [];
|
|
111
|
+
|
|
112
|
+
if (root.matches && root.matches(TABLE_SELECTOR)) {
|
|
113
|
+
tables.push(root);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (root.querySelectorAll) {
|
|
117
|
+
root.querySelectorAll(TABLE_SELECTOR).forEach(function(table) {
|
|
118
|
+
tables.push(table);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
tables.forEach(function(table) {
|
|
123
|
+
applyStickyColumns(table);
|
|
124
|
+
observeStickyColumns(table);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function initializeFromDocument() {
|
|
129
|
+
initializeStickyColumns(document);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (document.readyState === "loading") {
|
|
133
|
+
document.addEventListener("DOMContentLoaded", initializeFromDocument, { once: true });
|
|
134
|
+
} else {
|
|
135
|
+
initializeFromDocument();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
document.addEventListener("turbo:load", initializeFromDocument);
|
|
139
|
+
window.addEventListener("resize", initializeFromDocument);
|
|
140
|
+
})();
|
|
141
|
+
|