@rettangoli/ui 0.1.2-rc3 → 0.1.2-rc31
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/rettangoli-iife-layout.min.js +115 -43
- package/dist/rettangoli-iife-ui.min.js +187 -67
- package/package.json +5 -3
- package/src/cli/buildSvg.js +86 -0
- package/src/cli/index.js +1 -0
- package/src/common.js +19 -0
- package/src/components/breadcrumb/breadcrumb.handlers.js +9 -0
- package/src/components/breadcrumb/breadcrumb.store.js +29 -0
- package/src/components/breadcrumb/breadcrumb.view.yaml +64 -0
- package/src/components/dropdownMenu/dropdownMenu.handlers.js +4 -4
- package/src/components/dropdownMenu/dropdownMenu.store.js +5 -17
- package/src/components/dropdownMenu/dropdownMenu.view.yaml +15 -13
- package/src/components/form/form.handlers.js +173 -25
- package/src/components/form/form.store.js +176 -22
- package/src/components/form/form.view.yaml +217 -33
- package/src/components/pageOutline/pageOutline.handlers.js +57 -17
- package/src/components/pageOutline/pageOutline.store.js +46 -1
- package/src/components/pageOutline/pageOutline.view.yaml +7 -5
- package/src/components/popoverInput/popoverInput.handlers.js +99 -0
- package/src/components/popoverInput/popoverInput.store.js +48 -0
- package/src/components/popoverInput/popoverInput.view.yaml +55 -0
- package/src/components/select/select.handlers.js +116 -11
- package/src/components/select/select.store.js +84 -18
- package/src/components/select/select.view.yaml +40 -10
- package/src/components/sidebar/sidebar.view.yaml +1 -1
- package/src/components/sliderInput/sliderInput.handlers.js +41 -0
- package/src/components/sliderInput/sliderInput.store.js +18 -0
- package/src/components/sliderInput/sliderInput.view.yaml +42 -0
- package/src/components/table/table.handlers.js +1 -1
- package/src/components/tabs/tabs.handlers.js +10 -0
- package/src/components/tabs/tabs.store.js +29 -0
- package/src/components/tabs/tabs.view.yaml +64 -0
- package/src/components/tooltip/tooltip.handlers.js +0 -0
- package/src/components/tooltip/tooltip.store.js +12 -0
- package/src/components/tooltip/tooltip.view.yaml +27 -0
- package/src/components/waveform/waveform.handlers.js +92 -0
- package/src/components/waveform/waveform.store.js +17 -0
- package/src/components/waveform/waveform.view.yaml +38 -0
- package/src/entry-iife-layout.js +3 -0
- package/src/entry-iife-ui.js +4 -0
- package/src/index.js +5 -1
- package/src/primitives/button.js +10 -0
- package/src/primitives/colorPicker.js +9 -0
- package/src/primitives/dialog.js +254 -0
- package/src/primitives/input.js +41 -11
- package/src/primitives/popover.js +280 -0
- package/src/primitives/slider.js +18 -9
- package/src/primitives/svg.js +2 -0
- package/src/primitives/textarea.js +25 -1
- package/src/styles/cursorStyles.js +38 -2
- package/src/components/dialog/dialog.handlers.js +0 -5
- package/src/components/dialog/dialog.store.js +0 -25
- package/src/components/dialog/dialog.view.yaml +0 -44
- package/src/components/popover/popover.handlers.js +0 -5
- package/src/components/popover/popover.store.js +0 -12
- package/src/components/popover/popover.view.yaml +0 -57
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { css } from "../common.js";
|
|
2
|
+
|
|
3
|
+
class RettangoliDialogElement extends HTMLElement {
|
|
4
|
+
static styleSheet = null;
|
|
5
|
+
|
|
6
|
+
static initializeStyleSheet() {
|
|
7
|
+
if (!RettangoliDialogElement.styleSheet) {
|
|
8
|
+
RettangoliDialogElement.styleSheet = new CSSStyleSheet();
|
|
9
|
+
RettangoliDialogElement.styleSheet.replaceSync(css`
|
|
10
|
+
:host {
|
|
11
|
+
display: contents;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dialog {
|
|
15
|
+
padding: 0;
|
|
16
|
+
border: none;
|
|
17
|
+
background: transparent;
|
|
18
|
+
margin: auto;
|
|
19
|
+
overflow-y: scroll;
|
|
20
|
+
color: inherit;
|
|
21
|
+
max-height: 100vh;
|
|
22
|
+
height: 100vh;
|
|
23
|
+
max-width: 100vw;
|
|
24
|
+
scrollbar-width: none;
|
|
25
|
+
outline: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dialog::backdrop {
|
|
29
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
slot[name="content"] {
|
|
33
|
+
background-color: var(--background) !important;
|
|
34
|
+
display: block;
|
|
35
|
+
padding: var(--spacing-lg);
|
|
36
|
+
border: 1px solid var(--border);
|
|
37
|
+
border-radius: var(--border-radius-md);
|
|
38
|
+
margin-left: var(--spacing-lg);
|
|
39
|
+
margin-right: var(--spacing-lg);
|
|
40
|
+
width: fit-content;
|
|
41
|
+
max-width: calc(100vw - 2 * var(--spacing-lg));
|
|
42
|
+
/* Default margins will be set dynamically via JavaScript for adaptive centering */
|
|
43
|
+
margin-top: 40px;
|
|
44
|
+
margin-bottom: 40px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Size attribute styles */
|
|
48
|
+
:host([s="sm"]) slot[name="content"] {
|
|
49
|
+
width: 33vw;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:host([s="md"]) slot[name="content"] {
|
|
53
|
+
width: 50vw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:host([s="lg"]) slot[name="content"] {
|
|
57
|
+
width: 80vw;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([s="f"]) slot[name="content"] {
|
|
61
|
+
width: 100vw;
|
|
62
|
+
margin-left: 0;
|
|
63
|
+
margin-right: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes dialog-in {
|
|
67
|
+
from {
|
|
68
|
+
opacity: 0;
|
|
69
|
+
transform: scale(0.95);
|
|
70
|
+
}
|
|
71
|
+
to {
|
|
72
|
+
opacity: 1;
|
|
73
|
+
transform: scale(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
dialog[open] slot[name="content"] {
|
|
78
|
+
animation: dialog-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
79
|
+
}
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
super();
|
|
86
|
+
RettangoliDialogElement.initializeStyleSheet();
|
|
87
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
88
|
+
this.shadow.adoptedStyleSheets = [RettangoliDialogElement.styleSheet];
|
|
89
|
+
|
|
90
|
+
// Create dialog element
|
|
91
|
+
this._dialogElement = document.createElement('dialog');
|
|
92
|
+
this.shadow.appendChild(this._dialogElement);
|
|
93
|
+
|
|
94
|
+
// Store reference for content slot
|
|
95
|
+
this._slotElement = null;
|
|
96
|
+
this._isConnected = false;
|
|
97
|
+
|
|
98
|
+
// Handle click outside - emit custom event
|
|
99
|
+
this._dialogElement.addEventListener('click', (e) => {
|
|
100
|
+
if (e.target === this._dialogElement) {
|
|
101
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
102
|
+
detail: {}
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Handle right-click on overlay to close dialog
|
|
108
|
+
this._dialogElement.addEventListener('contextmenu', (e) => {
|
|
109
|
+
if (e.target === this._dialogElement) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
112
|
+
detail: {}
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Handle ESC key - prevent native close and emit custom event
|
|
118
|
+
this._dialogElement.addEventListener('cancel', (e) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
121
|
+
detail: {}
|
|
122
|
+
}));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static get observedAttributes() {
|
|
127
|
+
return ["open", "w", "s"];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
connectedCallback() {
|
|
131
|
+
this._updateDialog();
|
|
132
|
+
this._isConnected = true;
|
|
133
|
+
// Check initial open attribute
|
|
134
|
+
if (this.hasAttribute('open')) {
|
|
135
|
+
this._showModal();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
140
|
+
if (name === 'open') {
|
|
141
|
+
if (newValue !== null && !this._dialogElement.open && this._isConnected) {
|
|
142
|
+
this._showModal();
|
|
143
|
+
} else if (newValue === null && this._dialogElement.open) {
|
|
144
|
+
this._hideModal();
|
|
145
|
+
}
|
|
146
|
+
} else if (name === 'w') {
|
|
147
|
+
this._updateWidth();
|
|
148
|
+
} else if (name === 's') {
|
|
149
|
+
// Size is handled via CSS :host() selectors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_updateDialog() {
|
|
154
|
+
this._updateWidth();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_updateWidth() {
|
|
158
|
+
const width = this.getAttribute('w');
|
|
159
|
+
if (width) {
|
|
160
|
+
this._dialogElement.style.width = width;
|
|
161
|
+
} else {
|
|
162
|
+
this._dialogElement.style.width = '';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Internal methods
|
|
167
|
+
_showModal() {
|
|
168
|
+
if (!this._dialogElement.open) {
|
|
169
|
+
// Create and append slot for content only if it doesn't exist
|
|
170
|
+
if (!this._slotElement) {
|
|
171
|
+
this._slotElement = document.createElement('slot');
|
|
172
|
+
this._slotElement.setAttribute('name', 'content');
|
|
173
|
+
this._dialogElement.appendChild(this._slotElement);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this._dialogElement.showModal();
|
|
177
|
+
|
|
178
|
+
// Reset scroll position
|
|
179
|
+
this._dialogElement.scrollTop = 0;
|
|
180
|
+
|
|
181
|
+
// Apply adaptive centering
|
|
182
|
+
this._applyAdaptiveCentering();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_hideModal() {
|
|
187
|
+
if (this._dialogElement.open) {
|
|
188
|
+
this._dialogElement.close();
|
|
189
|
+
|
|
190
|
+
// Remove slot to unmount content
|
|
191
|
+
if (this._slotElement) {
|
|
192
|
+
// Reset any inline styles applied for adaptive centering
|
|
193
|
+
this._slotElement.style.marginTop = '';
|
|
194
|
+
this._slotElement.style.marginBottom = '';
|
|
195
|
+
|
|
196
|
+
this._dialogElement.removeChild(this._slotElement);
|
|
197
|
+
this._slotElement = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reset dialog height
|
|
201
|
+
this._dialogElement.style.height = '';
|
|
202
|
+
|
|
203
|
+
// Don't emit any event when programmatically closed via attribute
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_applyAdaptiveCentering() {
|
|
208
|
+
if (!this._slotElement) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
213
|
+
requestAnimationFrame(() => {
|
|
214
|
+
if (!this._slotElement) return;
|
|
215
|
+
|
|
216
|
+
// Get the actual height of the content
|
|
217
|
+
const contentHeight = this._slotElement.offsetHeight;
|
|
218
|
+
const viewportHeight = window.innerHeight;
|
|
219
|
+
|
|
220
|
+
// Calculate centered position with minimum margins for scrollability
|
|
221
|
+
const minMargin = 40; // Minimum margin in pixels to ensure scrollability
|
|
222
|
+
|
|
223
|
+
if (contentHeight >= viewportHeight - (2 * minMargin)) {
|
|
224
|
+
// Content is too tall, use minimum margins to allow scrolling
|
|
225
|
+
// Start near the top with small margin so content isn't pushed too far down
|
|
226
|
+
this._slotElement.style.marginTop = `${minMargin}px`;
|
|
227
|
+
this._slotElement.style.marginBottom = `${minMargin}px`;
|
|
228
|
+
// Keep dialog at full height for scrolling
|
|
229
|
+
this._dialogElement.style.height = '100vh';
|
|
230
|
+
} else {
|
|
231
|
+
// Content fits, center it vertically
|
|
232
|
+
const totalMargin = viewportHeight - contentHeight;
|
|
233
|
+
const margin = Math.floor(totalMargin / 2);
|
|
234
|
+
this._slotElement.style.marginTop = `${margin}px`;
|
|
235
|
+
this._slotElement.style.marginBottom = `${margin}px`;
|
|
236
|
+
// Set dialog height to auto to prevent unnecessary scrollbar
|
|
237
|
+
this._dialogElement.style.height = 'auto';
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
// Expose dialog element for advanced usage
|
|
244
|
+
get dialog() {
|
|
245
|
+
return this._dialogElement;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Export factory function to maintain API compatibility
|
|
250
|
+
export default ({ render, html }) => {
|
|
251
|
+
// Note: render and html parameters are accepted but not used
|
|
252
|
+
// This maintains backward compatibility with existing code
|
|
253
|
+
return RettangoliDialogElement;
|
|
254
|
+
};
|
package/src/primitives/input.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
css,
|
|
1
|
+
import {
|
|
2
|
+
css,
|
|
3
3
|
dimensionWithUnit,
|
|
4
4
|
convertObjectToCssString,
|
|
5
5
|
styleMapKeys,
|
|
@@ -59,7 +59,7 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
59
59
|
RettangoliInputElement.initializeStyleSheet();
|
|
60
60
|
this.shadow = this.attachShadow({ mode: "closed" });
|
|
61
61
|
this.shadow.adoptedStyleSheets = [RettangoliInputElement.styleSheet];
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
// Initialize style tracking properties
|
|
64
64
|
this._styles = {
|
|
65
65
|
default: {},
|
|
@@ -69,11 +69,11 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
69
69
|
xl: {},
|
|
70
70
|
};
|
|
71
71
|
this._lastStyleString = "";
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
// Create initial DOM structure
|
|
74
74
|
this._inputElement = document.createElement('input');
|
|
75
75
|
this._styleElement = document.createElement('style');
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
this.shadow.appendChild(this._styleElement);
|
|
78
78
|
this.shadow.appendChild(this._inputElement);
|
|
79
79
|
|
|
@@ -83,10 +83,11 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
83
83
|
|
|
84
84
|
static get observedAttributes() {
|
|
85
85
|
return [
|
|
86
|
-
"key",
|
|
87
|
-
"type",
|
|
88
|
-
"placeholder",
|
|
86
|
+
"key",
|
|
87
|
+
"type",
|
|
88
|
+
"placeholder",
|
|
89
89
|
"disabled",
|
|
90
|
+
"value",
|
|
90
91
|
"s",
|
|
91
92
|
...permutateBreakpoints([
|
|
92
93
|
...styleMapKeys,
|
|
@@ -105,6 +106,14 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
105
106
|
return this._inputElement.value;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
set value(newValue) {
|
|
110
|
+
this._inputElement.value = newValue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
focus() {
|
|
114
|
+
this._inputElement.focus();
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
_onChange = (event) => {
|
|
109
118
|
this.dispatchEvent(new CustomEvent('input-change', {
|
|
110
119
|
detail: {
|
|
@@ -114,8 +123,17 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
126
|
+
// Handle key attribute change - reset value
|
|
127
|
+
if (name === "key" && oldValue !== newValue) {
|
|
128
|
+
requestAnimationFrame((() => {
|
|
129
|
+
const value = this.getAttribute("value");
|
|
130
|
+
this._inputElement.value = value ?? "";
|
|
131
|
+
}))
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
// Handle input-specific attributes first
|
|
118
|
-
if (["type", "placeholder", "disabled", "s"].includes(name)) {
|
|
136
|
+
if (["type", "placeholder", "disabled", "value", "step", "s"].includes(name)) {
|
|
119
137
|
this._updateInputAttributes();
|
|
120
138
|
return;
|
|
121
139
|
}
|
|
@@ -188,16 +206,28 @@ class RettangoliInputElement extends HTMLElement {
|
|
|
188
206
|
_updateInputAttributes() {
|
|
189
207
|
const type = this.getAttribute("type") || "text";
|
|
190
208
|
const placeholder = this.getAttribute("placeholder");
|
|
209
|
+
const value = this.getAttribute("value");
|
|
210
|
+
const step = this.getAttribute("step");
|
|
191
211
|
const isDisabled = this.hasAttribute('disabled');
|
|
192
212
|
|
|
193
213
|
this._inputElement.setAttribute("type", type);
|
|
194
|
-
|
|
214
|
+
|
|
195
215
|
if (placeholder !== null) {
|
|
196
216
|
this._inputElement.setAttribute("placeholder", placeholder);
|
|
197
217
|
} else {
|
|
198
218
|
this._inputElement.removeAttribute("placeholder");
|
|
199
219
|
}
|
|
200
|
-
|
|
220
|
+
|
|
221
|
+
if (value !== null) {
|
|
222
|
+
this._inputElement.value = value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (step !== null) {
|
|
226
|
+
this._inputElement.setAttribute("step", step);
|
|
227
|
+
} else {
|
|
228
|
+
this._inputElement.removeAttribute("step");
|
|
229
|
+
}
|
|
230
|
+
|
|
201
231
|
if (isDisabled) {
|
|
202
232
|
this._inputElement.setAttribute("disabled", "");
|
|
203
233
|
} else {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { css } from "../common.js";
|
|
2
|
+
|
|
3
|
+
class RettangoliPopoverElement extends HTMLElement {
|
|
4
|
+
static styleSheet = null;
|
|
5
|
+
|
|
6
|
+
static initializeStyleSheet() {
|
|
7
|
+
if (!RettangoliPopoverElement.styleSheet) {
|
|
8
|
+
RettangoliPopoverElement.styleSheet = new CSSStyleSheet();
|
|
9
|
+
RettangoliPopoverElement.styleSheet.replaceSync(css`
|
|
10
|
+
:host {
|
|
11
|
+
display: contents;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.popover-overlay {
|
|
15
|
+
position: fixed;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
width: 100vw;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
z-index: 999;
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.popover-container {
|
|
25
|
+
position: fixed;
|
|
26
|
+
z-index: 1000;
|
|
27
|
+
display: none;
|
|
28
|
+
outline: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:host([open]:not([no-overlay])) .popover-overlay {
|
|
32
|
+
display: block;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:host([open]) .popover-container {
|
|
36
|
+
display: block;
|
|
37
|
+
visibility: hidden;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* For no-overlay mode, make the container non-interactive */
|
|
41
|
+
:host([no-overlay]) .popover-container {
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:host([open][positioned]) .popover-container {
|
|
46
|
+
visibility: visible;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
slot[name="content"] {
|
|
50
|
+
display: block;
|
|
51
|
+
background-color: var(--muted);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: var(--border-radius-md);
|
|
54
|
+
padding: var(--spacing-md);
|
|
55
|
+
min-width: 200px;
|
|
56
|
+
max-width: 400px;
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
RettangoliPopoverElement.initializeStyleSheet();
|
|
65
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
66
|
+
this.shadow.adoptedStyleSheets = [RettangoliPopoverElement.styleSheet];
|
|
67
|
+
|
|
68
|
+
// Create overlay
|
|
69
|
+
this._popoverOverlay = document.createElement('div');
|
|
70
|
+
this._popoverOverlay.className = 'popover-overlay';
|
|
71
|
+
this.shadow.appendChild(this._popoverOverlay);
|
|
72
|
+
|
|
73
|
+
// Handle overlay clicks to close popover
|
|
74
|
+
this._popoverOverlay.addEventListener('click', () => {
|
|
75
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
76
|
+
detail: {}
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Handle right-click on overlay to close popover
|
|
81
|
+
this._popoverOverlay.addEventListener('contextmenu', (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
84
|
+
detail: {}
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create popover container
|
|
89
|
+
this._popoverContainer = document.createElement('div');
|
|
90
|
+
this._popoverContainer.className = 'popover-container';
|
|
91
|
+
this.shadow.appendChild(this._popoverContainer);
|
|
92
|
+
|
|
93
|
+
// Store reference for content slot
|
|
94
|
+
this._slotElement = null;
|
|
95
|
+
|
|
96
|
+
// Track if we're open
|
|
97
|
+
this._isOpen = false;
|
|
98
|
+
|
|
99
|
+
// Bind event handlers
|
|
100
|
+
this._handleEscKey = this._handleEscKey.bind(this);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static get observedAttributes() {
|
|
104
|
+
return ["open", "x", "y", "placement", "no-overlay"];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
connectedCallback() {
|
|
108
|
+
// Check initial open attribute
|
|
109
|
+
if (this.hasAttribute('open')) {
|
|
110
|
+
this._show();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnectedCallback() {
|
|
115
|
+
// Clean up event listeners
|
|
116
|
+
this._removeGlobalListeners();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
120
|
+
if (name === 'open') {
|
|
121
|
+
if (newValue !== null && !this._isOpen) {
|
|
122
|
+
this._show();
|
|
123
|
+
} else if (newValue === null && this._isOpen) {
|
|
124
|
+
this._hide();
|
|
125
|
+
}
|
|
126
|
+
} else if ((name === 'x' || name === 'y' || name === 'placement') && this._isOpen) {
|
|
127
|
+
this._updatePosition();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_show() {
|
|
132
|
+
if (!this._isOpen) {
|
|
133
|
+
// Create and append slot for content only if it doesn't exist
|
|
134
|
+
if (!this._slotElement) {
|
|
135
|
+
this._slotElement = document.createElement('slot');
|
|
136
|
+
this._slotElement.setAttribute('name', 'content');
|
|
137
|
+
this._popoverContainer.appendChild(this._slotElement);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this._isOpen = true;
|
|
141
|
+
this._updatePosition();
|
|
142
|
+
this._addGlobalListeners();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_hide() {
|
|
147
|
+
if (this._isOpen) {
|
|
148
|
+
this._isOpen = false;
|
|
149
|
+
|
|
150
|
+
// Remove slot to unmount content
|
|
151
|
+
if (this._slotElement) {
|
|
152
|
+
this._popoverContainer.removeChild(this._slotElement);
|
|
153
|
+
this._slotElement = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this._removeGlobalListeners();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_updatePosition() {
|
|
161
|
+
const x = parseFloat(this.getAttribute('x') || '0');
|
|
162
|
+
const y = parseFloat(this.getAttribute('y') || '0');
|
|
163
|
+
const placement = this.getAttribute('placement') || 'bottom-start';
|
|
164
|
+
|
|
165
|
+
// Remove positioned attribute to hide during repositioning
|
|
166
|
+
this.removeAttribute('positioned');
|
|
167
|
+
|
|
168
|
+
// Calculate position based on placement
|
|
169
|
+
// We'll position after the popover is rendered to get its dimensions
|
|
170
|
+
requestAnimationFrame(() => {
|
|
171
|
+
const rect = this._popoverContainer.getBoundingClientRect();
|
|
172
|
+
const { left, top } = this._calculatePosition(x, y, rect.width, rect.height, placement);
|
|
173
|
+
|
|
174
|
+
// Set position first
|
|
175
|
+
this._popoverContainer.style.left = `${left}px`;
|
|
176
|
+
this._popoverContainer.style.top = `${top}px`;
|
|
177
|
+
|
|
178
|
+
// Then make visible in next frame to prevent flicker
|
|
179
|
+
requestAnimationFrame(() => {
|
|
180
|
+
this.setAttribute('positioned', '');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_calculatePosition(x, y, width, height, placement) {
|
|
186
|
+
const offset = 8; // Small offset from the cursor
|
|
187
|
+
let left = x;
|
|
188
|
+
let top = y;
|
|
189
|
+
|
|
190
|
+
switch (placement) {
|
|
191
|
+
case 'top':
|
|
192
|
+
left = x - width / 2;
|
|
193
|
+
top = y - height - offset;
|
|
194
|
+
break;
|
|
195
|
+
case 'top-start':
|
|
196
|
+
left = x;
|
|
197
|
+
top = y - height - offset;
|
|
198
|
+
break;
|
|
199
|
+
case 'top-end':
|
|
200
|
+
left = x - width;
|
|
201
|
+
top = y - height - offset;
|
|
202
|
+
break;
|
|
203
|
+
case 'right':
|
|
204
|
+
left = x + offset;
|
|
205
|
+
top = y - height / 2;
|
|
206
|
+
break;
|
|
207
|
+
case 'right-start':
|
|
208
|
+
left = x + offset;
|
|
209
|
+
top = y;
|
|
210
|
+
break;
|
|
211
|
+
case 'right-end':
|
|
212
|
+
left = x + offset;
|
|
213
|
+
top = y - height;
|
|
214
|
+
break;
|
|
215
|
+
case 'bottom':
|
|
216
|
+
left = x - width / 2;
|
|
217
|
+
top = y + offset;
|
|
218
|
+
break;
|
|
219
|
+
case 'bottom-start':
|
|
220
|
+
left = x;
|
|
221
|
+
top = y + offset;
|
|
222
|
+
break;
|
|
223
|
+
case 'bottom-end':
|
|
224
|
+
left = x - width;
|
|
225
|
+
top = y + offset;
|
|
226
|
+
break;
|
|
227
|
+
case 'left':
|
|
228
|
+
left = x - width - offset;
|
|
229
|
+
top = y - height / 2;
|
|
230
|
+
break;
|
|
231
|
+
case 'left-start':
|
|
232
|
+
left = x - width - offset;
|
|
233
|
+
top = y;
|
|
234
|
+
break;
|
|
235
|
+
case 'left-end':
|
|
236
|
+
left = x - width - offset;
|
|
237
|
+
top = y - height;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Ensure popover stays within viewport
|
|
242
|
+
const padding = 8;
|
|
243
|
+
left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
|
|
244
|
+
top = Math.max(padding, Math.min(top, window.innerHeight - height - padding));
|
|
245
|
+
|
|
246
|
+
return { left, top };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_addGlobalListeners() {
|
|
250
|
+
// Use setTimeout to avoid immediate triggering
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
document.addEventListener('keydown', this._handleEscKey);
|
|
253
|
+
}, 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_removeGlobalListeners() {
|
|
257
|
+
document.removeEventListener('keydown', this._handleEscKey);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
_handleEscKey(e) {
|
|
262
|
+
if (e.key === 'Escape') {
|
|
263
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
264
|
+
detail: {}
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Expose popover container for advanced usage
|
|
270
|
+
get popover() {
|
|
271
|
+
return this._popoverContainer;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Export factory function to maintain API compatibility
|
|
276
|
+
export default ({ render, html }) => {
|
|
277
|
+
// Note: render and html parameters are accepted but not used
|
|
278
|
+
// This maintains backward compatibility with existing code
|
|
279
|
+
return RettangoliPopoverElement;
|
|
280
|
+
};
|