yummy-guide-generic-administrate 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 473c94cf71c0941c5c1126338391f17de4ddb8f6f357a14db879008575fd5902
4
- data.tar.gz: 5a4fb4e5a0ba633c27ca3ccca0150d91b397219b069f8db9a3e9d4b9287557f2
3
+ metadata.gz: 12a0170a3f475fc86fa867a2dcdbbb558cce85d0b1024687cbcc84a4cdbc5501
4
+ data.tar.gz: bb0c145f4aaea534a02bff5f173e7917172193b6cca10f9a865cd4b2ebd6eda8
5
5
  SHA512:
6
- metadata.gz: b8e09eb75afdb7f9957973afb70a8cb44d2bb30c3d0c434df7f982b6e464086d877bf2e5f3089192118f04aba384f6311658c9cf45735b24f4418207803dcfd9
7
- data.tar.gz: '0810e4924d10070acc6f51e095c5a72b0b8b7ca73fb3c77ca23617e0d8598813e65f3ad4204b3b12efdade3425713f8801bf7ac6c1458dbe8ba30fe38944ab19'
6
+ metadata.gz: 35cec1573335c70b7779deb4cc9c492ff045a3acf785c5fac447789b1a80c6e7dac5b61423978a2a9751c7df6fb4c880aa5560d14652cd06fe6b8addec743530
7
+ data.tar.gz: 391aaee40d54f4f9ab58513d53bedfa9cdffd3a02f882ab679a690f394e9ea289927b34eb86b10da444348ddf8e34976c6482fa328810934240242a7dc091e76
data/README.md CHANGED
@@ -46,6 +46,7 @@ bundle install
46
46
  - fixed table header partial
47
47
  - filter form partial
48
48
  - `clipboards.js`
49
+ - `fixed_submit_actions.js`
49
50
  - `filter_form.js`
50
51
  - `sticky_left_columns.js`
51
52
  - `sticky_table_headers.js`
@@ -259,6 +260,7 @@ end
259
260
 
260
261
  ```js
261
262
  //= require yummy_guide_administrate/clipboards
263
+ //= require yummy_guide_administrate/fixed_submit_actions
262
264
  //= require yummy_guide_administrate/filter_form
263
265
  //= require yummy_guide_administrate/sticky_left_columns
264
266
  //= require yummy_guide_administrate/sticky_table_headers
@@ -268,6 +270,56 @@ end
268
270
  *= require yummy_guide_administrate/components
269
271
  ```
270
272
 
273
+ ### 固定更新ボタン
274
+
275
+ 管理画面の `new` / `edit` 系フォームでは、submit セクションを画面下に固定表示できます。
276
+ この機能は `fixed_submit_actions.js` と `components.css` に含まれる style によって動作します。
277
+
278
+ #### 固定対象を明示指定する場合
279
+
280
+ 固定したい submit セクションに `data-fixed-submit-actions="true"` を付けます。
281
+
282
+ ```erb
283
+ <div class="form-actions" data-fixed-submit-actions="true">
284
+ <%= f.submit %>
285
+ </div>
286
+ ```
287
+
288
+ この指定がある場合、gem はその submit セクションを最優先で固定対象にします。
289
+ 1 ページに複数フォームがあっても、明示指定した submit セクションだけが固定表示されます。
290
+
291
+ #### 設定しなかった場合の挙動
292
+
293
+ `data-fixed-submit-actions="true"` が 1 件もない場合、gem は admin の `new` / `edit`
294
+ ページで submit セクションを自動選択します。
295
+
296
+ - 各フォーム内の `.form-actions` / `.form_submit` を候補にする
297
+ - `form-actions--top` が付いた上部 submit は除外する
298
+ - `data-fixed-submit-exclude="true"` が付いた submit セクションも除外する
299
+ - 各フォームでは最後の submit セクションだけを候補にする
300
+ - 複数フォームがあるページでは、現在表示中のフォームに対応する submit を固定する
301
+
302
+ 上部 submit と下部 submit の両方があるフォームでは、下部 submit が自動で選ばれます。
303
+
304
+ #### fixed 帯に表示されるボタン
305
+
306
+ fixed 帯には submit セクションのクローンを表示します。
307
+
308
+ - 元のフォーム内ボタンの見た目は変更しない
309
+ - fixed 帯にだけ大きいボタンサイズを適用する
310
+ - 1 つの submit セクションに複数 submit ボタンがある場合は全部表示する
311
+
312
+ #### 自動選択から除外したい場合
313
+
314
+ 自動選択の候補から外したい submit セクションには、`data-fixed-submit-exclude="true"`
315
+ を付けます。
316
+
317
+ ```erb
318
+ <div class="form-actions" data-fixed-submit-exclude="true">
319
+ <%= f.submit "Preview" %>
320
+ </div>
321
+ ```
322
+
271
323
  ### Custom field
272
324
 
273
325
  dashboard から共通 field を利用できます。
@@ -0,0 +1,324 @@
1
+ (function() {
2
+ var ACTION_SELECTOR = ".form-actions, .form_submit";
3
+ var EXPLICIT_ACTION_SELECTOR = '[data-fixed-submit-actions="true"]';
4
+ var SUBMIT_SELECTOR = 'input[type="submit"], button[type="submit"], button.async-form__submit';
5
+ var scheduled = false;
6
+ var bar = null;
7
+ var barActions = null;
8
+ var observedAction = null;
9
+ var observer = null;
10
+
11
+ function toArray(nodeList) {
12
+ return Array.prototype.slice.call(nodeList || []);
13
+ }
14
+
15
+ function matchesSelector(element, selector) {
16
+ if (!element || element.nodeType !== 1) return false;
17
+
18
+ var proto = Element.prototype;
19
+ var matcher = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector;
20
+ return matcher ? matcher.call(element, selector) : false;
21
+ }
22
+
23
+ function closest(element, selector) {
24
+ var current = element;
25
+
26
+ while (current && current.nodeType === 1) {
27
+ if (matchesSelector(current, selector)) return current;
28
+ current = current.parentElement;
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function isVisible(element) {
35
+ if (!element) return false;
36
+
37
+ var styles = window.getComputedStyle(element);
38
+ return styles.display !== "none" &&
39
+ styles.visibility !== "hidden" &&
40
+ element.getClientRects().length > 0;
41
+ }
42
+
43
+ function containsSubmitControl(element) {
44
+ return !!element && !!element.querySelector(SUBMIT_SELECTOR);
45
+ }
46
+
47
+ function isAutoTargetPage() {
48
+ var path = window.location.pathname || "";
49
+
50
+ return path.indexOf("/admin/") === 0 &&
51
+ (path.indexOf("/new") !== -1 || path.indexOf("/edit") !== -1);
52
+ }
53
+
54
+ function isIgnoredAction(action) {
55
+ return action.classList.contains("form-actions--top") ||
56
+ action.getAttribute("data-fixed-submit-exclude") === "true";
57
+ }
58
+
59
+ function explicitInstance() {
60
+ var action = toArray(document.querySelectorAll(".main-content " + EXPLICIT_ACTION_SELECTOR)).find(function(candidate) {
61
+ return containsSubmitControl(candidate) && !isIgnoredAction(candidate);
62
+ });
63
+ var form = closest(action, "form");
64
+
65
+ if (!action || !form || !containsSubmitControl(action) || isIgnoredAction(action)) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ form: form,
71
+ action: action
72
+ };
73
+ }
74
+
75
+ function actionCandidates(form) {
76
+ return toArray(form.querySelectorAll(ACTION_SELECTOR)).filter(function(action) {
77
+ return closest(action, "form") === form &&
78
+ containsSubmitControl(action) &&
79
+ !isIgnoredAction(action);
80
+ });
81
+ }
82
+
83
+ function autoInstances() {
84
+ return toArray(document.querySelectorAll(".main-content form")).map(function(form) {
85
+ var candidates = actionCandidates(form);
86
+ var action = candidates[candidates.length - 1];
87
+
88
+ if (!action) return null;
89
+
90
+ return {
91
+ form: form,
92
+ action: action
93
+ };
94
+ }).filter(Boolean);
95
+ }
96
+
97
+ function anchorPosition() {
98
+ var header = document.querySelector(".main-content__header");
99
+ var headerBottom = header && isVisible(header) ? header.getBoundingClientRect().bottom : 0;
100
+ var viewportAnchor = Math.min(window.innerHeight * 0.35, 320);
101
+
102
+ return Math.min(
103
+ Math.max(headerBottom + 24, viewportAnchor),
104
+ Math.max(window.innerHeight - 48, 0)
105
+ );
106
+ }
107
+
108
+ function visibleInstances(instances) {
109
+ return instances.map(function(instance) {
110
+ instance.formRect = instance.form.getBoundingClientRect();
111
+ instance.actionRect = instance.action.getBoundingClientRect();
112
+ return instance;
113
+ }).filter(function(instance) {
114
+ return isVisible(instance.form) &&
115
+ isVisible(instance.action) &&
116
+ instance.formRect.bottom > 0 &&
117
+ instance.formRect.top < window.innerHeight;
118
+ });
119
+ }
120
+
121
+ function selectAutoInstance(instances) {
122
+ if (!instances.length) return null;
123
+
124
+ var anchorY = anchorPosition();
125
+ var containing = instances.filter(function(instance) {
126
+ return instance.formRect.top <= anchorY && instance.formRect.bottom > anchorY;
127
+ });
128
+
129
+ if (containing.length) {
130
+ return containing.sort(function(left, right) {
131
+ return right.formRect.top - left.formRect.top;
132
+ })[0];
133
+ }
134
+
135
+ var below = instances.filter(function(instance) {
136
+ return instance.formRect.top > anchorY;
137
+ });
138
+
139
+ if (below.length) {
140
+ return below.sort(function(left, right) {
141
+ return left.formRect.top - right.formRect.top;
142
+ })[0];
143
+ }
144
+
145
+ return instances.sort(function(left, right) {
146
+ return right.formRect.bottom - left.formRect.bottom;
147
+ })[0];
148
+ }
149
+
150
+ function currentInstance() {
151
+ var explicit = explicitInstance();
152
+ if (explicit) return explicit;
153
+
154
+ if (!isAutoTargetPage()) return null;
155
+
156
+ return selectAutoInstance(visibleInstances(autoInstances()));
157
+ }
158
+
159
+ function ensureBar() {
160
+ if (bar) return;
161
+
162
+ bar = document.createElement("div");
163
+ bar.className = "admin-fixed-submit-bar";
164
+ bar.hidden = true;
165
+
166
+ barActions = document.createElement("div");
167
+ barActions.className = "admin-fixed-submit-bar__actions";
168
+ bar.appendChild(barActions);
169
+
170
+ document.body.appendChild(bar);
171
+ }
172
+
173
+ function hideBar() {
174
+ ensureBar();
175
+ bar.hidden = true;
176
+ bar.style.removeProperty("--fixed-submit-left");
177
+ bar.style.removeProperty("--fixed-submit-width");
178
+ barActions.innerHTML = "";
179
+ }
180
+
181
+ function stripIds(node) {
182
+ if (!node || node.nodeType !== 1) return;
183
+
184
+ node.removeAttribute("id");
185
+ toArray(node.querySelectorAll("[id]")).forEach(function(child) {
186
+ child.removeAttribute("id");
187
+ });
188
+ }
189
+
190
+ function syncCloneControls(clone, sourceAction) {
191
+ var sourceControls = toArray(sourceAction.querySelectorAll(SUBMIT_SELECTOR));
192
+ var cloneControls = toArray(clone.querySelectorAll(SUBMIT_SELECTOR));
193
+
194
+ cloneControls.forEach(function(cloneControl, index) {
195
+ var sourceControl = sourceControls[index];
196
+
197
+ if (!sourceControl) {
198
+ cloneControl.remove();
199
+ return;
200
+ }
201
+
202
+ if (cloneControl.tagName === "INPUT") {
203
+ cloneControl.type = "button";
204
+ } else if (cloneControl.tagName === "BUTTON") {
205
+ cloneControl.type = "button";
206
+ }
207
+
208
+ cloneControl.disabled = !!sourceControl.disabled;
209
+
210
+ cloneControl.addEventListener("click", function(event) {
211
+ event.preventDefault();
212
+ if (sourceControl.disabled) return;
213
+ sourceControl.click();
214
+ });
215
+ });
216
+ }
217
+
218
+ function renderClone(instance) {
219
+ ensureBar();
220
+ barActions.innerHTML = "";
221
+
222
+ var clone = instance.action.cloneNode(true);
223
+ stripIds(clone);
224
+ clone.removeAttribute("data-fixed-submit-actions");
225
+ clone.classList.add("admin-fixed-submit-bar__action-clone");
226
+ syncCloneControls(clone, instance.action);
227
+
228
+ barActions.appendChild(clone);
229
+ }
230
+
231
+ function fixedFrame(instance) {
232
+ var mainContent = closest(instance.form, ".main-content") || document.querySelector(".main-content");
233
+ var section = closest(instance.action, ".main-content__body") || closest(instance.form, ".main-content__body") || instance.form;
234
+ var viewportMargin = window.innerWidth <= 767 ? 8 : 16;
235
+ var mainRect = mainContent ? mainContent.getBoundingClientRect() : { left: 0, right: window.innerWidth };
236
+ var sectionRect = section.getBoundingClientRect();
237
+ var left = Math.max(mainRect.left, sectionRect.left, viewportMargin);
238
+ var right = Math.min(mainRect.right, sectionRect.right, window.innerWidth - viewportMargin);
239
+
240
+ return {
241
+ left: Math.max(left, viewportMargin),
242
+ width: Math.max(right - left, 0)
243
+ };
244
+ }
245
+
246
+ function shouldPin(instance) {
247
+ var actionRect = instance.action.getBoundingClientRect();
248
+ var revealThreshold = Math.min(96, Math.max(48, actionRect.height));
249
+
250
+ return isVisible(instance.form) &&
251
+ isVisible(instance.action) &&
252
+ instance.form.getBoundingClientRect().bottom > 0 &&
253
+ instance.form.getBoundingClientRect().top < window.innerHeight &&
254
+ actionRect.top > window.innerHeight - revealThreshold;
255
+ }
256
+
257
+ function observeAction(action) {
258
+ if (observedAction === action) return;
259
+
260
+ if (observer) observer.disconnect();
261
+
262
+ observedAction = action;
263
+ if (!action) return;
264
+
265
+ observer = new MutationObserver(function() {
266
+ scheduleSync();
267
+ });
268
+
269
+ observer.observe(action, {
270
+ subtree: true,
271
+ childList: true,
272
+ attributes: true,
273
+ characterData: true
274
+ });
275
+ }
276
+
277
+ function sync() {
278
+ var instance = currentInstance();
279
+
280
+ if (!instance) {
281
+ observeAction(null);
282
+ hideBar();
283
+ return;
284
+ }
285
+
286
+ observeAction(instance.action);
287
+
288
+ if (!shouldPin(instance)) {
289
+ hideBar();
290
+ return;
291
+ }
292
+
293
+ var frame = fixedFrame(instance);
294
+ if (frame.width <= 0) {
295
+ hideBar();
296
+ return;
297
+ }
298
+
299
+ renderClone(instance);
300
+ bar.style.setProperty("--fixed-submit-left", frame.left + "px");
301
+ bar.style.setProperty("--fixed-submit-width", frame.width + "px");
302
+ bar.hidden = false;
303
+ }
304
+
305
+ function scheduleSync() {
306
+ if (scheduled) return;
307
+
308
+ scheduled = true;
309
+ (window.requestAnimationFrame || window.setTimeout)(function() {
310
+ scheduled = false;
311
+ sync();
312
+ }, 16);
313
+ }
314
+
315
+ function initialize() {
316
+ ensureBar();
317
+ scheduleSync();
318
+ }
319
+
320
+ document.addEventListener("DOMContentLoaded", initialize);
321
+ window.addEventListener("load", scheduleSync);
322
+ window.addEventListener("scroll", scheduleSync, { passive: true });
323
+ window.addEventListener("resize", scheduleSync);
324
+ })();
@@ -0,0 +1,45 @@
1
+ .admin-fixed-submit-bar {
2
+ position: fixed;
3
+ left: var(--fixed-submit-left, 0);
4
+ width: var(--fixed-submit-width, 100%);
5
+ bottom: 0;
6
+ z-index: 20;
7
+ padding: 0.9rem 1rem calc(0.9rem + env(safe-area-inset-bottom));
8
+ background: #fff;
9
+ border-top: 1px solid #d9dde8;
10
+ box-sizing: border-box;
11
+
12
+ @media screen and (max-width: 767px) {
13
+ padding-left: 0.8rem;
14
+ padding-right: 0.8rem;
15
+ }
16
+ }
17
+
18
+ .admin-fixed-submit-bar[hidden] {
19
+ display: none !important;
20
+ }
21
+
22
+ .admin-fixed-submit-bar__actions,
23
+ .admin-fixed-submit-bar__action-clone {
24
+ display: flex;
25
+ justify-content: flex-end;
26
+ align-items: center;
27
+ gap: 0.75rem;
28
+ }
29
+
30
+ .admin-fixed-submit-bar__action-clone,
31
+ .admin-fixed-submit-bar__action-clone.form-actions,
32
+ .admin-fixed-submit-bar__action-clone.form_submit {
33
+ margin: 0;
34
+ }
35
+
36
+ .admin-fixed-submit-bar__actions input[type="submit"],
37
+ .admin-fixed-submit-bar__actions input[type="button"],
38
+ .admin-fixed-submit-bar__actions button[type="submit"],
39
+ .admin-fixed-submit-bar__actions button[type="button"],
40
+ .admin-fixed-submit-bar__actions .async-form__submit {
41
+ min-width: 20rem;
42
+ min-height: 3.5rem;
43
+ font-size: 1.5em;
44
+ font-weight: 600;
45
+ }
@@ -1,3 +1,5 @@
1
+ @import "fixed_submit_actions";
2
+
1
3
  .scroll-table {
2
4
  overflow-x: auto;
3
5
  }
@@ -9,6 +9,7 @@ module YummyGuide
9
9
  app.config.assets.precompile += %w[
10
10
  yummy_guide_administrate/components.css
11
11
  yummy_guide_administrate/clipboards.js
12
+ yummy_guide_administrate/fixed_submit_actions.js
12
13
  yummy_guide_administrate/filter_form.js
13
14
  yummy_guide_administrate/sticky_left_columns.js
14
15
  yummy_guide_administrate/sticky_table_headers.js
@@ -2,6 +2,6 @@
2
2
 
3
3
  module YummyGuide
4
4
  module Administrate
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yummy-guide-generic-administrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - akatsuki-kk
@@ -92,8 +92,10 @@ files:
92
92
  - app/assets/images/yummy_guide_administrate/icon-copy.svg
93
93
  - app/assets/javascripts/yummy_guide_administrate/clipboards.js
94
94
  - app/assets/javascripts/yummy_guide_administrate/filter_form.js
95
+ - app/assets/javascripts/yummy_guide_administrate/fixed_submit_actions.js
95
96
  - app/assets/javascripts/yummy_guide_administrate/sticky_left_columns.js
96
97
  - app/assets/javascripts/yummy_guide_administrate/sticky_table_headers.js
98
+ - app/assets/stylesheets/yummy_guide_administrate/_fixed_submit_actions.scss
97
99
  - app/assets/stylesheets/yummy_guide_administrate/components.scss
98
100
  - app/controllers/concerns/yummy_guide/administrate/datetime_filter_parameters.rb
99
101
  - app/controllers/concerns/yummy_guide/administrate/default_sorting.rb