stimeo-ui 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +178 -0
- data/dist/controllers/alert_dialog_controller.js +75 -4
- data/dist/controllers/aspect_ratio_controller.js +19 -11
- data/dist/controllers/avatar_controller.js +237 -40
- data/dist/controllers/carousel_controller.js +85 -9
- data/dist/controllers/character_counter_controller.js +338 -63
- data/dist/controllers/checkbox_controller.js +136 -25
- data/dist/controllers/color_picker_controller.js +35 -9
- data/dist/controllers/command_palette_controller.js +75 -4
- data/dist/controllers/conditional_fields_controller.js +345 -51
- data/dist/controllers/confirm_controller.js +75 -4
- data/dist/controllers/date_range_picker_controller.js +157 -30
- data/dist/controllers/dialog_controller.js +75 -4
- data/dist/controllers/direct_upload_controller.js +201 -45
- data/dist/controllers/dirty_form_controller.js +192 -29
- data/dist/controllers/dismissible_controller.js +83 -18
- data/dist/controllers/drawer_controller.js +75 -4
- data/dist/controllers/file_dropzone_controller.js +26 -3
- data/dist/controllers/focus_controller.js +75 -4
- data/dist/controllers/form_field_controller.js +280 -62
- data/dist/controllers/form_validation_controller.js +208 -83
- data/dist/controllers/idle_controller.js +27 -5
- data/dist/controllers/menubar_controller.js +5 -3
- data/dist/controllers/multi_select_controller.js +460 -151
- data/dist/controllers/number_input_controller.js +317 -51
- data/dist/controllers/overflow_menu_controller.js +6 -1
- data/dist/controllers/pagination_controller.js +35 -1
- data/dist/controllers/password_strength_controller.js +20 -2
- data/dist/controllers/persist_controller.js +449 -120
- data/dist/controllers/popover_controller.js +77 -4
- data/dist/controllers/radio_group_controller.js +540 -56
- data/dist/controllers/rating_controller.js +276 -89
- data/dist/controllers/resizable_controller.js +33 -0
- data/dist/controllers/roving_controller.js +60 -5
- data/dist/controllers/scroll_area_controller.js +557 -125
- data/dist/controllers/scroll_visibility_controller.js +33 -0
- data/dist/controllers/separator_controller.js +354 -38
- data/dist/controllers/sidebar_controller.js +83 -10
- data/dist/controllers/submit_once_controller.js +399 -121
- data/dist/controllers/tags_input_controller.js +356 -120
- data/dist/controllers/theme_controller.js +8 -6
- data/dist/controllers/time_picker_controller.js +296 -107
- data/dist/controllers/toggle_group_controller.js +378 -55
- data/dist/controllers/toolbar_controller.js +5 -3
- data/dist/controllers/tree_view_controller.js +7 -4
- data/dist/index.js +4963 -1581
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -2,8 +2,79 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/dismissible_controller.ts
|
|
4
4
|
|
|
5
|
-
// src/utils/
|
|
6
|
-
|
|
5
|
+
// src/utils/focus_candidate.ts
|
|
6
|
+
function inheritsFieldsetDisabled(control) {
|
|
7
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
8
|
+
while (fieldset) {
|
|
9
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
10
|
+
if (!legend?.contains(control)) return true;
|
|
11
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function canTakeFocus(element) {
|
|
16
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
17
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
18
|
+
if (!("disabled" in element)) return true;
|
|
19
|
+
if (element.disabled) return false;
|
|
20
|
+
return !inheritsFieldsetDisabled(element);
|
|
21
|
+
}
|
|
22
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
23
|
+
"a[href]",
|
|
24
|
+
"area[href]",
|
|
25
|
+
"button",
|
|
26
|
+
"input",
|
|
27
|
+
"select",
|
|
28
|
+
"textarea",
|
|
29
|
+
"summary",
|
|
30
|
+
"iframe",
|
|
31
|
+
"audio[controls]",
|
|
32
|
+
"video[controls]",
|
|
33
|
+
"[tabindex]",
|
|
34
|
+
"[contenteditable]"
|
|
35
|
+
].join(",");
|
|
36
|
+
function isRenderedForFocus(element) {
|
|
37
|
+
const check = element.checkVisibility;
|
|
38
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
39
|
+
}
|
|
40
|
+
function authoredTabindex(element) {
|
|
41
|
+
const value = element.getAttribute("tabindex");
|
|
42
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
43
|
+
return Number(value);
|
|
44
|
+
}
|
|
45
|
+
function hasNativeTabStop(element) {
|
|
46
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
47
|
+
return element.hasAttribute("href");
|
|
48
|
+
}
|
|
49
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
53
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
54
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
55
|
+
return element.hasAttribute("controls");
|
|
56
|
+
}
|
|
57
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
58
|
+
const details = element.parentElement;
|
|
59
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function hasEditableTabStop(element) {
|
|
64
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
65
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
66
|
+
}
|
|
67
|
+
function isTabStop(element) {
|
|
68
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
69
|
+
const tabindex = authoredTabindex(element);
|
|
70
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
71
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
72
|
+
}
|
|
73
|
+
function tabStopsWithin(root) {
|
|
74
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
75
|
+
isTabStop
|
|
76
|
+
);
|
|
77
|
+
}
|
|
7
78
|
|
|
8
79
|
// src/controllers/dismissible_controller.ts
|
|
9
80
|
var DismissibleController = class extends Controller {
|
|
@@ -80,35 +151,29 @@ var DismissibleController = class extends Controller {
|
|
|
80
151
|
/** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
|
|
81
152
|
*#focusFallbacks(root) {
|
|
82
153
|
const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
|
|
83
|
-
if (explicitFallback && !root.contains(explicitFallback) && this.#
|
|
154
|
+
if (explicitFallback && !root.contains(explicitFallback) && this.#isExplicitFocusAvailable(explicitFallback)) {
|
|
84
155
|
yield explicitFallback;
|
|
85
156
|
}
|
|
86
|
-
const candidates = document
|
|
157
|
+
const candidates = tabStopsWithin(document);
|
|
87
158
|
for (const element of candidates) {
|
|
88
159
|
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
89
160
|
continue;
|
|
90
161
|
}
|
|
91
|
-
|
|
162
|
+
yield element;
|
|
92
163
|
}
|
|
93
164
|
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
94
|
-
const element = candidates
|
|
165
|
+
const element = candidates[index];
|
|
166
|
+
if (!element) continue;
|
|
95
167
|
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)) {
|
|
96
168
|
continue;
|
|
97
169
|
}
|
|
98
|
-
|
|
170
|
+
yield element;
|
|
99
171
|
}
|
|
100
172
|
}
|
|
101
|
-
/** Whether
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
if (element.matches(":disabled")) return false;
|
|
107
|
-
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
108
|
-
for (let current = element; current; current = current.parentElement) {
|
|
109
|
-
if (current.hidden || current.inert) return false;
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
173
|
+
/** Whether an explicit fallback can receive programmatic focus right now. */
|
|
174
|
+
#isExplicitFocusAvailable(element) {
|
|
175
|
+
const programmaticallyFocusable = element.hasAttribute("tabindex") || isTabStop(element);
|
|
176
|
+
return programmaticallyFocusable && canTakeFocus(element) && isRenderedForFocus(element);
|
|
112
177
|
}
|
|
113
178
|
};
|
|
114
179
|
|
|
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// src/utils/focus_candidate.ts
|
|
119
|
+
function inheritsFieldsetDisabled(control) {
|
|
120
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
121
|
+
while (fieldset) {
|
|
122
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
123
|
+
if (!legend?.contains(control)) return true;
|
|
124
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function canTakeFocus(element) {
|
|
129
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
130
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
|
+
if (!("disabled" in element)) return true;
|
|
132
|
+
if (element.disabled) return false;
|
|
133
|
+
return !inheritsFieldsetDisabled(element);
|
|
134
|
+
}
|
|
135
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
136
|
+
"a[href]",
|
|
137
|
+
"area[href]",
|
|
138
|
+
"button",
|
|
139
|
+
"input",
|
|
140
|
+
"select",
|
|
141
|
+
"textarea",
|
|
142
|
+
"summary",
|
|
143
|
+
"iframe",
|
|
144
|
+
"audio[controls]",
|
|
145
|
+
"video[controls]",
|
|
146
|
+
"[tabindex]",
|
|
147
|
+
"[contenteditable]"
|
|
148
|
+
].join(",");
|
|
149
|
+
function isRenderedForFocus(element) {
|
|
150
|
+
const check = element.checkVisibility;
|
|
151
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
152
|
+
}
|
|
153
|
+
function authoredTabindex(element) {
|
|
154
|
+
const value = element.getAttribute("tabindex");
|
|
155
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
156
|
+
return Number(value);
|
|
157
|
+
}
|
|
158
|
+
function hasNativeTabStop(element) {
|
|
159
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
160
|
+
return element.hasAttribute("href");
|
|
161
|
+
}
|
|
162
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
166
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
167
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
168
|
+
return element.hasAttribute("controls");
|
|
169
|
+
}
|
|
170
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
171
|
+
const details = element.parentElement;
|
|
172
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasEditableTabStop(element) {
|
|
177
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
178
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
179
|
+
}
|
|
180
|
+
function isTabStop(element) {
|
|
181
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
182
|
+
const tabindex = authoredTabindex(element);
|
|
183
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
184
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
|
+
}
|
|
186
|
+
function tabStopsWithin(root) {
|
|
187
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
188
|
+
isTabStop
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
// src/utils/focus_trap.ts
|
|
119
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
120
193
|
var FocusTrap = class {
|
|
121
194
|
/** The element focused before activation, restored on deactivation. */
|
|
122
195
|
#previouslyFocused = null;
|
|
@@ -278,9 +351,7 @@ var FocusTrap = class {
|
|
|
278
351
|
}
|
|
279
352
|
/** Collects the container's currently focusable descendants in DOM order. */
|
|
280
353
|
#focusableElements() {
|
|
281
|
-
return
|
|
282
|
-
(el) => !el.hidden
|
|
283
|
-
);
|
|
354
|
+
return tabStopsWithin(this.#getContainer());
|
|
284
355
|
}
|
|
285
356
|
};
|
|
286
357
|
|
|
@@ -12,18 +12,38 @@ var FileDropzoneController = class extends Controller {
|
|
|
12
12
|
static events = ["change", "reject"];
|
|
13
13
|
/** Selected files paired with their rendered item and any preview objectURL. */
|
|
14
14
|
#entries = [];
|
|
15
|
+
/** Prevents initial and teardown target callbacks from binding outside controller lifetime. */
|
|
16
|
+
#connected = false;
|
|
15
17
|
/** Wires file removal as a delegated listener on the list container. */
|
|
16
18
|
connect() {
|
|
17
|
-
|
|
19
|
+
this.#connected = true;
|
|
20
|
+
if (this.hasListTarget) this.#bindList(this.listTarget);
|
|
18
21
|
}
|
|
19
22
|
/** Revokes any outstanding preview URLs so none leaks across navigations. */
|
|
20
23
|
disconnect() {
|
|
21
|
-
|
|
24
|
+
this.#connected = false;
|
|
25
|
+
for (const list of this.listTargets) list.removeEventListener("click", this.#onItemClick);
|
|
22
26
|
for (const entry of this.#entries) {
|
|
23
27
|
if (entry.url) URL.revokeObjectURL(entry.url);
|
|
24
28
|
}
|
|
25
29
|
this.#entries.length = 0;
|
|
26
30
|
}
|
|
31
|
+
/** Rebinds removal and restores client-only previews when Turbo replaces the list target. */
|
|
32
|
+
listTargetConnected(list) {
|
|
33
|
+
if (!this.#connected) return;
|
|
34
|
+
this.#bindList(list);
|
|
35
|
+
}
|
|
36
|
+
/** Releases only the list target that actually disconnected. */
|
|
37
|
+
listTargetDisconnected(list) {
|
|
38
|
+
list.removeEventListener("click", this.#onItemClick);
|
|
39
|
+
}
|
|
40
|
+
/** Binds delegated removal once and moves live preview items into the current list. */
|
|
41
|
+
#bindList(list) {
|
|
42
|
+
list.addEventListener("click", this.#onItemClick);
|
|
43
|
+
for (const entry of this.#entries) {
|
|
44
|
+
if (!list.contains(entry.item)) list.appendChild(entry.item);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
27
47
|
/** Opens the native file dialog. Bound via `data-action` (trigger click). */
|
|
28
48
|
openDialog() {
|
|
29
49
|
this.inputTarget.click();
|
|
@@ -55,8 +75,11 @@ var FileDropzoneController = class extends Controller {
|
|
|
55
75
|
* an item is appended without waiting on Stimulus to wire a freshly created element.
|
|
56
76
|
*/
|
|
57
77
|
#onItemClick = (event) => {
|
|
78
|
+
const list = event.currentTarget;
|
|
58
79
|
const button = event.target.closest("button");
|
|
59
|
-
if (!button || !this.hasListTarget ||
|
|
80
|
+
if (!this.#connected || !list || !button || !this.hasListTarget || list !== this.listTarget || !list.contains(button)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
60
83
|
const index = this.#entries.findIndex((entry) => entry.item.contains(button));
|
|
61
84
|
if (index !== -1) this.#removeAt(index);
|
|
62
85
|
};
|
|
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// src/utils/focus_candidate.ts
|
|
119
|
+
function inheritsFieldsetDisabled(control) {
|
|
120
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
121
|
+
while (fieldset) {
|
|
122
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
123
|
+
if (!legend?.contains(control)) return true;
|
|
124
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function canTakeFocus(element) {
|
|
129
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
130
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
|
+
if (!("disabled" in element)) return true;
|
|
132
|
+
if (element.disabled) return false;
|
|
133
|
+
return !inheritsFieldsetDisabled(element);
|
|
134
|
+
}
|
|
135
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
136
|
+
"a[href]",
|
|
137
|
+
"area[href]",
|
|
138
|
+
"button",
|
|
139
|
+
"input",
|
|
140
|
+
"select",
|
|
141
|
+
"textarea",
|
|
142
|
+
"summary",
|
|
143
|
+
"iframe",
|
|
144
|
+
"audio[controls]",
|
|
145
|
+
"video[controls]",
|
|
146
|
+
"[tabindex]",
|
|
147
|
+
"[contenteditable]"
|
|
148
|
+
].join(",");
|
|
149
|
+
function isRenderedForFocus(element) {
|
|
150
|
+
const check = element.checkVisibility;
|
|
151
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
152
|
+
}
|
|
153
|
+
function authoredTabindex(element) {
|
|
154
|
+
const value = element.getAttribute("tabindex");
|
|
155
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
156
|
+
return Number(value);
|
|
157
|
+
}
|
|
158
|
+
function hasNativeTabStop(element) {
|
|
159
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
160
|
+
return element.hasAttribute("href");
|
|
161
|
+
}
|
|
162
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
166
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
167
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
168
|
+
return element.hasAttribute("controls");
|
|
169
|
+
}
|
|
170
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
171
|
+
const details = element.parentElement;
|
|
172
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasEditableTabStop(element) {
|
|
177
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
178
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
179
|
+
}
|
|
180
|
+
function isTabStop(element) {
|
|
181
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
182
|
+
const tabindex = authoredTabindex(element);
|
|
183
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
184
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
|
+
}
|
|
186
|
+
function tabStopsWithin(root) {
|
|
187
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
188
|
+
isTabStop
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
// src/utils/focus_trap.ts
|
|
119
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
120
193
|
var FocusTrap = class {
|
|
121
194
|
/** The element focused before activation, restored on deactivation. */
|
|
122
195
|
#previouslyFocused = null;
|
|
@@ -278,9 +351,7 @@ var FocusTrap = class {
|
|
|
278
351
|
}
|
|
279
352
|
/** Collects the container's currently focusable descendants in DOM order. */
|
|
280
353
|
#focusableElements() {
|
|
281
|
-
return
|
|
282
|
-
(el) => !el.hidden
|
|
283
|
-
);
|
|
354
|
+
return tabStopsWithin(this.#getContainer());
|
|
284
355
|
}
|
|
285
356
|
};
|
|
286
357
|
|