@jsenv/navi 0.0.1 → 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.
- package/dist/jsenv_navi.js +22954 -0
- package/index.js +66 -16
- package/package.json +22 -11
- package/src/actions.js +50 -26
- package/src/browser_integration/browser_integration.js +31 -6
- package/src/browser_integration/via_history.js +42 -9
- package/src/components/action_execution/render_actionable_component.jsx +6 -4
- package/src/components/action_execution/use_action.js +51 -282
- package/src/components/action_execution/use_execute_action.js +106 -92
- package/src/components/action_execution/use_run_on_mount.js +9 -0
- package/src/components/action_renderer.jsx +21 -32
- package/src/components/demos/0_button_demo.html +574 -103
- package/src/components/demos/10_column_reordering_debug.html +277 -0
- package/src/components/demos/11_table_selection_debug.html +432 -0
- package/src/components/demos/1_checkbox_demo.html +579 -202
- package/src/components/demos/2_input_textual_demo.html +81 -138
- package/src/components/demos/3_radio_demo.html +0 -2
- package/src/components/demos/4_select_demo.html +19 -23
- package/src/components/demos/6_tablist_demo.html +77 -0
- package/src/components/demos/7_table_selection_demo.html +176 -0
- package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
- package/src/components/demos/9_table_column_drag_demo.html +325 -0
- package/src/components/demos/action/0_button_demo.html +2 -4
- package/src/components/demos/action/1_input_text_demo.html +643 -222
- package/src/components/demos/action/3_details_demo.html +146 -115
- package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
- package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
- package/src/components/demos/action/7_radio_list_demo.html +310 -170
- package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
- package/src/components/demos/action/9_link_demo.html +84 -62
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
- package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
- package/src/components/details/details.jsx +62 -64
- package/src/components/edition/editable.jsx +186 -0
- package/src/components/field/README.md +247 -0
- package/src/components/{input → field}/button.jsx +151 -130
- package/src/components/field/checkbox_list.jsx +184 -0
- package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
- package/src/components/{input → field}/field_css.js +4 -1
- package/src/components/field/form.jsx +211 -0
- package/src/components/{input → field}/input.jsx +1 -0
- package/src/components/{input → field}/input_checkbox.jsx +132 -155
- package/src/components/{input → field}/input_radio.jsx +135 -46
- package/src/components/{input → field}/input_textual.jsx +247 -173
- package/src/components/field/label.jsx +32 -0
- package/src/components/field/radio_list.jsx +182 -0
- package/src/components/{input → field}/select.jsx +17 -32
- package/src/components/field/use_action_events.js +132 -0
- package/src/components/field/use_form_events.js +55 -0
- package/src/components/field/use_ui_state_controller.js +506 -0
- package/src/components/item_tracker/README.md +461 -0
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
- package/src/components/item_tracker/use_item_tracker.jsx +143 -0
- package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
- package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
- package/src/components/link/link.jsx +65 -102
- package/src/components/link/link_with_icon.jsx +52 -0
- package/src/components/loader/loader_background.jsx +85 -64
- package/src/components/loader/rectangle_loading.jsx +38 -19
- package/src/components/route.jsx +8 -4
- package/src/components/selection/selection.jsx +1583 -0
- package/src/components/svg/font_sized_svg.jsx +45 -0
- package/src/components/svg/icon_and_text.jsx +21 -0
- package/src/components/svg/svg_mask_overlay.jsx +105 -0
- package/src/components/table/drag/table_drag.jsx +506 -0
- package/src/components/table/resize/table_resize.jsx +650 -0
- package/src/components/table/resize/table_size.js +43 -0
- package/src/components/table/selection/table_selection.js +106 -0
- package/src/components/table/selection/table_selection.jsx +203 -0
- package/src/components/table/sticky/sticky_group.js +354 -0
- package/src/components/table/sticky/table_sticky.js +25 -0
- package/src/components/table/sticky/table_sticky.jsx +501 -0
- package/src/components/table/table.jsx +721 -0
- package/src/components/table/table_css.js +211 -0
- package/src/components/table/table_ui.jsx +49 -0
- package/src/components/table/use_cells_and_columns.js +90 -0
- package/src/components/table/use_object_array_to_cells.js +46 -0
- package/src/components/table/z_indexes.js +23 -0
- package/src/components/tablist/tablist.jsx +99 -0
- package/src/components/text/overflow.jsx +15 -0
- package/src/components/text/text_and_count.jsx +28 -0
- package/src/components/ui_transition.jsx +128 -0
- package/src/components/use_auto_focus.js +58 -7
- package/src/components/use_batch_during_render.js +33 -0
- package/src/components/use_debounce_true.js +7 -7
- package/src/components/use_dependencies_diff.js +35 -0
- package/src/components/use_focus_group.js +4 -3
- package/src/components/use_initial_value.js +8 -34
- package/src/components/use_signal_sync.js +1 -1
- package/src/components/use_stable_callback.js +68 -0
- package/src/components/use_state_array.js +16 -9
- package/src/docs/actions.md +22 -0
- package/src/notes.md +33 -12
- package/src/route/route.js +97 -47
- package/src/store/resource_graph.js +2 -1
- package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
- package/src/utils/is_signal.js +20 -0
- package/src/utils/stringify_for_display.js +4 -23
- package/src/validation/constraints/confirm_constraint.js +14 -0
- package/src/validation/constraints/create_unique_value_constraint.js +27 -0
- package/src/validation/constraints/native_constraints.js +313 -0
- package/src/validation/constraints/readonly_constraint.js +36 -0
- package/src/validation/constraints/single_space_constraint.js +13 -0
- package/src/validation/custom_constraint_validation.js +599 -0
- package/src/validation/custom_message.js +18 -0
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/form_validation_demo.html +142 -0
- package/src/validation/demos/form_validation_demo_preact.html +87 -0
- package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
- package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
- package/src/validation/demos/validation_message_demo.html +203 -0
- package/src/validation/hooks/use_constraints.js +23 -0
- package/src/validation/hooks/use_custom_validation_ref.js +73 -0
- package/src/validation/hooks/use_validation_message.js +19 -0
- package/src/validation/validation_message.js +741 -0
- package/src/components/editable_text/editable_text.jsx +0 -96
- package/src/components/form.jsx +0 -144
- package/src/components/input/checkbox_list.jsx +0 -294
- package/src/components/input/field.jsx +0 -61
- package/src/components/input/radio_list.jsx +0 -283
- package/src/components/input/use_form_event.js +0 -20
- package/src/components/input/use_on_change.js +0 -12
- package/src/components/selection/selection.js +0 -5
- package/src/components/selection/selection_context.jsx +0 -262
- package/src/components/shortcut/shortcut_context.jsx +0 -390
- package/src/components/use_action_events.js +0 -37
- package/src/utils/iterable_weak_set.js +0 -62
- /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
- /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
- /package/src/route/{route.test.html → route.xtest.html} +0 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allowWheelThrough,
|
|
3
|
+
getBorderSizes,
|
|
4
|
+
pickPositionRelativeTo,
|
|
5
|
+
visibleRectEffect,
|
|
6
|
+
} from "@jsenv/dom";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A validation message component that mimics native browser validation messages.
|
|
10
|
+
* Features:
|
|
11
|
+
* - Positions above or below target element based on available space
|
|
12
|
+
* - Follows target element during scrolling and resizing
|
|
13
|
+
* - Automatically hides when target element is not visible
|
|
14
|
+
* - Arrow points at the target element
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shows a validation message attached to the specified element
|
|
19
|
+
* @param {HTMLElement} targetElement - Element the validation message should follow
|
|
20
|
+
* @param {string} message - HTML content for the validation message
|
|
21
|
+
* @param {Object} options - Configuration options
|
|
22
|
+
* @param {boolean} options.scrollIntoView - Whether to scroll the target element into view
|
|
23
|
+
* @returns {Function} - Function to hide and remove the validation message
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import.meta.css = /* css */ `
|
|
27
|
+
/* Ensure the validation message CANNOT cause overflow */
|
|
28
|
+
/* might be important to ensure it cannot create scrollbars in the document */
|
|
29
|
+
/* When measuring the size it should take */
|
|
30
|
+
.jsenv_validation_message_container {
|
|
31
|
+
position: fixed;
|
|
32
|
+
inset: 0;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.jsenv_validation_message {
|
|
37
|
+
display: block;
|
|
38
|
+
overflow: visible;
|
|
39
|
+
height: auto;
|
|
40
|
+
position: absolute;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
opacity: 0;
|
|
43
|
+
left: 0;
|
|
44
|
+
top: 0;
|
|
45
|
+
/* will be positioned with transform: translate */
|
|
46
|
+
transition: opacity 0.2s ease-in-out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.jsenv_validation_message_border {
|
|
50
|
+
position: absolute;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
filter: drop-shadow(4px 4px 3px rgba(0, 0, 0, 0.2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.jsenv_validation_message_body_wrapper {
|
|
56
|
+
border-style: solid;
|
|
57
|
+
border-color: transparent;
|
|
58
|
+
position: relative;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.jsenv_validation_message_body {
|
|
62
|
+
padding: 8px;
|
|
63
|
+
position: relative;
|
|
64
|
+
max-width: 47vw;
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: row;
|
|
67
|
+
gap: 10px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.jsenv_validation_message_icon {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-self: flex-start;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
width: 22px;
|
|
76
|
+
height: 22px;
|
|
77
|
+
border-radius: 2px;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.jsenv_validation_message_exclamation_svg {
|
|
82
|
+
width: 16px;
|
|
83
|
+
height: 12px;
|
|
84
|
+
color: white;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.jsenv_validation_message[data-level="info"] .jsenv_validation_message_icon {
|
|
88
|
+
background-color: #2196f3;
|
|
89
|
+
}
|
|
90
|
+
.jsenv_validation_message[data-level="warning"]
|
|
91
|
+
.jsenv_validation_message_icon {
|
|
92
|
+
background-color: #ff9800;
|
|
93
|
+
}
|
|
94
|
+
.jsenv_validation_message[data-level="error"] .jsenv_validation_message_icon {
|
|
95
|
+
background-color: #f44336;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.jsenv_validation_message_content {
|
|
99
|
+
align-self: center;
|
|
100
|
+
word-break: break-word;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
overflow-wrap: anywhere;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.jsenv_validation_message_border svg {
|
|
106
|
+
position: absolute;
|
|
107
|
+
inset: 0;
|
|
108
|
+
overflow: visible;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.border_path {
|
|
112
|
+
fill: var(--border-color);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.background_path {
|
|
116
|
+
fill: var(--background-color);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.jsenv_validation_message_close_button_column {
|
|
120
|
+
display: flex;
|
|
121
|
+
height: 22px;
|
|
122
|
+
}
|
|
123
|
+
.jsenv_validation_message_close_button {
|
|
124
|
+
border: none;
|
|
125
|
+
background: none;
|
|
126
|
+
padding: 0;
|
|
127
|
+
width: 1em;
|
|
128
|
+
height: 1em;
|
|
129
|
+
font-size: inherit;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
border-radius: 0.2em;
|
|
132
|
+
align-self: center;
|
|
133
|
+
color: currentColor;
|
|
134
|
+
}
|
|
135
|
+
.jsenv_validation_message_close_button:hover {
|
|
136
|
+
background: rgba(0, 0, 0, 0.1);
|
|
137
|
+
}
|
|
138
|
+
.close_svg {
|
|
139
|
+
width: 100%;
|
|
140
|
+
height: 100%;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.error_stack {
|
|
144
|
+
overflow: auto;
|
|
145
|
+
max-height: 200px;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
// HTML template for the validation message
|
|
150
|
+
const validationMessageTemplate = /* html */ `
|
|
151
|
+
<div
|
|
152
|
+
class="jsenv_validation_message_container"
|
|
153
|
+
>
|
|
154
|
+
<div class="jsenv_validation_message" role="alert" aria-live="assertive">
|
|
155
|
+
<div class="jsenv_validation_message_body_wrapper">
|
|
156
|
+
<div class="jsenv_validation_message_border"></div>
|
|
157
|
+
<div class="jsenv_validation_message_body">
|
|
158
|
+
<div class="jsenv_validation_message_icon">
|
|
159
|
+
<svg
|
|
160
|
+
class="jsenv_validation_message_exclamation_svg"
|
|
161
|
+
viewBox="0 0 125 300"
|
|
162
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
163
|
+
>
|
|
164
|
+
<path
|
|
165
|
+
fill="currentColor"
|
|
166
|
+
d="m25,1 8,196h59l8-196zm37,224a37,37 0 1,0 2,0z"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="jsenv_validation_message_content">Default message</div>
|
|
171
|
+
<div class="jsenv_validation_message_close_button_column">
|
|
172
|
+
<button class="jsenv_validation_message_close_button">
|
|
173
|
+
<svg
|
|
174
|
+
class="close_svg"
|
|
175
|
+
viewBox="0 0 24 24"
|
|
176
|
+
fill="none"
|
|
177
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
178
|
+
>
|
|
179
|
+
<path
|
|
180
|
+
fill-rule="evenodd"
|
|
181
|
+
clip-rule="evenodd"
|
|
182
|
+
d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z"
|
|
183
|
+
fill="currentColor"
|
|
184
|
+
/>
|
|
185
|
+
</svg>
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
export const openValidationMessage = (
|
|
195
|
+
targetElement,
|
|
196
|
+
message,
|
|
197
|
+
{
|
|
198
|
+
level = "warning",
|
|
199
|
+
onClose,
|
|
200
|
+
closeOnClickOutside = level === "info",
|
|
201
|
+
debug = false,
|
|
202
|
+
} = {},
|
|
203
|
+
) => {
|
|
204
|
+
let _closeOnClickOutside = closeOnClickOutside;
|
|
205
|
+
|
|
206
|
+
if (debug) {
|
|
207
|
+
console.debug("open validation message on", targetElement, {
|
|
208
|
+
message,
|
|
209
|
+
level,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let opened = true;
|
|
214
|
+
const closeCallbackSet = new Set();
|
|
215
|
+
const close = (reason) => {
|
|
216
|
+
if (!opened) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (debug) {
|
|
220
|
+
console.debug(`validation message closed (reason: ${reason})`);
|
|
221
|
+
}
|
|
222
|
+
opened = false;
|
|
223
|
+
for (const closeCallback of closeCallbackSet) {
|
|
224
|
+
closeCallback();
|
|
225
|
+
}
|
|
226
|
+
closeCallbackSet.clear();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Create and add validation message to document
|
|
230
|
+
const jsenvValidationMessage = createValidationMessage();
|
|
231
|
+
const jsenvValidationMessageContent = jsenvValidationMessage.querySelector(
|
|
232
|
+
".jsenv_validation_message_content",
|
|
233
|
+
);
|
|
234
|
+
const jsenvValidationMessageCloseButton =
|
|
235
|
+
jsenvValidationMessage.querySelector(
|
|
236
|
+
".jsenv_validation_message_close_button",
|
|
237
|
+
);
|
|
238
|
+
jsenvValidationMessageCloseButton.onclick = () => {
|
|
239
|
+
close("click_close_button");
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const update = (
|
|
243
|
+
newMessage,
|
|
244
|
+
{ level = "warning", closeOnClickOutside = level === "info" } = {},
|
|
245
|
+
) => {
|
|
246
|
+
_closeOnClickOutside = closeOnClickOutside;
|
|
247
|
+
const borderColor =
|
|
248
|
+
level === "info" ? "blue" : level === "warning" ? "grey" : "red";
|
|
249
|
+
const backgroundColor = "white";
|
|
250
|
+
|
|
251
|
+
jsenvValidationMessage.style.setProperty("--border-color", borderColor);
|
|
252
|
+
jsenvValidationMessage.style.setProperty(
|
|
253
|
+
"--background-color",
|
|
254
|
+
backgroundColor,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (Error.isError(newMessage)) {
|
|
258
|
+
const error = newMessage;
|
|
259
|
+
newMessage = error.message;
|
|
260
|
+
newMessage += `<pre class="error_stack">${escapeHtml(error.stack)}</pre>`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
jsenvValidationMessage.setAttribute("data-level", level);
|
|
264
|
+
jsenvValidationMessageContent.innerHTML = newMessage;
|
|
265
|
+
};
|
|
266
|
+
update(message, { level });
|
|
267
|
+
|
|
268
|
+
jsenvValidationMessage.style.opacity = "0";
|
|
269
|
+
|
|
270
|
+
allowWheelThrough(jsenvValidationMessage, targetElement);
|
|
271
|
+
|
|
272
|
+
// Connect validation message with target element for accessibility
|
|
273
|
+
const validationMessageId = `jsenv_validation_message-${Date.now()}`;
|
|
274
|
+
jsenvValidationMessage.id = validationMessageId;
|
|
275
|
+
targetElement.setAttribute("aria-invalid", "true");
|
|
276
|
+
targetElement.setAttribute("aria-errormessage", validationMessageId);
|
|
277
|
+
closeCallbackSet.add(() => {
|
|
278
|
+
targetElement.removeAttribute("aria-invalid");
|
|
279
|
+
targetElement.removeAttribute("aria-errormessage");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
document.body.appendChild(jsenvValidationMessage);
|
|
283
|
+
closeCallbackSet.add(() => {
|
|
284
|
+
jsenvValidationMessage.remove();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const positionFollower = stickValidationMessageToTarget(
|
|
288
|
+
jsenvValidationMessage,
|
|
289
|
+
targetElement,
|
|
290
|
+
{
|
|
291
|
+
debug,
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
closeCallbackSet.add(() => {
|
|
295
|
+
positionFollower.stop();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (onClose) {
|
|
299
|
+
closeCallbackSet.add(onClose);
|
|
300
|
+
}
|
|
301
|
+
close_on_target_focus: {
|
|
302
|
+
const onfocus = () => {
|
|
303
|
+
if (level === "error") {
|
|
304
|
+
// error messages must be explicitely closed by the user
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (targetElement.hasAttribute("data-validation-message-stay-on-focus")) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
close("target_element_focus");
|
|
311
|
+
};
|
|
312
|
+
targetElement.addEventListener("focus", onfocus);
|
|
313
|
+
closeCallbackSet.add(() => {
|
|
314
|
+
targetElement.removeEventListener("focus", onfocus);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
close_on_click_outside: {
|
|
319
|
+
const handleClickOutside = (event) => {
|
|
320
|
+
if (!_closeOnClickOutside) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const clickTarget = event.target;
|
|
325
|
+
if (
|
|
326
|
+
clickTarget === jsenvValidationMessage ||
|
|
327
|
+
jsenvValidationMessage.contains(clickTarget)
|
|
328
|
+
) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// if (
|
|
332
|
+
// clickTarget === targetElement ||
|
|
333
|
+
// targetElement.contains(clickTarget)
|
|
334
|
+
// ) {
|
|
335
|
+
// return;
|
|
336
|
+
// }
|
|
337
|
+
close("click_outside");
|
|
338
|
+
};
|
|
339
|
+
document.addEventListener("click", handleClickOutside, true);
|
|
340
|
+
closeCallbackSet.add(() => {
|
|
341
|
+
document.removeEventListener("click", handleClickOutside, true);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const validationMessage = {
|
|
346
|
+
jsenvValidationMessage,
|
|
347
|
+
update,
|
|
348
|
+
close,
|
|
349
|
+
updatePosition: positionFollower.updatePosition,
|
|
350
|
+
};
|
|
351
|
+
targetElement.jsenvValidationMessage = validationMessage;
|
|
352
|
+
closeCallbackSet.add(() => {
|
|
353
|
+
delete targetElement.jsenvValidationMessage;
|
|
354
|
+
});
|
|
355
|
+
return validationMessage;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Configuration parameters for validation message appearance
|
|
359
|
+
const ARROW_WIDTH = 16;
|
|
360
|
+
const ARROW_HEIGHT = 8;
|
|
361
|
+
const CORNER_RADIUS = 3;
|
|
362
|
+
const BORDER_WIDTH = 1;
|
|
363
|
+
const ARROW_SPACING = 8;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Generates SVG path for validation message with arrow on top
|
|
367
|
+
* @param {number} width - Validation message width
|
|
368
|
+
* @param {number} height - Validation message height
|
|
369
|
+
* @param {number} arrowPosition - Horizontal position of arrow
|
|
370
|
+
* @returns {string} - SVG markup
|
|
371
|
+
*/
|
|
372
|
+
const generateSvgWithTopArrow = (width, height, arrowPosition) => {
|
|
373
|
+
// Calculate valid arrow position range
|
|
374
|
+
const arrowLeft =
|
|
375
|
+
ARROW_WIDTH / 2 + CORNER_RADIUS + BORDER_WIDTH + ARROW_SPACING;
|
|
376
|
+
const minArrowPos = arrowLeft;
|
|
377
|
+
const maxArrowPos = width - arrowLeft;
|
|
378
|
+
const constrainedArrowPos = Math.max(
|
|
379
|
+
minArrowPos,
|
|
380
|
+
Math.min(arrowPosition, maxArrowPos),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Calculate content height
|
|
384
|
+
const contentHeight = height - ARROW_HEIGHT;
|
|
385
|
+
|
|
386
|
+
// Create two paths: one for the border (outer) and one for the content (inner)
|
|
387
|
+
const adjustedWidth = width;
|
|
388
|
+
const adjustedHeight = contentHeight + ARROW_HEIGHT;
|
|
389
|
+
|
|
390
|
+
// Slight adjustment for visual balance
|
|
391
|
+
const innerArrowWidthReduction = Math.min(BORDER_WIDTH * 0.3, 1);
|
|
392
|
+
|
|
393
|
+
// Outer path (border)
|
|
394
|
+
const outerPath = `
|
|
395
|
+
M${CORNER_RADIUS},${ARROW_HEIGHT}
|
|
396
|
+
H${constrainedArrowPos - ARROW_WIDTH / 2}
|
|
397
|
+
L${constrainedArrowPos},0
|
|
398
|
+
L${constrainedArrowPos + ARROW_WIDTH / 2},${ARROW_HEIGHT}
|
|
399
|
+
H${width - CORNER_RADIUS}
|
|
400
|
+
Q${width},${ARROW_HEIGHT} ${width},${ARROW_HEIGHT + CORNER_RADIUS}
|
|
401
|
+
V${adjustedHeight - CORNER_RADIUS}
|
|
402
|
+
Q${width},${adjustedHeight} ${width - CORNER_RADIUS},${adjustedHeight}
|
|
403
|
+
H${CORNER_RADIUS}
|
|
404
|
+
Q0,${adjustedHeight} 0,${adjustedHeight - CORNER_RADIUS}
|
|
405
|
+
V${ARROW_HEIGHT + CORNER_RADIUS}
|
|
406
|
+
Q0,${ARROW_HEIGHT} ${CORNER_RADIUS},${ARROW_HEIGHT}
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
// Inner path (content) - keep arrow width almost the same
|
|
410
|
+
const innerRadius = Math.max(0, CORNER_RADIUS - BORDER_WIDTH);
|
|
411
|
+
const innerPath = `
|
|
412
|
+
M${innerRadius + BORDER_WIDTH},${ARROW_HEIGHT + BORDER_WIDTH}
|
|
413
|
+
H${constrainedArrowPos - ARROW_WIDTH / 2 + innerArrowWidthReduction}
|
|
414
|
+
L${constrainedArrowPos},${BORDER_WIDTH}
|
|
415
|
+
L${constrainedArrowPos + ARROW_WIDTH / 2 - innerArrowWidthReduction},${ARROW_HEIGHT + BORDER_WIDTH}
|
|
416
|
+
H${width - innerRadius - BORDER_WIDTH}
|
|
417
|
+
Q${width - BORDER_WIDTH},${ARROW_HEIGHT + BORDER_WIDTH} ${width - BORDER_WIDTH},${ARROW_HEIGHT + innerRadius + BORDER_WIDTH}
|
|
418
|
+
V${adjustedHeight - innerRadius - BORDER_WIDTH}
|
|
419
|
+
Q${width - BORDER_WIDTH},${adjustedHeight - BORDER_WIDTH} ${width - innerRadius - BORDER_WIDTH},${adjustedHeight - BORDER_WIDTH}
|
|
420
|
+
H${innerRadius + BORDER_WIDTH}
|
|
421
|
+
Q${BORDER_WIDTH},${adjustedHeight - BORDER_WIDTH} ${BORDER_WIDTH},${adjustedHeight - innerRadius - BORDER_WIDTH}
|
|
422
|
+
V${ARROW_HEIGHT + innerRadius + BORDER_WIDTH}
|
|
423
|
+
Q${BORDER_WIDTH},${ARROW_HEIGHT + BORDER_WIDTH} ${innerRadius + BORDER_WIDTH},${ARROW_HEIGHT + BORDER_WIDTH}
|
|
424
|
+
`;
|
|
425
|
+
|
|
426
|
+
return /*html */ `<svg
|
|
427
|
+
width="${adjustedWidth}"
|
|
428
|
+
height="${adjustedHeight}"
|
|
429
|
+
viewBox="0 0 ${adjustedWidth} ${adjustedHeight}"
|
|
430
|
+
fill="none"
|
|
431
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
432
|
+
role="presentation"
|
|
433
|
+
aria-hidden="true"
|
|
434
|
+
>
|
|
435
|
+
<path d="${outerPath}" class="border_path" />
|
|
436
|
+
<path d="${innerPath}" class="background_path" />
|
|
437
|
+
</svg>`;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Generates SVG path for validation message with arrow on bottom
|
|
442
|
+
* @param {number} width - Validation message width
|
|
443
|
+
* @param {number} height - Validation message height
|
|
444
|
+
* @param {number} arrowPosition - Horizontal position of arrow
|
|
445
|
+
* @returns {string} - SVG markup
|
|
446
|
+
*/
|
|
447
|
+
const generateSvgWithBottomArrow = (width, height, arrowPosition) => {
|
|
448
|
+
// Calculate valid arrow position range
|
|
449
|
+
const arrowLeft =
|
|
450
|
+
ARROW_WIDTH / 2 + CORNER_RADIUS + BORDER_WIDTH + ARROW_SPACING;
|
|
451
|
+
const minArrowPos = arrowLeft;
|
|
452
|
+
const maxArrowPos = width - arrowLeft;
|
|
453
|
+
const constrainedArrowPos = Math.max(
|
|
454
|
+
minArrowPos,
|
|
455
|
+
Math.min(arrowPosition, maxArrowPos),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Calculate content height
|
|
459
|
+
const contentHeight = height - ARROW_HEIGHT;
|
|
460
|
+
|
|
461
|
+
// Create two paths: one for the border (outer) and one for the content (inner)
|
|
462
|
+
const adjustedWidth = width;
|
|
463
|
+
const adjustedHeight = contentHeight + ARROW_HEIGHT;
|
|
464
|
+
|
|
465
|
+
// For small border widths, keep inner arrow nearly the same size as outer
|
|
466
|
+
const innerArrowWidthReduction = Math.min(BORDER_WIDTH * 0.3, 1);
|
|
467
|
+
|
|
468
|
+
// Outer path with rounded corners
|
|
469
|
+
const outerPath = `
|
|
470
|
+
M${CORNER_RADIUS},0
|
|
471
|
+
H${width - CORNER_RADIUS}
|
|
472
|
+
Q${width},0 ${width},${CORNER_RADIUS}
|
|
473
|
+
V${contentHeight - CORNER_RADIUS}
|
|
474
|
+
Q${width},${contentHeight} ${width - CORNER_RADIUS},${contentHeight}
|
|
475
|
+
H${constrainedArrowPos + ARROW_WIDTH / 2}
|
|
476
|
+
L${constrainedArrowPos},${adjustedHeight}
|
|
477
|
+
L${constrainedArrowPos - ARROW_WIDTH / 2},${contentHeight}
|
|
478
|
+
H${CORNER_RADIUS}
|
|
479
|
+
Q0,${contentHeight} 0,${contentHeight - CORNER_RADIUS}
|
|
480
|
+
V${CORNER_RADIUS}
|
|
481
|
+
Q0,0 ${CORNER_RADIUS},0
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
// Inner path with correct arrow direction and color
|
|
485
|
+
const innerRadius = Math.max(0, CORNER_RADIUS - BORDER_WIDTH);
|
|
486
|
+
const innerPath = `
|
|
487
|
+
M${innerRadius + BORDER_WIDTH},${BORDER_WIDTH}
|
|
488
|
+
H${width - innerRadius - BORDER_WIDTH}
|
|
489
|
+
Q${width - BORDER_WIDTH},${BORDER_WIDTH} ${width - BORDER_WIDTH},${innerRadius + BORDER_WIDTH}
|
|
490
|
+
V${contentHeight - innerRadius - BORDER_WIDTH}
|
|
491
|
+
Q${width - BORDER_WIDTH},${contentHeight - BORDER_WIDTH} ${width - innerRadius - BORDER_WIDTH},${contentHeight - BORDER_WIDTH}
|
|
492
|
+
H${constrainedArrowPos + ARROW_WIDTH / 2 - innerArrowWidthReduction}
|
|
493
|
+
L${constrainedArrowPos},${adjustedHeight - BORDER_WIDTH}
|
|
494
|
+
L${constrainedArrowPos - ARROW_WIDTH / 2 + innerArrowWidthReduction},${contentHeight - BORDER_WIDTH}
|
|
495
|
+
H${innerRadius + BORDER_WIDTH}
|
|
496
|
+
Q${BORDER_WIDTH},${contentHeight - BORDER_WIDTH} ${BORDER_WIDTH},${contentHeight - innerRadius - BORDER_WIDTH}
|
|
497
|
+
V${innerRadius + BORDER_WIDTH}
|
|
498
|
+
Q${BORDER_WIDTH},${BORDER_WIDTH} ${innerRadius + BORDER_WIDTH},${BORDER_WIDTH}
|
|
499
|
+
`;
|
|
500
|
+
|
|
501
|
+
return /*html */ `<svg
|
|
502
|
+
width="${adjustedWidth}"
|
|
503
|
+
height="${adjustedHeight}"
|
|
504
|
+
viewBox="0 0 ${adjustedWidth} ${adjustedHeight}"
|
|
505
|
+
fill="none"
|
|
506
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
507
|
+
role="presentation"
|
|
508
|
+
aria-hidden="true"
|
|
509
|
+
>
|
|
510
|
+
<path d="${outerPath}" class="border_path" />
|
|
511
|
+
<path d="${innerPath}" class="background_path" />
|
|
512
|
+
</svg>`;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Creates a new validation message element with specified content
|
|
517
|
+
* @param {string} content - HTML content for the validation message
|
|
518
|
+
* @returns {HTMLElement} - The validation message element
|
|
519
|
+
*/
|
|
520
|
+
const createValidationMessage = () => {
|
|
521
|
+
const div = document.createElement("div");
|
|
522
|
+
div.innerHTML = validationMessageTemplate;
|
|
523
|
+
const validationMessage = div.querySelector(".jsenv_validation_message");
|
|
524
|
+
return validationMessage;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const stickValidationMessageToTarget = (validationMessage, targetElement) => {
|
|
528
|
+
// Get references to validation message parts
|
|
529
|
+
const validationMessageBodyWrapper = validationMessage.querySelector(
|
|
530
|
+
".jsenv_validation_message_body_wrapper",
|
|
531
|
+
);
|
|
532
|
+
const validationMessageBorder = validationMessage.querySelector(
|
|
533
|
+
".jsenv_validation_message_border",
|
|
534
|
+
);
|
|
535
|
+
const validationMessageContent = validationMessage.querySelector(
|
|
536
|
+
".jsenv_validation_message_content",
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Set initial border styles
|
|
540
|
+
validationMessageBodyWrapper.style.borderWidth = `${BORDER_WIDTH}px`;
|
|
541
|
+
validationMessageBorder.style.left = `-${BORDER_WIDTH}px`;
|
|
542
|
+
validationMessageBorder.style.right = `-${BORDER_WIDTH}px`;
|
|
543
|
+
|
|
544
|
+
const targetVisibleRectEffect = visibleRectEffect(
|
|
545
|
+
targetElement,
|
|
546
|
+
({ left: targetLeft, right: targetRight, visibilityRatio }) => {
|
|
547
|
+
// reset max height and overflow because it impacts the element size
|
|
548
|
+
// and we need to re-check if we need to have an overflow or not.
|
|
549
|
+
// to avoid visual impact we do this on an invisible clone.
|
|
550
|
+
// It's ok to do this because the element is absolutely positioned
|
|
551
|
+
const validationMessageClone = validationMessage.cloneNode(true);
|
|
552
|
+
validationMessageClone.style.visibility = "hidden";
|
|
553
|
+
const validationMessageContentClone =
|
|
554
|
+
validationMessageClone.querySelector(
|
|
555
|
+
".jsenv_validation_message_content",
|
|
556
|
+
);
|
|
557
|
+
validationMessageContentClone.style.maxHeight = "";
|
|
558
|
+
validationMessageContentClone.style.overflowY = "";
|
|
559
|
+
validationMessage.parentNode.appendChild(validationMessageClone);
|
|
560
|
+
const {
|
|
561
|
+
position,
|
|
562
|
+
left: validationMessageLeft,
|
|
563
|
+
top: validationMessageTop,
|
|
564
|
+
width: validationMessageWidth,
|
|
565
|
+
height: validationMessageHeight,
|
|
566
|
+
spaceAboveTarget,
|
|
567
|
+
spaceBelowTarget,
|
|
568
|
+
} = pickPositionRelativeTo(validationMessageClone, targetElement, {
|
|
569
|
+
alignToViewportEdgeWhenTargetNearEdge: 20,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Get element padding and border to properly position arrow
|
|
573
|
+
const targetBorderSizes = getBorderSizes(targetElement);
|
|
574
|
+
|
|
575
|
+
// Calculate arrow position to point at target element
|
|
576
|
+
let arrowLeftPosOnValidationMessage;
|
|
577
|
+
// Determine arrow target position based on attribute
|
|
578
|
+
const arrowPositionAttribute = targetElement.getAttribute(
|
|
579
|
+
"data-validation-message-arrow-x",
|
|
580
|
+
);
|
|
581
|
+
let arrowTargetLeft;
|
|
582
|
+
if (arrowPositionAttribute === "center") {
|
|
583
|
+
// Target the center of the element
|
|
584
|
+
arrowTargetLeft = targetRight / 2;
|
|
585
|
+
} else {
|
|
586
|
+
// Default behavior: target the left edge of the element (after borders)
|
|
587
|
+
arrowTargetLeft = targetLeft + targetBorderSizes.left;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Calculate arrow position within the validation message
|
|
591
|
+
if (validationMessageLeft < arrowTargetLeft) {
|
|
592
|
+
// Validation message is left of the target point, move arrow right
|
|
593
|
+
const diff = arrowTargetLeft - validationMessageLeft;
|
|
594
|
+
arrowLeftPosOnValidationMessage = diff;
|
|
595
|
+
} else if (
|
|
596
|
+
validationMessageLeft + validationMessageWidth <
|
|
597
|
+
arrowTargetLeft
|
|
598
|
+
) {
|
|
599
|
+
// Edge case: target point is beyond right edge of validation message
|
|
600
|
+
arrowLeftPosOnValidationMessage = validationMessageWidth - ARROW_WIDTH;
|
|
601
|
+
} else {
|
|
602
|
+
// Target point is within validation message width
|
|
603
|
+
arrowLeftPosOnValidationMessage =
|
|
604
|
+
arrowTargetLeft - validationMessageLeft;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Ensure arrow stays within validation message bounds with some padding
|
|
608
|
+
const minArrowPos = CORNER_RADIUS + ARROW_WIDTH / 2 + ARROW_SPACING;
|
|
609
|
+
const maxArrowPos = validationMessageWidth - minArrowPos;
|
|
610
|
+
arrowLeftPosOnValidationMessage = Math.max(
|
|
611
|
+
minArrowPos,
|
|
612
|
+
Math.min(arrowLeftPosOnValidationMessage, maxArrowPos),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// Force content overflow when there is not enough space to display
|
|
616
|
+
// the entirety of the validation message
|
|
617
|
+
const spaceAvailable =
|
|
618
|
+
position === "below" ? spaceBelowTarget : spaceAboveTarget;
|
|
619
|
+
let spaceAvailableForContent = spaceAvailable;
|
|
620
|
+
spaceAvailableForContent -= ARROW_HEIGHT;
|
|
621
|
+
spaceAvailableForContent -= BORDER_WIDTH * 2;
|
|
622
|
+
spaceAvailableForContent -= 16; // padding * 2
|
|
623
|
+
let contentHeight = validationMessageHeight;
|
|
624
|
+
contentHeight -= ARROW_HEIGHT;
|
|
625
|
+
contentHeight -= BORDER_WIDTH * 2;
|
|
626
|
+
contentHeight -= 16; // padding * 2
|
|
627
|
+
const spaceRemainingAfterContent =
|
|
628
|
+
spaceAvailableForContent - contentHeight;
|
|
629
|
+
console.log({
|
|
630
|
+
position,
|
|
631
|
+
spaceBelowTarget,
|
|
632
|
+
validationMessageHeight,
|
|
633
|
+
spaceAvailableForContent,
|
|
634
|
+
contentHeight,
|
|
635
|
+
spaceRemainingAfterContent,
|
|
636
|
+
});
|
|
637
|
+
if (spaceRemainingAfterContent < 2) {
|
|
638
|
+
const maxHeight = spaceAvailableForContent;
|
|
639
|
+
validationMessageContent.style.maxHeight = `${maxHeight}px`;
|
|
640
|
+
validationMessageContent.style.overflowY = "scroll";
|
|
641
|
+
} else {
|
|
642
|
+
validationMessageContent.style.maxHeight = "";
|
|
643
|
+
validationMessageContent.style.overflowY = "";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const { width, height } = validationMessage.getBoundingClientRect();
|
|
647
|
+
if (position === "above") {
|
|
648
|
+
// Position above target element
|
|
649
|
+
validationMessageBodyWrapper.style.marginTop = "";
|
|
650
|
+
validationMessageBodyWrapper.style.marginBottom = `${ARROW_HEIGHT}px`;
|
|
651
|
+
validationMessageBorder.style.top = `-${BORDER_WIDTH}px`;
|
|
652
|
+
validationMessageBorder.style.bottom = `-${BORDER_WIDTH + ARROW_HEIGHT - 0.5}px`;
|
|
653
|
+
validationMessageBorder.innerHTML = generateSvgWithBottomArrow(
|
|
654
|
+
width,
|
|
655
|
+
height,
|
|
656
|
+
arrowLeftPosOnValidationMessage,
|
|
657
|
+
);
|
|
658
|
+
} else {
|
|
659
|
+
validationMessageBodyWrapper.style.marginTop = `${ARROW_HEIGHT}px`;
|
|
660
|
+
validationMessageBodyWrapper.style.marginBottom = "";
|
|
661
|
+
validationMessageBorder.style.top = `-${BORDER_WIDTH + ARROW_HEIGHT - 0.5}px`;
|
|
662
|
+
validationMessageBorder.style.bottom = `-${BORDER_WIDTH}px`;
|
|
663
|
+
validationMessageBorder.innerHTML = generateSvgWithTopArrow(
|
|
664
|
+
width,
|
|
665
|
+
height,
|
|
666
|
+
arrowLeftPosOnValidationMessage,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
validationMessage.style.opacity = visibilityRatio ? "1" : "0";
|
|
671
|
+
validationMessage.setAttribute("data-position", position);
|
|
672
|
+
validationMessage.style.transform = `translateX(${validationMessageLeft}px) translateY(${validationMessageTop}px)`;
|
|
673
|
+
|
|
674
|
+
validationMessageClone.remove();
|
|
675
|
+
},
|
|
676
|
+
);
|
|
677
|
+
const messageSizeChangeObserver = observeValidationMessageSizeChange(
|
|
678
|
+
validationMessageContent,
|
|
679
|
+
(width, height) => {
|
|
680
|
+
targetVisibleRectEffect.check(`content_size_change (${width}x${height})`);
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
targetVisibleRectEffect.onBeforeAutoCheck(() => {
|
|
684
|
+
// prevent feedback loop because check triggers size change which triggers check...
|
|
685
|
+
messageSizeChangeObserver.disable();
|
|
686
|
+
return () => {
|
|
687
|
+
messageSizeChangeObserver.enable();
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
updatePosition: targetVisibleRectEffect.check,
|
|
693
|
+
stop: () => {
|
|
694
|
+
messageSizeChangeObserver.disconnect();
|
|
695
|
+
targetVisibleRectEffect.disconnect();
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const observeValidationMessageSizeChange = (elementSizeToObserve, callback) => {
|
|
701
|
+
let lastContentWidth;
|
|
702
|
+
let lastContentHeight;
|
|
703
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
704
|
+
const [entry] = entries;
|
|
705
|
+
const { width, height } = entry.contentRect;
|
|
706
|
+
// Debounce tiny changes that are likely sub-pixel rounding
|
|
707
|
+
if (lastContentWidth !== undefined) {
|
|
708
|
+
const widthDiff = Math.abs(width - lastContentWidth);
|
|
709
|
+
const heightDiff = Math.abs(height - lastContentHeight);
|
|
710
|
+
const threshold = 1; // Ignore changes smaller than 1px
|
|
711
|
+
if (widthDiff < threshold && heightDiff < threshold) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
lastContentWidth = width;
|
|
716
|
+
lastContentHeight = height;
|
|
717
|
+
callback(width, height);
|
|
718
|
+
});
|
|
719
|
+
resizeObserver.observe(elementSizeToObserve);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
disable: () => {
|
|
723
|
+
resizeObserver.unobserve(elementSizeToObserve);
|
|
724
|
+
},
|
|
725
|
+
enable: () => {
|
|
726
|
+
resizeObserver.observe(elementSizeToObserve);
|
|
727
|
+
},
|
|
728
|
+
disconnect: () => {
|
|
729
|
+
resizeObserver.disconnect();
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const escapeHtml = (string) => {
|
|
735
|
+
return string
|
|
736
|
+
.replace(/&/g, "&")
|
|
737
|
+
.replace(/</g, "<")
|
|
738
|
+
.replace(/>/g, ">")
|
|
739
|
+
.replace(/"/g, """)
|
|
740
|
+
.replace(/'/g, "'");
|
|
741
|
+
};
|