@csedl/hotwire-svelte-helpers 0.1.0 → 1.0.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/dictionaries/project.xml +7 -0
- package/README.md +34 -29
- package/index.js +8 -15
- package/package.json +9 -5
- package/src/lib/config.js +85 -0
- package/src/{floating-ui-functions.js → lib/floating-ui-functions.js} +4 -3
- package/src/lib/type-validators.js +34 -0
- package/src/lib/utils.js +277 -0
- package/src/stimulus/dropdown-controller.js +34 -0
- package/src/{move-panels-controller.js → stimulus/move-panels-controller.js} +2 -2
- package/src/{tooltip-controller.js → stimulus/tooltip-controller.js} +4 -4
- package/src/svelte/cleanMount.js +80 -0
- package/src/config.js +0 -114
- package/src/dropdown-controller.js +0 -59
- package/src/utils.js +0 -226
package/README.md
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hotwire Svelte Helpers
|
|
2
|
+
|
|
3
|
+
is the successor of the `@csedl/stimulus-dropdown` package
|
|
2
4
|
|
|
3
5
|
Dropdown with stimulus, based on floating-UI.
|
|
4
6
|
|
|
5
7
|
**Links:**
|
|
6
|
-
- [Online Demo App](https://
|
|
7
|
-
- [Ruby Gem: csedl-stimulus-
|
|
8
|
+
- [Online Demo App](https://hotwire-svelte-helpers.sedlmair.ch/)
|
|
9
|
+
- [Ruby Gem: csedl-stimulus-dropdownSvelte](https://gitlab.com/sedl/csedl-stimulus-dropdown)
|
|
8
10
|
- [How we are building a Rails App](https://dev.to/chmich/setup-vite-svelte-inertia-stimulus-bootstrap-foundation-on-rails-7-overview-1bk1)
|
|
9
11
|
|
|
10
12
|
## Import and config
|
|
11
13
|
|
|
12
14
|
```javascript
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
+
import {HotwireSvelteHelpers} from "@csedl/hotwire-svelte-helpers"
|
|
16
|
+
HotwireSvelteHelpers.debug = true
|
|
17
|
+
HotwireSvelteHelpers.initializeOverlays()
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
All configurations and their defaults are as follows:
|
|
18
21
|
|
|
19
22
|
```javascript
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
HotwireSvelteHelpers.initializeOverlays({
|
|
24
|
+
debug: false,
|
|
25
|
+
closeButtonSelector: '.close-button',
|
|
26
|
+
dropdownContentSelector: '.content',
|
|
27
|
+
// when a data-src attribute is added to the panel, the content-tag is replaced by the result of the xhr-response
|
|
28
|
+
tooltipContentSelector: '.content',
|
|
29
|
+
// same as dropdownContentSelector
|
|
30
|
+
addArrow: true,
|
|
31
|
+
// add element with id #arrow within the panel, on opening
|
|
32
|
+
persistTooltipOnClick: false
|
|
33
|
+
// clicking on the tooltip-label causes the tooltip-panel to persist open
|
|
34
|
+
// this may mostly be helpful for development, so you may make it environment-dependent
|
|
35
|
+
})
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
## Example
|
|
34
39
|
|
|
35
|
-
There is a [online example app](https://
|
|
40
|
+
There is a [online example app](https://hotwire-svelte-helpers.sedlmair.ch/)
|
|
36
41
|
|
|
37
42
|
```html
|
|
38
43
|
<div data-controller="csedl-dropdown" data-panel-id="dropdown-panel-3h5k7l4">
|
|
39
44
|
Button
|
|
40
45
|
</div>
|
|
41
|
-
<div id="dropdown-panel-3h5k7l4" class="hide
|
|
46
|
+
<div id="dropdown-panel-3h5k7l4" class="hide dropdownSvelte-panel-example-class">
|
|
42
47
|
... any content
|
|
43
48
|
</div>
|
|
44
49
|
```
|
|
@@ -53,35 +58,35 @@ There is a [online example app](https://stimulus-dropdown.sedlmair.ch/)
|
|
|
53
58
|
|
|
54
59
|
## Close on click outside
|
|
55
60
|
|
|
56
|
-
When a
|
|
61
|
+
When a dropdownSvelte is open, it closes when clicking outside a panel.
|
|
57
62
|
|
|
58
63
|
This behaviour can be stopped on:
|
|
59
64
|
|
|
60
|
-
- The clicked element or its parent elements has the `data-
|
|
65
|
+
- The clicked element or its parent elements has the `data-dropdownSvelte-persist` (not: `data-dropdownSvelte-persist="false"`) attribute.
|
|
61
66
|
- the event has the attribute `event.detail.dataDropdownPersist` set to true.
|
|
62
67
|
|
|
63
68
|
## Flexibility
|
|
64
69
|
|
|
65
|
-
The functions of this package are intended to give flexibility on various ways building a
|
|
70
|
+
The functions of this package are intended to give flexibility on various ways building a dropdownSvelte.
|
|
66
71
|
|
|
67
72
|
- Stimulus Controller with Rails Helper
|
|
68
73
|
- Example: [Stimulus controller within this package](https://gitlab.com/sedl/csedl-stimulus-dropdown-js/-/blob/main/src/dropdown-controller.js?ref_type=heads)
|
|
69
74
|
- Stimulus with Svelte component
|
|
70
75
|
- Example: [Stimulus controller on example app](https://gitlab.com/sedl/stimulusfloatingdropdown/-/blob/main/app/frontend/javascript/svelte-dropdown-controller.js?ref_type=heads)
|
|
71
76
|
|
|
72
|
-
**Important:** When creating or initializing the
|
|
77
|
+
**Important:** When creating or initializing the dropdownSvelte,
|
|
73
78
|
always call the initialize function before attaching a listener to the panel's close event.
|
|
74
79
|
The initialize function adds a `close` event listener to the panel that executes the `onPanelClose` function.
|
|
75
80
|
If your custom close function destroys the panel, this has to be done after the `onPanelClose` is fired by the `close` event.
|
|
76
81
|
|
|
77
82
|
## Requirements
|
|
78
83
|
|
|
79
|
-
- The class `
|
|
84
|
+
- The class `dropdownSvelte-panel-example-class` must be set to `position: absolute;`.
|
|
80
85
|
- The class `hide` must be set to `display: none;`.
|
|
81
86
|
|
|
82
87
|
## Options
|
|
83
88
|
|
|
84
|
-
- If there is an element with ID `arrow` inside the
|
|
89
|
+
- If there is an element with ID `arrow` inside the dropdownSvelte panel, it is treated as described on [floating-ui](https://floating-ui.com/docs/arrow).
|
|
85
90
|
- 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
91
|
|
|
87
92
|
## Events
|
|
@@ -109,12 +114,12 @@ Event Triggers on the panel element:
|
|
|
109
114
|
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
115
|
|
|
111
116
|
```html
|
|
112
|
-
<div data-controller="csedl-place-
|
|
117
|
+
<div data-controller="csedl-place-dropdownSvelte-panels" data-on="scroll" data-run-after="500" style="overflow: scroll;">
|
|
113
118
|
...
|
|
114
119
|
</div>
|
|
115
120
|
```
|
|
116
121
|
|
|
117
|
-
Now, on scrolling, it searches for all
|
|
122
|
+
Now, on scrolling, it searches for all dropdownSvelte-buttons (by class-name `has-open-panel`) and triggers the `place-panel` event there.
|
|
118
123
|
|
|
119
124
|
**Options**
|
|
120
125
|
|
|
@@ -151,15 +156,15 @@ makes a tooltip.
|
|
|
151
156
|
|
|
152
157
|
It adds the class `tooltip-is-visible` to the tooltip label while the tooltip is visible.
|
|
153
158
|
|
|
154
|
-
`data-src` attribute is working similar to
|
|
159
|
+
`data-src` attribute is working similar to dropdownSvelte
|
|
155
160
|
|
|
156
161
|
## Rails Helpers
|
|
157
162
|
|
|
158
163
|
There is a corresponding rails gem, on [GitLab](https://gitlab.com/sedl/csedl-stimulus-dropdown)
|
|
159
164
|
|
|
160
|
-
## Stimulus Usage in stimulus-
|
|
165
|
+
## Stimulus Usage in stimulus-dropdownSvelte
|
|
161
166
|
|
|
162
|
-
This package uses Stimulus unconventionally to initialize and toggle external
|
|
167
|
+
This package uses Stimulus unconventionally to initialize and toggle external dropdownSvelte or tooltip panels
|
|
163
168
|
(via data-panel-id) rather than managing child elements within the controller’s scope,
|
|
164
169
|
as is typical in Stimulus documentation.
|
|
165
170
|
|
package/index.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import HotwireSvelteHelpers from "./src/lib/config.js";
|
|
3
|
+
import { cleanMount, unmountAllDetached } from './src/svelte/cleanMount';
|
|
4
|
+
import {initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog} from './src/lib/utils.js'
|
|
3
5
|
|
|
4
|
-
import { Application } from "@hotwired/stimulus"
|
|
5
|
-
window.Stimulus = Application.start()
|
|
6
6
|
|
|
7
|
-
// initialize dropdowns
|
|
8
|
-
import dc from './src/dropdown-controller'
|
|
9
|
-
Stimulus.register('csedl-dropdown', dc)
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import ppc from './src/move-panels-controller'
|
|
17
|
-
Stimulus.register('csedl-move-panels', ppc)
|
|
18
|
-
|
|
19
|
-
export { StimulusDropdown }
|
|
8
|
+
export {
|
|
9
|
+
HotwireSvelteHelpers,
|
|
10
|
+
cleanMount, unmountAllDetached,
|
|
11
|
+
initializeDropdown, openDropdownPanel, getOrSetPanelId, debugLog
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csedl/hotwire-svelte-helpers",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals +
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals + RTurbo-friendly utilities. Build together with the rubygem svelte-on-rails and its npm-package.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js"
|
|
11
|
+
},
|
|
9
12
|
"repository": {
|
|
10
13
|
"type": "git",
|
|
11
14
|
"url": "git+https://gitlab.com/sedl/csedl-hotwire-svelte-helpers"
|
|
12
15
|
},
|
|
13
16
|
"keywords": [
|
|
14
|
-
"
|
|
17
|
+
"Hotwire",
|
|
15
18
|
"Dropdown",
|
|
16
|
-
"Rails"
|
|
19
|
+
"Rails",
|
|
20
|
+
"Svelte"
|
|
17
21
|
],
|
|
18
22
|
"author": "Christian Sedlmair",
|
|
19
23
|
"license": "MIT",
|
|
20
24
|
"bugs": {
|
|
21
25
|
"url": "https://gitlab.com/sedl/csedl-stimulus-dropdown-js/-/issues"
|
|
22
26
|
},
|
|
23
|
-
"homepage": "https://gitlab.com/sedl/csedl-
|
|
27
|
+
"homepage": "https://gitlab.com/sedl/csedl-hotwire-svelte-helpers",
|
|
24
28
|
"dependencies": {
|
|
25
29
|
"@floating-ui/dom": ">=1.5.1",
|
|
26
30
|
"@hotwired/stimulus": ">=3.2.1"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {debugLog} from "./utils.js";
|
|
2
|
+
import { Application } from "@hotwired/stimulus"
|
|
3
|
+
import dc from '../stimulus/dropdown-controller'
|
|
4
|
+
import ttc from '../stimulus/tooltip-controller'
|
|
5
|
+
import ppc from '../stimulus/move-panels-controller'
|
|
6
|
+
import { validateOptions } from "./type-validators";
|
|
7
|
+
|
|
8
|
+
// DEFAULTS
|
|
9
|
+
let _debug = false;
|
|
10
|
+
|
|
11
|
+
let _close_on_click_outside_listener_added = false;
|
|
12
|
+
|
|
13
|
+
const HotwireSvelteHelpers = {
|
|
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
|
+
overlays: {
|
|
29
|
+
closeButtonSelector: '.close-button',
|
|
30
|
+
dropdownContentSelector: '.content',
|
|
31
|
+
tooltipContentSelector: '.content',
|
|
32
|
+
addArrow: true,
|
|
33
|
+
persistTooltipOnClick: false,
|
|
34
|
+
closeOnClickOutsideListenerAdded: false,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
initializeOverlays(options = {}) {
|
|
38
|
+
|
|
39
|
+
validateOptions(options, {
|
|
40
|
+
closeButtonSelector: 'string',
|
|
41
|
+
dropdownContentSelector: 'string',
|
|
42
|
+
tooltipContentSelector: 'string',
|
|
43
|
+
addArrow: 'boolean',
|
|
44
|
+
persistTooltipOnClick: 'boolean',
|
|
45
|
+
closeOnClickOutsideListenerAdded: 'boolean',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const overlays = this.overlays;
|
|
49
|
+
|
|
50
|
+
if (options.closeButtonSelector !== undefined) {
|
|
51
|
+
overlays.closeButtonSelector = options.closeButtonSelector;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.dropdownContentSelector !== undefined) {
|
|
55
|
+
overlays.dropdownContentSelector = options.dropdownContentSelector;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.tooltipContentSelector !== undefined) {
|
|
59
|
+
overlays.tooltipContentSelector = options.tooltipContentSelector;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.addArrow !== undefined) {
|
|
63
|
+
overlays.addArrow = options.addArrow;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.persistTooltipOnClick !== undefined) {
|
|
67
|
+
overlays.persistTooltipOnClick = options.persistTooltipOnClick;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
window.Stimulus = Application.start()
|
|
71
|
+
|
|
72
|
+
Stimulus.register('csedl-dropdown', dc)
|
|
73
|
+
Stimulus.register('csedl-tooltip', ttc)
|
|
74
|
+
Stimulus.register('csedl-move-panels', ppc)
|
|
75
|
+
|
|
76
|
+
debugLog('Overlays active, driven by Stimulus')
|
|
77
|
+
|
|
78
|
+
return overlays;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
window.HotwireSvelteHelpers = HotwireSvelteHelpers;
|
|
84
|
+
|
|
85
|
+
export default HotwireSvelteHelpers;
|
|
@@ -28,7 +28,7 @@ export function getAllOpenPanels(scopeElement) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
debugLog(`FOUND ${elements.length} OPEN DROPDOWNS`)
|
|
31
|
+
debugLog(`FOUND ${elements.length} OPEN DROPDOWNS for ${buttons.length} buttons`)
|
|
32
32
|
|
|
33
33
|
return (elements)
|
|
34
34
|
}
|
|
@@ -42,6 +42,7 @@ export function positionAllPanels(elements) {
|
|
|
42
42
|
|
|
43
43
|
// Positions a single panel relative to its button
|
|
44
44
|
export function positionPanel(button, panel) {
|
|
45
|
+
|
|
45
46
|
let arrowElement = panel.querySelector('#arrow')
|
|
46
47
|
panel.style.removeProperty('height')
|
|
47
48
|
panel.style.removeProperty('left')
|
|
@@ -75,9 +76,9 @@ export function positionPanel(button, panel) {
|
|
|
75
76
|
bottom: '',
|
|
76
77
|
[staticSide]: '-4px',
|
|
77
78
|
});
|
|
78
|
-
debugLog(`panel + arrow positioned`)
|
|
79
|
+
debugLog(`panel + arrow positioned: ${Math.round(x)}/${Math.round(y)}`)
|
|
79
80
|
} else {
|
|
80
|
-
debugLog(`panel positioned`)
|
|
81
|
+
debugLog(`panel positioned: ${Math.round(x)}/${Math.round(y)}`)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
adjustToWindowBounds(button, panel)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that all options in `givenOptions` are valid keys
|
|
3
|
+
* and have the correct runtime type as defined in `validations`.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} givenOptions - The options object to validate
|
|
6
|
+
* @param {Object} validations - Map of option name → expected typeof string
|
|
7
|
+
* e.g. { timeout: 'number', debug: 'boolean' }
|
|
8
|
+
* @throws {Error} If an invalid option is found or type doesn't match
|
|
9
|
+
*/
|
|
10
|
+
export function validateOptions(givenOptions, validations) {
|
|
11
|
+
const validKeys = Object.keys(validations);
|
|
12
|
+
|
|
13
|
+
for (const option of Object.keys(givenOptions)) {
|
|
14
|
+
// Check if the option is allowed
|
|
15
|
+
if (!validKeys.includes(option)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Invalid option: "${option}". ` +
|
|
18
|
+
`Available options are:\n • ${validKeys.map(k => `"${k}"`).join("\n • ")}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check the type
|
|
23
|
+
const expectedType = validations[option];
|
|
24
|
+
const actualType = typeof givenOptions[option];
|
|
25
|
+
|
|
26
|
+
if (actualType !== expectedType) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Option «${option}»: expected type «${expectedType}», ` +
|
|
29
|
+
`but received «${actualType}» ` +
|
|
30
|
+
`(value: ${JSON.stringify(givenOptions[option])})`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {positionPanelSelf, positionPanelByButton, positionPanel} from "./floating-ui-functions.js";
|
|
2
|
+
import {validateOptions} from "./type-validators";
|
|
3
|
+
|
|
4
|
+
// Fetch content from server based on panel's data-src attribute and update content
|
|
5
|
+
export function openDropdownPanel(buttonElement, panelElement, options = {}) {
|
|
6
|
+
|
|
7
|
+
if (buttonElement.classList.contains('has-open-panel')) {return}
|
|
8
|
+
|
|
9
|
+
//validateOptions(options, {clickedElement: 'object'})
|
|
10
|
+
//const clickedElement = options.clickedElement || buttonElement;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// link button and panel, initialize
|
|
14
|
+
panelElement.id = getOrSetPanelId(buttonElement);
|
|
15
|
+
initializeDropdown(buttonElement)
|
|
16
|
+
debugLog(`opening panel ${panelElement.id.split('-').pop()}`)
|
|
17
|
+
panelElement.style.display = 'block';
|
|
18
|
+
|
|
19
|
+
// add arrow
|
|
20
|
+
if (window.HotwireSvelteHelpers.overlays.addArrow) {
|
|
21
|
+
if (!panelElement.querySelector(':scope > #arrow')) {
|
|
22
|
+
const arrowTag = document.createElement('div');
|
|
23
|
+
arrowTag.id = 'arrow';
|
|
24
|
+
panelElement.appendChild(arrowTag)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
positionPanel(buttonElement, panelElement);
|
|
29
|
+
|
|
30
|
+
// Set focus to input element
|
|
31
|
+
if (panelElement.hasAttribute('data-set-focus')) {
|
|
32
|
+
let dataFocus = panelElement.getAttribute('data-set-focus');
|
|
33
|
+
let focusElement = panelElement.querySelector(dataFocus);
|
|
34
|
+
focusElement.focus();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Set status attribute
|
|
38
|
+
panelElement.setAttribute('data-hsh-panel-status', 'open');
|
|
39
|
+
buttonElement.classList.add('has-open-panel');
|
|
40
|
+
|
|
41
|
+
// Dispatch events
|
|
42
|
+
const panelOpenEvent = new CustomEvent('before-open');
|
|
43
|
+
panelElement.dispatchEvent(panelOpenEvent);
|
|
44
|
+
|
|
45
|
+
const buttonOpenEvent = new CustomEvent('before-open-panel');
|
|
46
|
+
buttonElement.dispatchEvent(buttonOpenEvent);
|
|
47
|
+
|
|
48
|
+
// fetch content from server
|
|
49
|
+
const src = panelElement.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 buttonElement = document.querySelector(`[data-panel-id="${panelElement.id}"]`);
|
|
60
|
+
const ctrl = buttonElement.getAttribute('data-controller');
|
|
61
|
+
const config = window.HotwireSvelteHelpers.overlays;
|
|
62
|
+
const contentSelector = config.dropdownContentSelector;
|
|
63
|
+
|
|
64
|
+
if (ctrl === 'csedl-dropdown' && contentSelector) {
|
|
65
|
+
debugLog(`dropdown / contentSelector: «${contentSelector}»`);
|
|
66
|
+
const wrapper = panelElement.querySelector(contentSelector);
|
|
67
|
+
wrapper.innerHTML = xhr.response;
|
|
68
|
+
} else if (ctrl === 'csedl-tooltip' && contentSelector) {
|
|
69
|
+
debugLog(`tooltip / contentSelector: «${contentSelector}»`);
|
|
70
|
+
const wrapper = panelElement.querySelector(config.tooltipContentSelector);
|
|
71
|
+
wrapper.innerHTML = xhr.response;
|
|
72
|
+
} else {
|
|
73
|
+
debugLog(`? / contentSelector: «${contentSelector}»`);
|
|
74
|
+
panelElement.innerHTML = xhr.response;
|
|
75
|
+
console.error('fallback to replace whole panel');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
positionPanelByButton(buttonElement, `after http-request "${ctrl}"`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
} else {
|
|
82
|
+
debugLog('no data-src attribute provided on panel');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle panel closing
|
|
88
|
+
export function closePanel(button) {
|
|
89
|
+
|
|
90
|
+
const panel = findPanelOrThrow(button);
|
|
91
|
+
|
|
92
|
+
debugLog(`closing panel ${panel.id.split('-').pop()}`)
|
|
93
|
+
|
|
94
|
+
// Update status attributes
|
|
95
|
+
button.classList.remove('has-open-panel');
|
|
96
|
+
panel.setAttribute('data-hsh-panel-status', 'closed');
|
|
97
|
+
panel.style.display = 'none';
|
|
98
|
+
|
|
99
|
+
// Remove outside click listener if no panels are open
|
|
100
|
+
const openPanels = document.querySelectorAll("[data-hsh-panel-status='open']");
|
|
101
|
+
debugLog(`panel closed, still open: ${openPanels.length}`);
|
|
102
|
+
if (window.HotwireSvelteHelpers.closeOnClickOutsideListenerAdded && openPanels.length === 0) {
|
|
103
|
+
window.HotwireSvelteHelpers.closeOnClickOutsideListenerAdded = false;
|
|
104
|
+
window.removeEventListener('click', closeOnOutsideClick);
|
|
105
|
+
debugLog('Listener for close panels on click outside removed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dispatch close event
|
|
109
|
+
let buttonCloseEvent = new CustomEvent('after-close-panel');
|
|
110
|
+
button.dispatchEvent(buttonCloseEvent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Close panels when clicking outside
|
|
114
|
+
export function closeOnOutsideClick(clickedElement) {
|
|
115
|
+
|
|
116
|
+
// set button
|
|
117
|
+
let btn
|
|
118
|
+
if (clickedElement instanceof HTMLElement) {
|
|
119
|
+
btn = clickedElement;
|
|
120
|
+
} else if (clickedElement instanceof Event) {
|
|
121
|
+
btn = clickedElement.target;
|
|
122
|
+
} else {
|
|
123
|
+
console.error('closeOnOutsideClick: invalid argument', clickedElement);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// do nothing if clicked within button or panel
|
|
128
|
+
const in_btn = btn.closest(".dropdown-button");
|
|
129
|
+
const in_panel = btn.closest(".dropdown-panel");
|
|
130
|
+
if (in_btn || in_panel) return;
|
|
131
|
+
|
|
132
|
+
let parentPanelIds = [];
|
|
133
|
+
|
|
134
|
+
const persistElement = btn.closest('[data-overlay-persist]');
|
|
135
|
+
if (persistElement) {
|
|
136
|
+
if (persistElement.getAttribute('data-overlay-persist') !== 'false') {
|
|
137
|
+
debugLog('closing panel prevented because the target element has attribute "data-overlay-persist"');
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
debugLog('closeOnOutsideClick called, clicked on:', btn);
|
|
143
|
+
|
|
144
|
+
while (true) {
|
|
145
|
+
const parentPanel = btn.closest("[data-hsh-panel-status='open']");
|
|
146
|
+
if (parentPanel) {
|
|
147
|
+
parentPanelIds.push(parentPanel.id);
|
|
148
|
+
btn = document.querySelector(`[data-panel-id="${parentPanel.id}"]`);
|
|
149
|
+
} else {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const openPanels = document.querySelectorAll("[data-hsh-panel-status='open']");
|
|
155
|
+
for (const panel of openPanels) {
|
|
156
|
+
if (!parentPanelIds.includes(panel.id)) {
|
|
157
|
+
debugLog(`closeOnOutsideClick: dispatching close event for panel ${panel.id.split('-').pop()}`);
|
|
158
|
+
const ev = new Event('close');
|
|
159
|
+
panel.dispatchEvent(ev);
|
|
160
|
+
} else {
|
|
161
|
+
debugLog('closeOnOutsideClick: panel is still open:', panel);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Add panel functionality, including close event listeners
|
|
167
|
+
export function initializeDropdown(button) {
|
|
168
|
+
|
|
169
|
+
// find panel
|
|
170
|
+
const panel_id = button.getAttribute('data-panel-id');
|
|
171
|
+
if (!panel_id) {
|
|
172
|
+
throw new Error(`data-panel-id attribute is not defined for given dropdown button`);
|
|
173
|
+
}
|
|
174
|
+
const panel = document.getElementById(panel_id);
|
|
175
|
+
if (!panel) {
|
|
176
|
+
throw new Error(`panel element not found by ID: «${panel_id}»`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let msg = []
|
|
180
|
+
|
|
181
|
+
// initialize panel
|
|
182
|
+
if (!panel.hasAttribute('data-hsh-initialized')) {
|
|
183
|
+
panel.setAttribute('data-hsh-initialized', 'true');
|
|
184
|
+
|
|
185
|
+
// Add close button functionality
|
|
186
|
+
const selector = window.HotwireSvelteHelpers.overlays.closeButtonSelector;
|
|
187
|
+
const closeButtons = panel.querySelectorAll(selector);
|
|
188
|
+
for (const btn of closeButtons) {
|
|
189
|
+
btn.addEventListener('click', () => {
|
|
190
|
+
const ev = new Event('close');
|
|
191
|
+
panel.dispatchEvent(ev);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Add event listeners to panel
|
|
195
|
+
panel.addEventListener('place-me', () => positionPanelSelf(panel));
|
|
196
|
+
panel.addEventListener('close', () => closePanel(button))
|
|
197
|
+
msg.push('panel')
|
|
198
|
+
} else {
|
|
199
|
+
debugLog('initializeDropdown: panel already initialized:', panel.id.split('-').pop())
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// initialize the button
|
|
203
|
+
if (!button.hasAttribute('data-hsh-initialized')) {
|
|
204
|
+
button.setAttribute('data-hsh-initialized', 'true');
|
|
205
|
+
button.addEventListener('place-panel', () => positionPanelByButton(button))
|
|
206
|
+
msg.push('button')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Add listener for closing on outside click
|
|
210
|
+
let overlays = window.HotwireSvelteHelpers.overlays
|
|
211
|
+
if (!overlays.closeOnClickOutsideListenerAdded) {
|
|
212
|
+
overlays.closeOnClickOutsideListenerAdded = true;
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
window.addEventListener('click', closeOnOutsideClick)
|
|
215
|
+
}, 100)
|
|
216
|
+
msg.push('close-on-outside-click')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if (msg.length > 0) {
|
|
221
|
+
debugLog(`Initialized «${panel_id}»: ${msg.join(' + ')}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function findPanelOrThrow(button) {
|
|
226
|
+
const panel = document.getElementById(button.getAttribute('data-panel-id'))
|
|
227
|
+
if (!panel) {
|
|
228
|
+
throw new Error(`dropdown button not found by attribute data-panel-id: «${button.getAttribute('data-panel-id')}»`)
|
|
229
|
+
}
|
|
230
|
+
return (panel)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
234
|
+
// LOGGER
|
|
235
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
236
|
+
|
|
237
|
+
export function debugLog(message, object) {
|
|
238
|
+
if (window.HotwireSvelteHelpers.debug) {
|
|
239
|
+
|
|
240
|
+
if (object) {
|
|
241
|
+
console.log(`[HSH] ${message}`, object);
|
|
242
|
+
|
|
243
|
+
} else {
|
|
244
|
+
console.log(`[HSH] ${message}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
251
|
+
// ADDITIONAL FUNCTIONS
|
|
252
|
+
// utils that are not used for the primary functions of this package itself, but helpful for building associated functions
|
|
253
|
+
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
// find or add the data-panel-id attribute of a button and return it
|
|
257
|
+
export function getOrSetPanelId(buttonTag) {
|
|
258
|
+
let id = buttonTag.getAttribute('data-panel-id')
|
|
259
|
+
if (id) {
|
|
260
|
+
return (id)
|
|
261
|
+
} else {
|
|
262
|
+
id = `dropdown-panel-${generateRandomHex(8)}`
|
|
263
|
+
buttonTag.setAttribute('data-panel-id', id)
|
|
264
|
+
return id
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
function generateRandomHex(n) {
|
|
270
|
+
if (n <= 0) return '';
|
|
271
|
+
return Array.from({length: n}, () =>
|
|
272
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
273
|
+
).join('');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { openDropdownPanel, closePanel, findPanelOrThrow } from "../lib/utils.js";
|
|
3
|
+
import { debugLog } from "../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
|
|
9
|
+
this.element.addEventListener('click', (e) => this.toggle(e))
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
toggle(e) {
|
|
15
|
+
if (this.element.getAttribute('data-prevent-default')) {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
}
|
|
18
|
+
e.stopPropagation()
|
|
19
|
+
debugLog('toggle panel', e)
|
|
20
|
+
|
|
21
|
+
const panel = findPanelOrThrow(this.element)
|
|
22
|
+
if (panel.style.display === 'block') {
|
|
23
|
+
closePanel(this.element)
|
|
24
|
+
} else {
|
|
25
|
+
openDropdownPanel(this.element, panel)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Controller} from "@hotwired/stimulus"
|
|
2
|
-
import {positionAllPanels, getAllOpenPanels} from "
|
|
3
|
-
import {debugLog} from "
|
|
2
|
+
import {positionAllPanels, getAllOpenPanels} from "../lib/floating-ui-functions";
|
|
3
|
+
import {debugLog} from "../lib/utils.js";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Controller} from "@hotwired/stimulus"
|
|
2
|
-
import {positionPanelByButton} from "
|
|
3
|
-
import {debugLog,
|
|
2
|
+
import {positionPanelByButton} from "../lib/floating-ui-functions.js";
|
|
3
|
+
import {debugLog, findPanelOrThrow, openDropdownPanel} from "../lib/utils.js";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ export default class extends Controller {
|
|
|
17
17
|
}
|
|
18
18
|
this.element.addEventListener('mouseenter', (e) => this.start_opening(e, panel_id, delay_sec * 1000))
|
|
19
19
|
this.element.addEventListener('mouseleave', (e) => this.close(e, panel_id))
|
|
20
|
-
if (window.
|
|
20
|
+
if (window.HotwireSvelteHelpers.persistTooltipOnClick) {
|
|
21
21
|
this.element.addEventListener('click', (e) => this.toggleByClick(e, panel_id))
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -42,7 +42,7 @@ export default class extends Controller {
|
|
|
42
42
|
|
|
43
43
|
open(e, panel_id) {
|
|
44
44
|
this.element.classList.add('tooltip-is-visible')
|
|
45
|
-
|
|
45
|
+
openDropdownPanel(this.element, findPanelOrThrow(this.element))
|
|
46
46
|
this.opening_timer_id = null
|
|
47
47
|
let panel = document.getElementById(panel_id)
|
|
48
48
|
panel.style.display = 'block';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte';
|
|
2
|
+
import { debugLog } from '../lib/utils.js';
|
|
3
|
+
|
|
4
|
+
const mountedByTarget = new WeakMap();
|
|
5
|
+
const trackedTargets = new Set();
|
|
6
|
+
|
|
7
|
+
function getTargetRecordList(target) {
|
|
8
|
+
let records = mountedByTarget.get(target);
|
|
9
|
+
|
|
10
|
+
if (!records) {
|
|
11
|
+
records = [];
|
|
12
|
+
mountedByTarget.set(target, records);
|
|
13
|
+
trackedTargets.add(target);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return records;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cleanMount(component, options = {}) {
|
|
20
|
+
|
|
21
|
+
if (!options.target) {
|
|
22
|
+
throw new Error('[HSH] cleanMount: target is required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!component) {
|
|
26
|
+
throw new Error('[HSH] cleanMount: component is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const instance = mount(component, options);
|
|
30
|
+
|
|
31
|
+
const records = getTargetRecordList(options.target);
|
|
32
|
+
records.push({
|
|
33
|
+
target: options.target,
|
|
34
|
+
component: component,
|
|
35
|
+
instance: instance
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
debugLog(`cleanMount: mounted, stored: ${records.length} components`, instance)
|
|
39
|
+
|
|
40
|
+
return instance;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function unmountAllDetached() {
|
|
44
|
+
let unmountedCount = 0;
|
|
45
|
+
|
|
46
|
+
for (const target of Array.from(trackedTargets)) {
|
|
47
|
+
const records = mountedByTarget.get(target);
|
|
48
|
+
|
|
49
|
+
if (!records || records.length === 0) {
|
|
50
|
+
trackedTargets.delete(target);
|
|
51
|
+
mountedByTarget.delete(target);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (target.isConnected) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
try {
|
|
61
|
+
unmount(record.instance);
|
|
62
|
+
unmountedCount++;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('[HSH] unmountAllDetached: failed to unmount component', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
records.length = 0;
|
|
69
|
+
trackedTargets.delete(target);
|
|
70
|
+
mountedByTarget.delete(target);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (unmountedCount === 0) {
|
|
74
|
+
debugLog('unmountAllDetached: nothing to unmount')
|
|
75
|
+
} else {
|
|
76
|
+
debugLog(`unmountAllDetached: unmounted ${unmountedCount} components`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return unmountedCount;
|
|
80
|
+
}
|
package/src/config.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
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;
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
|
package/src/utils.js
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
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
|
-
}
|