yummy-guide-generic-administrate 0.8.3 → 0.8.5

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: a6e8b69f0347b22f47b72cf96c4f99c0c398d58a8f118809735f7bafaee65e20
4
- data.tar.gz: 37decc40a9672713935cd76fdea50ff20992e6e35298a8096c1ab7625209060b
3
+ metadata.gz: 3940f84fd25e7fe60419f17273656e50af22465caf4bbfa29e1ac209d03b5bd4
4
+ data.tar.gz: 3bc3455f747754d483ee5329c5d4991289eaac0630961a808558f551e7f9c819
5
5
  SHA512:
6
- metadata.gz: b3d6976869de3d91b3affb029c77195053e541513f5f8ebbe4279543dfea132cf2536ec0880d555e2112ea63178ac61140eea4bf43fb3b365f58951e9335aae9
7
- data.tar.gz: 81db24d807a65056e6b9ce35c5f5240d30c370b11def34fb2ae6664c7404e850458cd9a6119de8602faf259fcbfe0d42872da16373a0154cdc81c28b0c5da496
6
+ metadata.gz: b9843e5fdc03dd2e08330368cb267ac888fa1d5327bad40550b44a0960881d6d4cd87b77a0e2cad10f70d3d04f503d88b39192f20f82e4e3adc04910e5af3a7e
7
+ data.tar.gz: ba1be8f0226de1a9bd37650827529ea7b5c59d2d680f4d31066f86246fcb3085a87b9a5c1b6c91ccce5d7ffd0360e98d5f2bc281a07426f07987edef714a1503
data/README.md CHANGED
@@ -43,6 +43,8 @@ bundle install
43
43
  - datetime フィルターや checkbox group の組み立てを補助する helper
44
44
  - `YummyGuide::Administrate::FilterControlsHelper`
45
45
  - dashboard の Field 型フィルター定義から Filter ボタンとモーダルフォームを描画する helper
46
+ - `YummyGuide::Administrate::TooltipHelper`
47
+ - 管理画面内で補足説明 tooltip を描画する helper
46
48
  - `YummyGuide::Administrate::DatetimeInputHelper`
47
49
  - 管理画面フォーム用の date + time 入力 helper
48
50
  - `YummyGuide::Administrate::NumberInputHelper`
@@ -58,6 +60,7 @@ bundle install
58
60
  - `filter_form.js`
59
61
  - `sticky_left_columns.js`
60
62
  - `sticky_table_headers.js`
63
+ - `tooltips.js`
61
64
  - `components.css`
62
65
  - 共通 field
63
66
  - `YummyGuide::Administrate::Fields::JsonPrettyField`
@@ -109,12 +112,15 @@ class Admin::ApplicationController < Administrate::ApplicationController
109
112
  helper YummyGuide::Administrate::FilterControlsHelper
110
113
  helper YummyGuide::Administrate::FilterFormHelper
111
114
  helper YummyGuide::Administrate::NumberInputHelper
115
+ helper YummyGuide::Administrate::TooltipHelper
112
116
  end
113
117
  ```
114
118
 
115
119
  `NumberInputHelper` は `Administrate::ApplicationController` の view helper として
116
120
  自動適用されます。`Administrate::ApplicationController` を継承しない独自 admin
117
121
  controller で同じ挙動が必要な場合だけ、上記のように明示的に読み込んでください。
122
+ `TooltipHelper` も `Administrate::ApplicationController` の view helper として
123
+ 自動適用されます。
118
124
 
119
125
  ### Collection partial
120
126
 
@@ -461,6 +467,30 @@ Admin/Administrate 画面では、`number_field` / `number_field_tag` を
461
467
  `range_field` / `range_field_tag` は対象外で、従来どおり `type="range"` として描画
462
468
  されます。raw HTML の `<input type="number">` は helper を通らないため対象外です。
463
469
 
470
+ ### Tooltip helper
471
+
472
+ ラベルやボタンの横に補足説明用の tooltip アイコンを表示する場合は、
473
+ `admin_tooltip` を使います。PC では hover / focus 時に表示し、モバイルではタップで
474
+ 表示・非表示を切り替えます。
475
+
476
+ ```erb
477
+ <%= f.label :published_at %>
478
+ <%= admin_tooltip("公開日時を過ぎると公開ページに表示されます。") %>
479
+ ```
480
+
481
+ 説明本文に改行などの HTML を含める場合は block を渡します。`text` 引数と block を
482
+ 同時に指定した場合は block が優先されます。
483
+
484
+ ```erb
485
+ <%= admin_tooltip do %>
486
+ Add - 調整レコードを追加表示するようにします。<br>
487
+ Only - 調整レコードのみを表示します。
488
+ <% end %>
489
+ ```
490
+
491
+ block を使わない場合、tooltip 本文はテキストとして扱われ、HTML は描画しません。
492
+ 見た目と表示制御には `components.css` と `tooltips.js` の読み込みが必要です。
493
+
464
494
  ### Asset の読み込み
465
495
 
466
496
  この engine の asset はホストアプリ側で明示的に読み込んでください。
@@ -473,6 +503,7 @@ Admin/Administrate 画面では、`number_field` / `number_field_tag` を
473
503
  //= require yummy_guide_administrate/filter_form
474
504
  //= require yummy_guide_administrate/sticky_left_columns
475
505
  //= require yummy_guide_administrate/sticky_table_headers
506
+ //= require yummy_guide_administrate/tooltips
476
507
  ```
477
508
 
478
509
  ```scss
@@ -71,6 +71,11 @@
71
71
  return preciseNumber(value) + 'px';
72
72
  }
73
73
 
74
+ function parsedPixelValue(value) {
75
+ var parsedValue = parseFloat(value || '0');
76
+ return Number.isNaN(parsedValue) ? 0 : preciseNumber(parsedValue);
77
+ }
78
+
74
79
  function measuredWidth(element) {
75
80
  if (!element) return 0;
76
81
 
@@ -307,6 +312,7 @@
307
312
  indexByColumnId: indexByColumnId
308
313
  };
309
314
  tableStates.set(table, state);
315
+ refreshCssStickyLeftColumns(table);
310
316
 
311
317
  return state;
312
318
  }
@@ -375,6 +381,7 @@
375
381
  table.style.setProperty(columnWidthVariable(index), widthValue);
376
382
  setAdjustedColumn(table, index, true);
377
383
  applyColgroupWidth(table, state.columnCount, index, widthValue);
384
+ refreshCssStickyLeftColumns(table);
378
385
  }
379
386
 
380
387
  function clearTableColumnWidth(table, columnId) {
@@ -385,6 +392,65 @@
385
392
  table.style.removeProperty(columnWidthVariable(index));
386
393
  setAdjustedColumn(table, index, false);
387
394
  clearColgroupWidth(table, state.columnCount, index);
395
+ refreshCssStickyLeftColumns(table);
396
+ }
397
+
398
+ function stickyColumnIndexes(table, className) {
399
+ var row = table && table.querySelector('thead tr');
400
+ if (!row) return [];
401
+
402
+ return directCells(row).map(function(cell, index) {
403
+ return cell.classList && cell.classList.contains(className) ? index : null;
404
+ }).filter(function(index) {
405
+ return index !== null;
406
+ });
407
+ }
408
+
409
+ function managedColumnWidth(table, index) {
410
+ var colgroup = table && table.querySelector('colgroup[data-admin-column-resizer-colgroup], colgroup[data-fixed-header-colgroup]');
411
+ var col = colgroup && colgroup.children[index];
412
+
413
+ return col ? parsedPixelValue(col.style.width) : 0;
414
+ }
415
+
416
+ function stickyColumnWidth(table, header, index, widthVariable) {
417
+ return measuredWidth(header) ||
418
+ managedColumnWidth(table, index) ||
419
+ parsedPixelValue(header && header.style && header.style.getPropertyValue(widthVariable));
420
+ }
421
+
422
+ function applyStickyColumnPosition(table, index, leftVariable, left, widthVariable, width) {
423
+ Array.from(table.querySelectorAll('thead tr, tbody tr, tfoot tr')).forEach(function(row) {
424
+ var cell = directCells(row)[index];
425
+ if (!cell) return;
426
+
427
+ cell.style.setProperty(leftVariable, cssPixelValue(left));
428
+ if (width) {
429
+ cell.style.setProperty(widthVariable, cssPixelValue(width));
430
+ }
431
+ });
432
+ }
433
+
434
+ function refreshCssStickyLeftColumnSet(table, className, leftVariable, widthVariable) {
435
+ var headerRow = table && table.querySelector('thead tr');
436
+ if (!headerRow) return;
437
+
438
+ var headerCells = directCells(headerRow);
439
+ var left = 0;
440
+
441
+ stickyColumnIndexes(table, className).forEach(function(index) {
442
+ var width = stickyColumnWidth(table, headerCells[index], index, widthVariable);
443
+
444
+ applyStickyColumnPosition(table, index, leftVariable, left, widthVariable, width);
445
+ left += width;
446
+ });
447
+ }
448
+
449
+ function refreshCssStickyLeftColumns(table) {
450
+ if (!table || table.getAttribute('aria-hidden') === 'true' || table.classList.contains(FIXED_HEADER_TABLE_CLASS)) return;
451
+
452
+ refreshCssStickyLeftColumnSet(table, 'sticky-left', '--sticky-left', '--sticky-width');
453
+ refreshCssStickyLeftColumnSet(table, 'sticky-left-mobile', '--sticky-mobile-left', '--sticky-mobile-width');
388
454
  }
389
455
 
390
456
  function applyColumnWidth(columnId, width, key) {
@@ -0,0 +1,214 @@
1
+ (function() {
2
+ var TRIGGER_SELECTOR = "[data-admin-tooltip-trigger='true']";
3
+ var TOOLTIP_ID = "admin-tooltip";
4
+ var TOOLTIP_CLASS = "admin-tooltip";
5
+ var VISIBLE_CLASS = "admin-tooltip--visible";
6
+ var MOBILE_MEDIA_QUERY = "(max-width: 767px)";
7
+ var EDGE_GAP = 8;
8
+ var OFFSET = 8;
9
+ var activeTrigger = null;
10
+ var hoveredTrigger = null;
11
+ var focusedTrigger = null;
12
+ var tooltipElement = null;
13
+
14
+ function isMobile() {
15
+ return window.matchMedia && window.matchMedia(MOBILE_MEDIA_QUERY).matches;
16
+ }
17
+
18
+ function closestTrigger(target) {
19
+ if (!target || !target.closest) return null;
20
+
21
+ return target.closest(TRIGGER_SELECTOR);
22
+ }
23
+
24
+ function clamp(value, min, max) {
25
+ return Math.min(Math.max(value, min), max);
26
+ }
27
+
28
+ function getTooltipElement() {
29
+ if (tooltipElement) return tooltipElement;
30
+
31
+ tooltipElement = document.createElement("div");
32
+ tooltipElement.id = TOOLTIP_ID;
33
+ tooltipElement.className = TOOLTIP_CLASS;
34
+ tooltipElement.setAttribute("role", "tooltip");
35
+ document.body.appendChild(tooltipElement);
36
+
37
+ return tooltipElement;
38
+ }
39
+
40
+ function tooltipContent(trigger) {
41
+ var contentId = trigger.getAttribute("data-admin-tooltip-content-id");
42
+ var template = contentId ? document.getElementById(contentId) : null;
43
+ if (template) {
44
+ return {
45
+ html: template.innerHTML,
46
+ text: ""
47
+ };
48
+ }
49
+
50
+ var text = trigger.getAttribute("data-admin-tooltip-text") || "";
51
+ return {
52
+ html: "",
53
+ text: text.trim() ? text : ""
54
+ };
55
+ }
56
+
57
+ function setTriggerState(trigger, expanded) {
58
+ if (!trigger) return;
59
+
60
+ trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
61
+
62
+ if (expanded) {
63
+ trigger.setAttribute("aria-describedby", TOOLTIP_ID);
64
+ } else {
65
+ trigger.removeAttribute("aria-describedby");
66
+ }
67
+ }
68
+
69
+ function positionTooltip() {
70
+ if (!activeTrigger || !tooltipElement || !tooltipElement.classList.contains(VISIBLE_CLASS)) return;
71
+
72
+ var targetRect = activeTrigger.getBoundingClientRect();
73
+ tooltipElement.style.left = "0px";
74
+ tooltipElement.style.top = "0px";
75
+
76
+ var tooltipRect = tooltipElement.getBoundingClientRect();
77
+ var viewportWidth = document.documentElement.clientWidth || window.innerWidth;
78
+ var viewportHeight = document.documentElement.clientHeight || window.innerHeight;
79
+ var top = targetRect.top - tooltipRect.height - OFFSET;
80
+ var placement = "top";
81
+
82
+ if (top < EDGE_GAP) {
83
+ top = targetRect.bottom + OFFSET;
84
+ placement = "bottom";
85
+ }
86
+
87
+ top = clamp(top, EDGE_GAP, Math.max(EDGE_GAP, viewportHeight - tooltipRect.height - EDGE_GAP));
88
+
89
+ var left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
90
+ left = clamp(left, EDGE_GAP, Math.max(EDGE_GAP, viewportWidth - tooltipRect.width - EDGE_GAP));
91
+
92
+ var arrowMax = Math.max(10, tooltipRect.width - 10);
93
+ var arrowLeft = clamp(targetRect.left + (targetRect.width / 2) - left - 4, 10, arrowMax);
94
+
95
+ tooltipElement.style.left = Math.round(left) + "px";
96
+ tooltipElement.style.top = Math.round(top) + "px";
97
+ tooltipElement.style.setProperty("--admin-tooltip-arrow-left", Math.round(arrowLeft) + "px");
98
+ tooltipElement.setAttribute("data-admin-tooltip-placement", placement);
99
+ }
100
+
101
+ function showTooltip(trigger) {
102
+ var content = tooltipContent(trigger);
103
+ if (!content.html && !content.text) return;
104
+
105
+ if (activeTrigger && activeTrigger !== trigger) {
106
+ hideTooltip();
107
+ }
108
+
109
+ activeTrigger = trigger;
110
+ tooltipElement = getTooltipElement();
111
+ if (content.html) {
112
+ tooltipElement.innerHTML = content.html;
113
+ } else {
114
+ tooltipElement.textContent = content.text;
115
+ }
116
+ tooltipElement.classList.add(VISIBLE_CLASS);
117
+ setTriggerState(trigger, true);
118
+
119
+ if (window.requestAnimationFrame) {
120
+ window.requestAnimationFrame(positionTooltip);
121
+ } else {
122
+ positionTooltip();
123
+ }
124
+ }
125
+
126
+ function hideTooltip(trigger) {
127
+ if (trigger && activeTrigger !== trigger) return;
128
+
129
+ setTriggerState(activeTrigger, false);
130
+
131
+ if (tooltipElement) {
132
+ tooltipElement.classList.remove(VISIBLE_CLASS);
133
+ }
134
+
135
+ activeTrigger = null;
136
+ }
137
+
138
+ function toggleTooltip(trigger) {
139
+ if (activeTrigger === trigger && tooltipElement && tooltipElement.classList.contains(VISIBLE_CLASS)) {
140
+ hideTooltip(trigger);
141
+ return;
142
+ }
143
+
144
+ showTooltip(trigger);
145
+ }
146
+
147
+ document.addEventListener("mouseover", function(event) {
148
+ var trigger = closestTrigger(event.target);
149
+ if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
150
+
151
+ hoveredTrigger = trigger;
152
+ showTooltip(trigger);
153
+ });
154
+
155
+ document.addEventListener("mouseout", function(event) {
156
+ var trigger = closestTrigger(event.target);
157
+ if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
158
+
159
+ if (hoveredTrigger === trigger) {
160
+ hoveredTrigger = null;
161
+ }
162
+ if (focusedTrigger === trigger) return;
163
+
164
+ hideTooltip(trigger);
165
+ });
166
+
167
+ document.addEventListener("focusin", function(event) {
168
+ var trigger = closestTrigger(event.target);
169
+ if (!trigger || isMobile()) return;
170
+
171
+ focusedTrigger = trigger;
172
+ showTooltip(trigger);
173
+ });
174
+
175
+ document.addEventListener("focusout", function(event) {
176
+ var trigger = closestTrigger(event.target);
177
+ if (!trigger || isMobile()) return;
178
+
179
+ if (focusedTrigger === trigger) {
180
+ focusedTrigger = null;
181
+ }
182
+ if (hoveredTrigger === trigger) return;
183
+
184
+ hideTooltip(trigger);
185
+ });
186
+
187
+ document.addEventListener("click", function(event) {
188
+ var trigger = closestTrigger(event.target);
189
+
190
+ if (trigger) {
191
+ if (!isMobile()) return;
192
+
193
+ event.preventDefault();
194
+ event.stopPropagation();
195
+ toggleTooltip(trigger);
196
+ return;
197
+ }
198
+
199
+ if (activeTrigger && isMobile()) {
200
+ hideTooltip();
201
+ }
202
+ });
203
+
204
+ document.addEventListener("keydown", function(event) {
205
+ if (event.key !== "Escape") return;
206
+
207
+ hideTooltip();
208
+ });
209
+
210
+ window.addEventListener("scroll", positionTooltip, true);
211
+ window.addEventListener("resize", function() {
212
+ hideTooltip();
213
+ });
214
+ })();
@@ -0,0 +1,85 @@
1
+ .admin-tooltip-trigger {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 1.25rem;
6
+ min-width: 1.25rem;
7
+ height: 1.25rem;
8
+ margin: 0 0 0 0.25rem;
9
+ padding: 0;
10
+ border: 1px solid #7a8594;
11
+ border-radius: 50%;
12
+ background: #fff;
13
+ color: #4b5563;
14
+ cursor: help;
15
+ font-size: 0.75rem;
16
+ font-weight: 700;
17
+ line-height: 1;
18
+ vertical-align: middle;
19
+ }
20
+
21
+ .admin-tooltip-trigger:hover,
22
+ .admin-tooltip-trigger:focus-visible,
23
+ .admin-tooltip-trigger[aria-expanded="true"] {
24
+ border-color: #0d6efd;
25
+ background: #f8fbff;
26
+ color: #0d6efd;
27
+ box-shadow: none;
28
+ }
29
+
30
+ .admin-tooltip-trigger__icon {
31
+ pointer-events: none;
32
+ }
33
+
34
+ .admin-tooltip {
35
+ position: fixed;
36
+ z-index: 1100;
37
+ max-width: 280px;
38
+ padding: 8px 10px;
39
+ border-radius: 4px;
40
+ background: #1f2933;
41
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
42
+ color: #fff;
43
+ font-size: 0.82rem;
44
+ line-height: 1.45;
45
+ opacity: 0;
46
+ overflow-wrap: anywhere;
47
+ pointer-events: none;
48
+ transform: translateY(2px);
49
+ transition: opacity 0.12s ease, transform 0.12s ease;
50
+ white-space: normal;
51
+ }
52
+
53
+ .admin-tooltip--visible {
54
+ opacity: 1;
55
+ transform: translateY(0);
56
+ }
57
+
58
+ .admin-tooltip::before {
59
+ content: "";
60
+ position: absolute;
61
+ left: var(--admin-tooltip-arrow-left, 50%);
62
+ width: 8px;
63
+ height: 8px;
64
+ background: #1f2933;
65
+ transform: rotate(45deg);
66
+ }
67
+
68
+ .admin-tooltip[data-admin-tooltip-placement="top"]::before {
69
+ bottom: -4px;
70
+ }
71
+
72
+ .admin-tooltip[data-admin-tooltip-placement="bottom"]::before {
73
+ top: -4px;
74
+ }
75
+
76
+ @media screen and (max-width: 767px) {
77
+ .admin-tooltip-trigger {
78
+ cursor: pointer;
79
+ }
80
+
81
+ .admin-tooltip {
82
+ max-width: calc(100vw - 24px);
83
+ font-size: 0.85rem;
84
+ }
85
+ }