@csedl/hotwire-svelte-helpers 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/.idea/StimulusDropdown-NpmPackage.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +174 -0
- package/index.js +19 -0
- package/package.json +28 -0
- package/src/config.js +114 -0
- package/src/dropdown-controller.js +59 -0
- package/src/floating-ui-functions.js +106 -0
- package/src/move-panels-controller.js +99 -0
- package/src/tooltip-controller.js +62 -0
- package/src/utils.js +226 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/StimulusDropdown-NpmPackage.iml" filepath="$PROJECT_DIR$/.idea/StimulusDropdown-NpmPackage.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Stimulus Dropdown
|
|
2
|
+
|
|
3
|
+
Dropdown with stimulus, based on floating-UI.
|
|
4
|
+
|
|
5
|
+
**Links:**
|
|
6
|
+
- [Online Demo App](https://stimulus-dropdown.sedlmair.ch/)
|
|
7
|
+
- [Ruby Gem: csedl-stimulus-dropdown](https://gitlab.com/sedl/csedl-stimulus-dropdown)
|
|
8
|
+
- [How we are building a Rails App](https://dev.to/chmich/setup-vite-svelte-inertia-stimulus-bootstrap-foundation-on-rails-7-overview-1bk1)
|
|
9
|
+
|
|
10
|
+
## Import and config
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
import {StimulusDropdown} from "@csedl/stimulus-dropdown"
|
|
14
|
+
StimulusDropdown.debug = false
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
All configurations and their defaults are as follows:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
debug: false
|
|
21
|
+
closeButtonSelector: '.close-button'
|
|
22
|
+
dropdownContentSelector: '.content'
|
|
23
|
+
// when a data-src attribute is added to the panel, the content-tag is replaced by the result of the xhr-response
|
|
24
|
+
tooltipContentSelector: '.content'
|
|
25
|
+
// same as dropdownContentSelector
|
|
26
|
+
addArrow: true
|
|
27
|
+
// add element with id #arrow within the panel, on opening
|
|
28
|
+
persistTooltipOnClick: false
|
|
29
|
+
// clicking on the tooltip-label causes the tooltip-panel to persist open
|
|
30
|
+
// this may mostly be helpful for development, so you may make it environment-dependent
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
There is a [online example app](https://stimulus-dropdown.sedlmair.ch/)
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<div data-controller="csedl-dropdown" data-panel-id="dropdown-panel-3h5k7l4">
|
|
39
|
+
Button
|
|
40
|
+
</div>
|
|
41
|
+
<div id="dropdown-panel-3h5k7l4" class="hide dropdown-panel-example-class">
|
|
42
|
+
... any content
|
|
43
|
+
</div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What it does
|
|
47
|
+
|
|
48
|
+
- When the button is clicked, it toggles the `hide` class on the panel and places the panel using floating-ui.
|
|
49
|
+
- When the panel is open, the `has-open-panel` class is added to the button, otherwise it is removed.
|
|
50
|
+
- Adds functionality to close all panels when clicking outside a panel.
|
|
51
|
+
- When a `data-src` attribute is given to the panel, on opening the panel, it fires a xhr request and replaces the configured content-tags (see: content-selectors on configs) by the response.
|
|
52
|
+
- This all works with stacked panels too (panel in panel).
|
|
53
|
+
|
|
54
|
+
## Close on click outside
|
|
55
|
+
|
|
56
|
+
When a dropdown is open, it closes when clicking outside a panel.
|
|
57
|
+
|
|
58
|
+
This behaviour can be stopped on:
|
|
59
|
+
|
|
60
|
+
- The clicked element or its parent elements has the `data-dropdown-persist` (not: `data-dropdown-persist="false"`) attribute.
|
|
61
|
+
- the event has the attribute `event.detail.dataDropdownPersist` set to true.
|
|
62
|
+
|
|
63
|
+
## Flexibility
|
|
64
|
+
|
|
65
|
+
The functions of this package are intended to give flexibility on various ways building a dropdown.
|
|
66
|
+
|
|
67
|
+
- Stimulus Controller with Rails Helper
|
|
68
|
+
- Example: [Stimulus controller within this package](https://gitlab.com/sedl/csedl-stimulus-dropdown-js/-/blob/main/src/dropdown-controller.js?ref_type=heads)
|
|
69
|
+
- Stimulus with Svelte component
|
|
70
|
+
- Example: [Stimulus controller on example app](https://gitlab.com/sedl/stimulusfloatingdropdown/-/blob/main/app/frontend/javascript/svelte-dropdown-controller.js?ref_type=heads)
|
|
71
|
+
|
|
72
|
+
**Important:** When creating or initializing the dropdown,
|
|
73
|
+
always call the initialize function before attaching a listener to the panel's close event.
|
|
74
|
+
The initialize function adds a `close` event listener to the panel that executes the `onPanelClose` function.
|
|
75
|
+
If your custom close function destroys the panel, this has to be done after the `onPanelClose` is fired by the `close` event.
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- The class `dropdown-panel-example-class` must be set to `position: absolute;`.
|
|
80
|
+
- The class `hide` must be set to `display: none;`.
|
|
81
|
+
|
|
82
|
+
## Options
|
|
83
|
+
|
|
84
|
+
- If there is an element with ID `arrow` inside the dropdown panel, it is treated as described on [floating-ui](https://floating-ui.com/docs/arrow).
|
|
85
|
+
- The `data-placement` attribute on the panel can be used to control positioning, see [floating-ui/placements](https://floating-ui.com/docs/tutorial#placements).
|
|
86
|
+
|
|
87
|
+
## Events
|
|
88
|
+
|
|
89
|
+
Events on the button element:
|
|
90
|
+
|
|
91
|
+
- `place-panel` places the panel, and, if present, the arrow element, by `floating-ui`.
|
|
92
|
+
|
|
93
|
+
Events the panel element:
|
|
94
|
+
|
|
95
|
+
- `close` closes the panel.
|
|
96
|
+
- `place-me` like place-panel on the button.
|
|
97
|
+
|
|
98
|
+
Event Triggers on the button element:
|
|
99
|
+
|
|
100
|
+
- `before-open-panel`
|
|
101
|
+
- `after-close-panel`
|
|
102
|
+
|
|
103
|
+
Event Triggers on the panel element:
|
|
104
|
+
|
|
105
|
+
- `before-open`
|
|
106
|
+
|
|
107
|
+
## Helpers
|
|
108
|
+
|
|
109
|
+
If the panels are rendered to a different location than the button (see z-index on [rails-app](https://gitlab.com/sedl/stimulusfloatingdropdown)), within a scrollable (e.g.) container, the button would scroll away from the panel. For such cases, add this both data-attributes to the scrollable element:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<div data-controller="csedl-place-dropdown-panels" data-on="scroll" data-run-after="500" style="overflow: scroll;">
|
|
113
|
+
...
|
|
114
|
+
</div>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Now, on scrolling, it searches for all dropdown-buttons (by class-name `has-open-panel`) and triggers the `place-panel` event there.
|
|
118
|
+
|
|
119
|
+
**Options**
|
|
120
|
+
|
|
121
|
+
`data-on` Attribute:
|
|
122
|
+
|
|
123
|
+
- `scroll` triggered by `scroll` Event of the given element.
|
|
124
|
+
- `resize-observer` triggered by [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) on the given element.
|
|
125
|
+
|
|
126
|
+
`data-run-after` Attribute:
|
|
127
|
+
|
|
128
|
+
- Milliseconds as number.
|
|
129
|
+
|
|
130
|
+
This is only relevant if you have things like `css transition` enabled, so that after the above resize events are fired, subsequent events are needed. It will fire the `place-panel` after the last resize/scroll event within the given time.
|
|
131
|
+
|
|
132
|
+
**Tip** Turn `console-debug-log` on (see configs) and check how events are working.
|
|
133
|
+
|
|
134
|
+
**Explanation**
|
|
135
|
+
|
|
136
|
+
What these helpers mainly do is to find all the dropdowns by the `has-open-panel` class and fire the `place-panel` event. But within the helper, things like performance optimisation are done: it searches once and places the panels multiple times.
|
|
137
|
+
|
|
138
|
+
## Tooltip
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<span data-controller="csedl-tooltip" data-panel-id="tooltip-123" data-delay="0.2">
|
|
142
|
+
Text-with-tooltip
|
|
143
|
+
</span>
|
|
144
|
+
<div id="tooltip-123" class="hide tooltip-panel">
|
|
145
|
+
<div id="arrow"></div>
|
|
146
|
+
... any content
|
|
147
|
+
</div>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
makes a tooltip.
|
|
151
|
+
|
|
152
|
+
It adds the class `tooltip-is-visible` to the tooltip label while the tooltip is visible.
|
|
153
|
+
|
|
154
|
+
`data-src` attribute is working similar to dropdown
|
|
155
|
+
|
|
156
|
+
## Rails Helpers
|
|
157
|
+
|
|
158
|
+
There is a corresponding rails gem, on [GitLab](https://gitlab.com/sedl/csedl-stimulus-dropdown)
|
|
159
|
+
|
|
160
|
+
## Stimulus Usage in stimulus-dropdown
|
|
161
|
+
|
|
162
|
+
This package uses Stimulus unconventionally to initialize and toggle external dropdown or tooltip panels
|
|
163
|
+
(via data-panel-id) rather than managing child elements within the controller’s scope,
|
|
164
|
+
as is typical in Stimulus documentation.
|
|
165
|
+
|
|
166
|
+
The same result could be achieved using MutationObserver and plain JavaScript.
|
|
167
|
+
|
|
168
|
+
Stimulus was chosen because this package is intended for use with Hotwire/Turbo
|
|
169
|
+
where Stimulus already is installed. It has a modest footprint of just 10 KB.
|
|
170
|
+
It also holds many configs for MutationObserver, especially for our exact purpose.
|
|
171
|
+
|
|
172
|
+
**License**
|
|
173
|
+
|
|
174
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import StimulusDropdown from "./src/config.js";
|
|
3
|
+
|
|
4
|
+
import { Application } from "@hotwired/stimulus"
|
|
5
|
+
window.Stimulus = Application.start()
|
|
6
|
+
|
|
7
|
+
// initialize dropdowns
|
|
8
|
+
import dc from './src/dropdown-controller'
|
|
9
|
+
Stimulus.register('csedl-dropdown', dc)
|
|
10
|
+
|
|
11
|
+
// initialize tooltips
|
|
12
|
+
import ttc from './src/tooltip-controller'
|
|
13
|
+
Stimulus.register('csedl-tooltip', ttc)
|
|
14
|
+
|
|
15
|
+
// initialize place-panel controller
|
|
16
|
+
import ppc from './src/move-panels-controller'
|
|
17
|
+
Stimulus.register('csedl-move-panels', ppc)
|
|
18
|
+
|
|
19
|
+
export { StimulusDropdown }
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@csedl/hotwire-svelte-helpers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals + Rails form error mapping + Turbo-friendly utilities. Build together with the rubygem svelte-on-rails and its npm-package.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://gitlab.com/sedl/csedl-hotwire-svelte-helpers"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"Stimulus",
|
|
15
|
+
"Dropdown",
|
|
16
|
+
"Rails"
|
|
17
|
+
],
|
|
18
|
+
"author": "Christian Sedlmair",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://gitlab.com/sedl/csedl-stimulus-dropdown-js/-/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://gitlab.com/sedl/csedl-stimulus-dropdown-js",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@floating-ui/dom": ">=1.5.1",
|
|
26
|
+
"@hotwired/stimulus": ">=3.2.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {debugLog} from "./utils.js";
|
|
2
|
+
|
|
3
|
+
// DEFAULTS
|
|
4
|
+
let _debug = false;
|
|
5
|
+
let _close_button_selector = '.close-button';
|
|
6
|
+
let _dropdown_content_selector = '.content';
|
|
7
|
+
let _tooltip_content_selector = '.content';
|
|
8
|
+
let _add_arrow = true;
|
|
9
|
+
let _persist_tooltip_on_click = false;
|
|
10
|
+
|
|
11
|
+
let _close_on_click_outside_listener_added = false;
|
|
12
|
+
|
|
13
|
+
const StimulusDropdown = {
|
|
14
|
+
|
|
15
|
+
// debug
|
|
16
|
+
|
|
17
|
+
get debug() {
|
|
18
|
+
return _debug;
|
|
19
|
+
},
|
|
20
|
+
set debug(value) {
|
|
21
|
+
if (typeof value !== "boolean") {
|
|
22
|
+
throw new Error("Debug value must be true or false");
|
|
23
|
+
}
|
|
24
|
+
_debug = value;
|
|
25
|
+
debugLog('debugging active')
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// close button selector
|
|
29
|
+
|
|
30
|
+
get closeButtonSelector() {
|
|
31
|
+
return _close_button_selector;
|
|
32
|
+
},
|
|
33
|
+
set closeButtonSelector(string) {
|
|
34
|
+
if (typeof string !== "string") {
|
|
35
|
+
throw new Error("Close Button Selector must be a string");
|
|
36
|
+
}
|
|
37
|
+
_close_button_selector = string;
|
|
38
|
+
debugLog(`close button selector is: «${string}»`);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// content selector
|
|
42
|
+
|
|
43
|
+
get dropdownContentSelector() {
|
|
44
|
+
return _dropdown_content_selector;
|
|
45
|
+
},
|
|
46
|
+
set dropdownContentSelector(string) {
|
|
47
|
+
if (typeof string !== "string") {
|
|
48
|
+
throw new Error("Content Selector must be a string");
|
|
49
|
+
}
|
|
50
|
+
_dropdown_content_selector = string;
|
|
51
|
+
debugLog(`dropdown content selector is: «${string}»`);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// tooltip content selector
|
|
55
|
+
|
|
56
|
+
get tooltipContentSelector() {
|
|
57
|
+
return _tooltip_content_selector;
|
|
58
|
+
},
|
|
59
|
+
set tooltipContentSelector(string) {
|
|
60
|
+
if (typeof string !== "string") {
|
|
61
|
+
throw new Error("Tooltip Content Selector must be a string");
|
|
62
|
+
}
|
|
63
|
+
_tooltip_content_selector = string;
|
|
64
|
+
debugLog(`tooltip content selector is: «${string}»`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// add arrow
|
|
68
|
+
|
|
69
|
+
get addArrow() {
|
|
70
|
+
return _add_arrow;
|
|
71
|
+
},
|
|
72
|
+
set addArrow(boolean) {
|
|
73
|
+
if (typeof boolean !== "boolean") {
|
|
74
|
+
throw new Error("Tooltip Content Selector must be a string");
|
|
75
|
+
}
|
|
76
|
+
_add_arrow = boolean;
|
|
77
|
+
debugLog(`add-arrow is set to: «${boolean}»`);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// add arrow
|
|
81
|
+
|
|
82
|
+
get persistTooltipOnClick() {
|
|
83
|
+
return _persist_tooltip_on_click;
|
|
84
|
+
},
|
|
85
|
+
set persistTooltipOnClick(boolean) {
|
|
86
|
+
if (typeof boolean !== "boolean") {
|
|
87
|
+
throw new Error("Tooltip Content Selector must be a string");
|
|
88
|
+
}
|
|
89
|
+
_persist_tooltip_on_click = boolean;
|
|
90
|
+
debugLog(`Persist tooltip on click is set to: «${boolean}»`);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
94
|
+
// INTERNAL USE
|
|
95
|
+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
96
|
+
|
|
97
|
+
// closeOnClickOutsideListenerAdded
|
|
98
|
+
|
|
99
|
+
get closeOnClickOutsideListenerAdded() {
|
|
100
|
+
return _close_on_click_outside_listener_added;
|
|
101
|
+
},
|
|
102
|
+
set closeOnClickOutsideListenerAdded(bool) {
|
|
103
|
+
if (typeof bool !== "boolean") {
|
|
104
|
+
throw new Error("closeOnClickOutsideListenerAdded must be a boolean");
|
|
105
|
+
}
|
|
106
|
+
_close_on_click_outside_listener_added = bool;
|
|
107
|
+
debugLog(`closeOnClickOutsideListenerAdded is: «${bool}»`);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
window.StimulusDropdown = StimulusDropdown;
|
|
113
|
+
|
|
114
|
+
export default StimulusDropdown;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {Controller} from "@hotwired/stimulus"
|
|
2
|
+
import {initializeDropdown, onPanelOpen} from "./utils.js";
|
|
3
|
+
import {debugLog} from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
|
|
9
|
+
initializeDropdown(this.element)
|
|
10
|
+
|
|
11
|
+
this.element.addEventListener('click', (e) => this.toggle(e))
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
toggle(e) {
|
|
17
|
+
if (this.element.getAttribute('data-prevent-default')) {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
}
|
|
20
|
+
e.stopPropagation()
|
|
21
|
+
debugLog('toggle panel', e)
|
|
22
|
+
const target_id = this.element.getAttribute('data-panel-id')
|
|
23
|
+
const panel = document.getElementById(target_id)
|
|
24
|
+
if (!panel) {
|
|
25
|
+
console.error(`Panel-element with ID ${target_id} not found`)
|
|
26
|
+
} else if (panel.style.display === 'block') {
|
|
27
|
+
this.close(panel)
|
|
28
|
+
} else {
|
|
29
|
+
this.open(e, panel)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
open(e, panel) {
|
|
34
|
+
|
|
35
|
+
// open the panel
|
|
36
|
+
|
|
37
|
+
panel.style.display = 'block';
|
|
38
|
+
debugLog('opened panel:', panel)
|
|
39
|
+
|
|
40
|
+
onPanelOpen(e, this.element)
|
|
41
|
+
|
|
42
|
+
panel.addEventListener('close', () => this.close(panel))
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
close(panel) {
|
|
47
|
+
|
|
48
|
+
// Close actions
|
|
49
|
+
|
|
50
|
+
panel.style.display = 'none';
|
|
51
|
+
debugLog('panel closed:', panel)
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {arrow, computePosition, flip, offset, shift} from "@floating-ui/dom";
|
|
2
|
+
import {debugLog} from "./utils.js";
|
|
3
|
+
|
|
4
|
+
// Positions a panel relative to its trigger button
|
|
5
|
+
export function positionPanelByButton(button, logContext) {
|
|
6
|
+
const toggleId = button.getAttribute('data-panel-id')
|
|
7
|
+
let panel = document.getElementById(toggleId)
|
|
8
|
+
positionPanel(button, panel)
|
|
9
|
+
debugLog('panel positioned', logContext)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Positions a panel using its event listener
|
|
13
|
+
export function positionPanelSelf(panel) {
|
|
14
|
+
let button = document.querySelector(`[data-panel-id="${panel.id}"]`)
|
|
15
|
+
positionPanel(button, panel)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Retrieves all currently open panels within a scope element
|
|
19
|
+
export function getAllOpenPanels(scopeElement) {
|
|
20
|
+
const buttons = scopeElement.getElementsByClassName('has-open-panel')
|
|
21
|
+
const elements = []
|
|
22
|
+
|
|
23
|
+
for (const button of buttons) {
|
|
24
|
+
if (button.getAttribute('data-controller') === 'csedl-dropdown') {
|
|
25
|
+
const getOrSetPanelId = button.getAttribute('data-panel-id')
|
|
26
|
+
const panel = document.getElementById(getOrSetPanelId)
|
|
27
|
+
elements.push([button, panel])
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debugLog(`FOUND ${elements.length} OPEN DROPDOWNS`)
|
|
32
|
+
|
|
33
|
+
return (elements)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Positions all provided panel-button pairs
|
|
37
|
+
export function positionAllPanels(elements) {
|
|
38
|
+
for (const el of elements) {
|
|
39
|
+
positionPanel(el[0], el[1])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Positions a single panel relative to its button
|
|
44
|
+
export function positionPanel(button, panel) {
|
|
45
|
+
let arrowElement = panel.querySelector('#arrow')
|
|
46
|
+
panel.style.removeProperty('height')
|
|
47
|
+
panel.style.removeProperty('left')
|
|
48
|
+
panel.style.removeProperty('top')
|
|
49
|
+
let placementAttr = panel.getAttribute('data-placement')
|
|
50
|
+
let placement = (placementAttr ? placementAttr : 'bottom')
|
|
51
|
+
debugLog(`placement: «${placement}»`)
|
|
52
|
+
|
|
53
|
+
computePosition(button, panel, {
|
|
54
|
+
middleware: [flip(), offset(6), shift({padding: 5}), arrow({element: arrowElement})],
|
|
55
|
+
placement: placement
|
|
56
|
+
}).then(({x, y, placement, middlewareData}) => {
|
|
57
|
+
Object.assign(panel.style, {
|
|
58
|
+
left: `${x}px`,
|
|
59
|
+
top: `${y}px`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (arrowElement) {
|
|
63
|
+
// ARROW
|
|
64
|
+
const {x: arrowX, y: arrowY} = middlewareData.arrow;
|
|
65
|
+
const staticSide = {
|
|
66
|
+
top: 'bottom',
|
|
67
|
+
right: 'left',
|
|
68
|
+
bottom: 'top',
|
|
69
|
+
left: 'right',
|
|
70
|
+
}[placement.split('-')[0]];
|
|
71
|
+
Object.assign(arrowElement.style, {
|
|
72
|
+
left: arrowX != null ? `${arrowX}px` : '',
|
|
73
|
+
top: arrowY != null ? `${arrowY}px` : '',
|
|
74
|
+
right: '',
|
|
75
|
+
bottom: '',
|
|
76
|
+
[staticSide]: '-4px',
|
|
77
|
+
});
|
|
78
|
+
debugLog(`panel + arrow positioned`)
|
|
79
|
+
} else {
|
|
80
|
+
debugLog(`panel positioned`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
adjustToWindowBounds(button, panel)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Adjusts panel to prevent overflow beyond window boundaries
|
|
88
|
+
function adjustToWindowBounds(button, panel) {
|
|
89
|
+
if (button.getAttribute('data-constrain-to-window-borders') === 'true') {
|
|
90
|
+
const rect = panel.getBoundingClientRect();
|
|
91
|
+
|
|
92
|
+
if (rect.top < 0) {
|
|
93
|
+
const newTop = parseFloat(panel.style.top) + Math.abs(rect.top) + 2
|
|
94
|
+
console.log('constrain top', 'actual top:', panel.style.top, 'absolute top:', rect.top, 'new top:', newTop)
|
|
95
|
+
panel.style.top = `${newTop}px`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rect2 = panel.getBoundingClientRect();
|
|
99
|
+
|
|
100
|
+
if (rect2.bottom > window.innerHeight) {
|
|
101
|
+
console.log('constrain height')
|
|
102
|
+
const height = window.innerHeight - rect2.top - 2
|
|
103
|
+
panel.style.height = `${height}px`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {Controller} from "@hotwired/stimulus"
|
|
2
|
+
import {positionAllPanels, getAllOpenPanels} from "./floating-ui-functions";
|
|
3
|
+
import {debugLog} from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
|
|
7
|
+
elements_to_place = null
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
const on = this.element.getAttribute('data-on')
|
|
11
|
+
|
|
12
|
+
if (on.includes('scroll')) {
|
|
13
|
+
this.element.addEventListener('scroll', () => {
|
|
14
|
+
this.handle_placement('scroll')
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (on.includes('resize-observer')) {
|
|
19
|
+
|
|
20
|
+
var resize_observer = new ResizeObserver(entries => {
|
|
21
|
+
for (let entry of entries) {
|
|
22
|
+
this.handle_placement('resize-observer')
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
resize_observer.observe(this.element)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
handle_placement(trigger_label) {
|
|
31
|
+
if (this.elements_to_place === null) {
|
|
32
|
+
this.elements_to_place = getAllOpenPanels(this.element)
|
|
33
|
+
this.element.setAttribute('csedl-place-all-first-done-time', performance.now())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (this.elements_to_place.length === 0) {
|
|
37
|
+
this.cleanup()
|
|
38
|
+
|
|
39
|
+
} else {
|
|
40
|
+
const c = Number(this.element.getAttribute('csedl-place-all-counter'))
|
|
41
|
+
this.element.setAttribute('csedl-place-all-counter', c + 1)
|
|
42
|
+
const first_run = Number(this.element.getAttribute('csedl-place-all-first-done-time'))
|
|
43
|
+
|
|
44
|
+
debugLog(`place panels by ${trigger_label} ${c} / ${performance.now() - first_run}ms: x: ${this.element.offsetWidth} y: ${this.element.offsetHeight}`)
|
|
45
|
+
positionAllPanels(this.elements_to_place)
|
|
46
|
+
this.element.setAttribute('csedl-place-all-done-time', performance.now())
|
|
47
|
+
this.setup_run_after_event()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setup_run_after_event() {
|
|
52
|
+
if (this.element.hasAttribute('data-run-after')) {
|
|
53
|
+
const run_after_str = this.element.getAttribute('data-run-after')
|
|
54
|
+
const run_after = Number(run_after_str)
|
|
55
|
+
if (isNaN(run_after)) {
|
|
56
|
+
console.error(`the data-run-after attribute must be a valid positive number (integer for milliseconds) but is: «${run_after_str}»`, this.element)
|
|
57
|
+
} else if (run_after < 0.0) {
|
|
58
|
+
console.error(`the data-run-after attribute must be a positive number (integer for milliseconds) but is: «${run_after_str}»`, this.element)
|
|
59
|
+
} else {
|
|
60
|
+
if (this.element.getAttribute('csedl-run-after-is-active')) {
|
|
61
|
+
debugLog('place-panel run-after is already installed (nothing to do)')
|
|
62
|
+
} else {
|
|
63
|
+
this.element.setAttribute('csedl-run-after-is-active', true)
|
|
64
|
+
this.element.setAttribute('csedl-place-all-counter', 0)
|
|
65
|
+
setTimeout(() => this.handle_placement_after(), 50);
|
|
66
|
+
debugLog('place-panel run-after INSTALLED')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handle_placement_after() {
|
|
73
|
+
const run_after = Number(this.element.getAttribute('data-run-after'))
|
|
74
|
+
const last_run = Number(this.element.getAttribute('csedl-place-all-done-time'))
|
|
75
|
+
const c = Number(this.element.getAttribute('csedl-place-all-counter'))
|
|
76
|
+
this.element.setAttribute('csedl-place-all-counter', c + 1)
|
|
77
|
+
const running_time = performance.now() - last_run
|
|
78
|
+
|
|
79
|
+
positionAllPanels(this.elements_to_place)
|
|
80
|
+
debugLog(`PLACEMENT FOLLOW-UP ${c}: ${running_time}ms / ${run_after}ms`)
|
|
81
|
+
|
|
82
|
+
if (running_time < run_after) {
|
|
83
|
+
setTimeout(() => this.handle_placement_after(), 50);
|
|
84
|
+
|
|
85
|
+
} else {
|
|
86
|
+
this.cleanup()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
cleanup() {
|
|
91
|
+
this.element.removeAttribute('csedl-place-all-done-time')
|
|
92
|
+
this.element.removeAttribute('csedl-place-all-first-done-time')
|
|
93
|
+
this.element.removeAttribute('csedl-place-all-counter')
|
|
94
|
+
this.element.removeAttribute('csedl-run-after-is-active')
|
|
95
|
+
this.elements_to_place = null
|
|
96
|
+
debugLog('FINISHED placement follow-up')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {Controller} from "@hotwired/stimulus"
|
|
2
|
+
import {positionPanelByButton} from "./floating-ui-functions.js";
|
|
3
|
+
import {debugLog, onPanelOpen} from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
|
|
7
|
+
opening_timer_id = null
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
const panel_id = this.element.getAttribute('data-panel-id')
|
|
11
|
+
if (!panel_id) {
|
|
12
|
+
console.error(`Attribute data-panel-id missing`, this.element)
|
|
13
|
+
}
|
|
14
|
+
const delay_sec = parseFloat(this.element.getAttribute('data-delay'))
|
|
15
|
+
if (!delay_sec) {
|
|
16
|
+
console.error('missing required attribute: data-delay', this.element)
|
|
17
|
+
}
|
|
18
|
+
this.element.addEventListener('mouseenter', (e) => this.start_opening(e, panel_id, delay_sec * 1000))
|
|
19
|
+
this.element.addEventListener('mouseleave', (e) => this.close(e, panel_id))
|
|
20
|
+
if (window.StimulusDropdown.persistTooltipOnClick) {
|
|
21
|
+
this.element.addEventListener('click', (e) => this.toggleByClick(e, panel_id))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
start_opening(e, panel_id, delay_ms) {
|
|
26
|
+
if (this.element.hasAttribute('data-tooltip-click')) { return; }
|
|
27
|
+
this.opening_timer_id = setTimeout(() => this.open(e, panel_id), delay_ms)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
toggleByClick(e, panel_id) {
|
|
31
|
+
const stat = this.element.getAttribute('data-tooltip-click')
|
|
32
|
+
if (stat === 'open') {
|
|
33
|
+
debugLog('closing by click')
|
|
34
|
+
this.element.removeAttribute('data-tooltip-click')
|
|
35
|
+
this.close(e, panel_id)
|
|
36
|
+
} else {
|
|
37
|
+
debugLog('opening by click')
|
|
38
|
+
this.open(e, panel_id)
|
|
39
|
+
this.element.setAttribute('data-tooltip-click', 'open')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
open(e, panel_id) {
|
|
44
|
+
this.element.classList.add('tooltip-is-visible')
|
|
45
|
+
onPanelOpen(e, this.element)
|
|
46
|
+
this.opening_timer_id = null
|
|
47
|
+
let panel = document.getElementById(panel_id)
|
|
48
|
+
panel.style.display = 'block';
|
|
49
|
+
positionPanelByButton(this.element)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
close(e, panel_id) {
|
|
53
|
+
if (this.element.hasAttribute('data-tooltip-click')) { return; }
|
|
54
|
+
this.element.classList.remove('tooltip-is-visible')
|
|
55
|
+
if (this.opening_timer_id) {
|
|
56
|
+
clearTimeout(this.opening_timer_id)
|
|
57
|
+
}
|
|
58
|
+
let panel = document.getElementById(panel_id)
|
|
59
|
+
panel.style.display = 'none';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {positionPanelSelf, positionPanelByButton} from "./floating-ui-functions.js";
|
|
2
|
+
|
|
3
|
+
// Fetch content from server based on panel's data-src attribute and update content
|
|
4
|
+
export function onPanelOpen(event, button) {
|
|
5
|
+
|
|
6
|
+
const panel = findPanel(button);
|
|
7
|
+
|
|
8
|
+
debugLog('ON-PANEL-OPEN FUNCTIONS')
|
|
9
|
+
|
|
10
|
+
closeOnOutsideClick(event)
|
|
11
|
+
|
|
12
|
+
// add arrow
|
|
13
|
+
if (window.StimulusDropdown.addArrow) {
|
|
14
|
+
if (!panel.querySelector(':scope > #arrow')) {
|
|
15
|
+
const arrowTag = document.createElement('div');
|
|
16
|
+
arrowTag.id = 'arrow';
|
|
17
|
+
panel.appendChild(arrowTag)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
positionPanelByButton(button);
|
|
22
|
+
|
|
23
|
+
// Set focus to input element
|
|
24
|
+
if (panel.hasAttribute('data-set-focus')) {
|
|
25
|
+
let dataFocus = panel.getAttribute('data-set-focus');
|
|
26
|
+
let focusElement = panel.querySelector(dataFocus);
|
|
27
|
+
focusElement.focus();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Set status attribute
|
|
31
|
+
panel.setAttribute('data-stimulus-dropdown-panel-status', 'open');
|
|
32
|
+
button.classList.add('has-open-panel');
|
|
33
|
+
|
|
34
|
+
// Dispatch events
|
|
35
|
+
const panelOpenEvent = new CustomEvent('before-open');
|
|
36
|
+
panel.dispatchEvent(panelOpenEvent);
|
|
37
|
+
|
|
38
|
+
const buttonOpenEvent = new CustomEvent('before-open-panel');
|
|
39
|
+
button.dispatchEvent(buttonOpenEvent);
|
|
40
|
+
|
|
41
|
+
// Add listener for closing on outside click
|
|
42
|
+
if (!window.StimulusDropdown.closeOnClickOutsideListenerAdded) {
|
|
43
|
+
window.StimulusDropdown.closeOnClickOutsideListenerAdded = true;
|
|
44
|
+
window.addEventListener('click', closeOnOutsideClick);
|
|
45
|
+
debugLog('Listener for close panels on click outside added');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// fetch content from server
|
|
49
|
+
const src = panel.getAttribute('data-src');
|
|
50
|
+
if (src) {
|
|
51
|
+
let xhr = new XMLHttpRequest();
|
|
52
|
+
debugLog(`Panel / data-src: «${src}»`);
|
|
53
|
+
xhr.open('GET', src);
|
|
54
|
+
xhr.send();
|
|
55
|
+
xhr.onload = function () {
|
|
56
|
+
if (xhr.status !== 200) {
|
|
57
|
+
alert(`Dropdown Controller, GET «${src}», Error ${xhr.status}: ${xhr.statusText}`);
|
|
58
|
+
} else {
|
|
59
|
+
const button = document.querySelector(`[data-panel-id="${panel.id}"]`);
|
|
60
|
+
const ctrl = button.getAttribute('data-controller');
|
|
61
|
+
const config = window.StimulusDropdown;
|
|
62
|
+
const contentSelector = config.dropdownContentSelector;
|
|
63
|
+
|
|
64
|
+
if (ctrl === 'csedl-dropdown' && contentSelector) {
|
|
65
|
+
debugLog(`dropdown / contentSelector: «${contentSelector}»`);
|
|
66
|
+
const wrapper = panel.querySelector(contentSelector);
|
|
67
|
+
wrapper.innerHTML = xhr.response;
|
|
68
|
+
} else if (ctrl === 'csedl-tooltip' && contentSelector) {
|
|
69
|
+
debugLog(`tooltip / contentSelector: «${contentSelector}»`);
|
|
70
|
+
const wrapper = panel.querySelector(config.tooltipContentSelector);
|
|
71
|
+
wrapper.innerHTML = xhr.response;
|
|
72
|
+
} else {
|
|
73
|
+
debugLog(`? / contentSelector: «${contentSelector}»`);
|
|
74
|
+
panel.innerHTML = xhr.response;
|
|
75
|
+
console.error('fallback to replace whole panel');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
positionPanelByButton(button, `after http-request "${ctrl}"`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
} else {
|
|
82
|
+
debugLog('no data-src attribute provided on panel');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle panel closing
|
|
87
|
+
export function onPanelClose(button) {
|
|
88
|
+
|
|
89
|
+
const panel = findPanel(button);
|
|
90
|
+
|
|
91
|
+
debugLog('ON-PANEL-CLOSE FUNCTIONS')
|
|
92
|
+
|
|
93
|
+
// Update status attributes
|
|
94
|
+
button.classList.remove('has-open-panel');
|
|
95
|
+
panel.setAttribute('data-stimulus-dropdown-panel-status', 'closed');
|
|
96
|
+
|
|
97
|
+
// Remove outside click listener if no panels are open
|
|
98
|
+
const openPanels = document.querySelectorAll("[data-stimulus-dropdown-panel-status='open']");
|
|
99
|
+
debugLog(`panel closed, still open: ${openPanels.length}`);
|
|
100
|
+
if (window.StimulusDropdown.closeOnClickOutsideListenerAdded && openPanels.length === 0) {
|
|
101
|
+
window.StimulusDropdown.closeOnClickOutsideListenerAdded = false;
|
|
102
|
+
window.removeEventListener('click', closeOnOutsideClick);
|
|
103
|
+
debugLog('Listener for close panels on click outside removed');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Dispatch close event
|
|
107
|
+
let buttonCloseEvent = new CustomEvent('after-close-panel');
|
|
108
|
+
button.dispatchEvent(buttonCloseEvent);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Close panels when clicking outside
|
|
112
|
+
export function closeOnOutsideClick(ev) {
|
|
113
|
+
|
|
114
|
+
let btn = ev.target;
|
|
115
|
+
let parentPanelIds = [];
|
|
116
|
+
|
|
117
|
+
const persistElement = ev.target.closest('[data-dropdown-persist]');
|
|
118
|
+
if (persistElement) {
|
|
119
|
+
if (persistElement.getAttribute('data-dropdown-persist') !== 'false') {
|
|
120
|
+
debugLog('closing panel prevented because the target element has attribute "data-dropdown-persist"');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
debugLog('closeOnOutsideClick called, event.target:', ev.target);
|
|
126
|
+
|
|
127
|
+
while (true) {
|
|
128
|
+
const parentPanel = btn.closest("[data-stimulus-dropdown-panel-status='open']");
|
|
129
|
+
if (parentPanel) {
|
|
130
|
+
parentPanelIds.push(parentPanel.id);
|
|
131
|
+
btn = document.querySelector(`[data-panel-id="${parentPanel.id}"]`);
|
|
132
|
+
} else {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const openPanels = document.querySelectorAll("[data-stimulus-dropdown-panel-status='open']");
|
|
138
|
+
for (const panel of openPanels) {
|
|
139
|
+
if (!parentPanelIds.includes(panel.id)) {
|
|
140
|
+
const ev = new Event('close');
|
|
141
|
+
panel.dispatchEvent(ev);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add panel functionality, including close event listeners
|
|
147
|
+
export function initializeDropdown(button) {
|
|
148
|
+
|
|
149
|
+
// find panel
|
|
150
|
+
const panel_id = button.getAttribute('data-panel-id');
|
|
151
|
+
if (!panel_id) {
|
|
152
|
+
throw new Error(`data-panel-id attribute is not defined for given dropdown button`);
|
|
153
|
+
}
|
|
154
|
+
const panel = document.getElementById(panel_id);
|
|
155
|
+
if (!panel) {
|
|
156
|
+
throw new Error(`panel element not found by ID: «${panel_id}»`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add close button functionality
|
|
160
|
+
const selector = window.StimulusDropdown.closeButtonSelector;
|
|
161
|
+
const closeButtons = panel.querySelectorAll(selector);
|
|
162
|
+
for (const btn of closeButtons) {
|
|
163
|
+
btn.addEventListener('click', () => {
|
|
164
|
+
const ev = new Event('close');
|
|
165
|
+
panel.dispatchEvent(ev);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add event listeners to panel
|
|
170
|
+
panel.addEventListener('place-me', () => positionPanelSelf(panel));
|
|
171
|
+
button.addEventListener('place-panel', () => positionPanelByButton(button))
|
|
172
|
+
panel.addEventListener('close', () => onPanelClose(button))
|
|
173
|
+
|
|
174
|
+
debugLog(`Initialized dropdown, panel-ID: «${panel_id}»`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
178
|
+
// LOGGER
|
|
179
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
180
|
+
|
|
181
|
+
export function debugLog(message, object) {
|
|
182
|
+
if (window.StimulusDropdown.debug) {
|
|
183
|
+
|
|
184
|
+
if (object) {
|
|
185
|
+
console.log(`[stimulus-dropdown] ${message}`, object);
|
|
186
|
+
|
|
187
|
+
} else {
|
|
188
|
+
console.log(`[stimulus-dropdown] ${message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
195
|
+
// ADDITIONAL FUNCTIONS
|
|
196
|
+
// utils that are not used for the primary functions of this package itself, but helpful for building associated functions
|
|
197
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
// find or add the data-panel-id attribute of a button and return it
|
|
201
|
+
export function getOrSetPanelId(buttonTag) {
|
|
202
|
+
let id = buttonTag.getAttribute('data-panel-id')
|
|
203
|
+
if (id) {
|
|
204
|
+
return (id)
|
|
205
|
+
} else {
|
|
206
|
+
id = `dropdown-panel-${generateRandomHex(8)}`
|
|
207
|
+
buttonTag.setAttribute('data-panel-id', id)
|
|
208
|
+
return id
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
function generateRandomHex(n) {
|
|
214
|
+
if (n <= 0) return '';
|
|
215
|
+
return Array.from({length: n}, () =>
|
|
216
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
217
|
+
).join('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function findPanel(button) {
|
|
221
|
+
const panel = document.getElementById(button.getAttribute('data-panel-id'))
|
|
222
|
+
if (!panel) {
|
|
223
|
+
throw new Error(`dropdown button not found by attribute data-panel-id: «${button.getAttribute('data-panel-id')}»`)
|
|
224
|
+
}
|
|
225
|
+
return(panel)
|
|
226
|
+
}
|