@neovici/cosmoz-bottom-bar 8.1.0 → 9.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/dist/cosmoz-bottom-bar.d.ts +28 -0
- package/dist/cosmoz-bottom-bar.js +233 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/overflow.d.ts +19 -0
- package/dist/overflow.js +68 -0
- package/package.json +14 -22
- package/cosmoz-bottom-bar.html.js +0 -137
- package/cosmoz-bottom-bar.js +0 -435
- package/src/cosmoz-bottom-bar-next.js +0 -232
- package/src/cosmoz-bottom-bar-next.style.js +0 -98
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import '@neovici/cosmoz-dropdown';
|
|
2
|
+
export declare const openMenu: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* `<cosmoz-bottom-bar>` is a horizontal responsive bottom toolbar containing items that
|
|
5
|
+
* can be used for actions.
|
|
6
|
+
*
|
|
7
|
+
* The items placed inside the `cosmoz-bottom-bar` are distributed into the toolbar in a horizontal container.
|
|
8
|
+
* If the items do not fit the available width, those that do not fit are placed in a dropdown
|
|
9
|
+
* menu triggered by a button in the toolbar.
|
|
10
|
+
* The class specified by the property `toolbarClass` (default `cosmoz-bottom-bar-toolbar`)
|
|
11
|
+
* is applied to items distributed to the toolbar.
|
|
12
|
+
* The class specified in the property `menuClass` (default `cosmoz-bottom-bar-menu`)
|
|
13
|
+
* is applied to items distributed to the menu.
|
|
14
|
+
*
|
|
15
|
+
* ### Usage
|
|
16
|
+
*
|
|
17
|
+
* See demo for example usage
|
|
18
|
+
*
|
|
19
|
+
* @element cosmoz-bottom-bar
|
|
20
|
+
* @demo demo/bottom-bar-next.html Basic Demo
|
|
21
|
+
*/
|
|
22
|
+
type Host = HTMLElement & {
|
|
23
|
+
active?: boolean;
|
|
24
|
+
maxToolbarItems?: number;
|
|
25
|
+
};
|
|
26
|
+
declare const CosmozBottomBar: (host: Host) => import("lit-html").TemplateResult<1>;
|
|
27
|
+
export default CosmozBottomBar;
|
|
28
|
+
export declare const bottomBarSlots: import("lit-html").TemplateResult<1>, bottomBarSlotsPolymer: HTMLTemplateElement;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
import { html } from 'lit-html';
|
|
3
|
+
import { map } from 'lit-html/directives/map.js';
|
|
4
|
+
import { html as polymerHtml } from '@polymer/polymer/polymer-element.js';
|
|
5
|
+
import { component, css, useEffect, useLayoutEffect, useMemo, useState, } from '@pionjs/pion';
|
|
6
|
+
import { toggleSize } from '@neovici/cosmoz-collapse/toggle';
|
|
7
|
+
import { useActivity } from '@neovici/cosmoz-utils/keybindings/use-activity';
|
|
8
|
+
import '@neovici/cosmoz-dropdown';
|
|
9
|
+
import overflow from './overflow';
|
|
10
|
+
const style = css `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
bottom: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-width: 100%; /* Firefox fix */
|
|
18
|
+
background-color: inherit;
|
|
19
|
+
transition: max-height 0.3s ease;
|
|
20
|
+
flex: none;
|
|
21
|
+
background-color: var(
|
|
22
|
+
--cosmoz-bottom-bar-bg-color,
|
|
23
|
+
rgba(230, 230, 230, 0.8)
|
|
24
|
+
);
|
|
25
|
+
box-shadow: var(--cosmoz-bottom-bar-shadow, none);
|
|
26
|
+
z-index: 1;
|
|
27
|
+
|
|
28
|
+
--cosmoz-dropdown-anchor-spacing: 12px 6px;
|
|
29
|
+
--paper-button: {
|
|
30
|
+
background-color: inherit;
|
|
31
|
+
text-transform: none;
|
|
32
|
+
border: 0;
|
|
33
|
+
border-radius: 0;
|
|
34
|
+
font-size: inherit;
|
|
35
|
+
color: inherit;
|
|
36
|
+
font-weight: inherit;
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host([force-open]) {
|
|
43
|
+
transition: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[hidden],
|
|
47
|
+
::slotted([hidden]) {
|
|
48
|
+
display: none !important;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#bar {
|
|
52
|
+
height: 64px;
|
|
53
|
+
padding: 0 3%;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#info {
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
padding-right: 3%;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#buttonContainer {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex: 1 1 auto;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
justify-content: flex-start;
|
|
70
|
+
flex-direction: row-reverse;
|
|
71
|
+
position: relative;
|
|
72
|
+
margin: 0 8px;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
max-height: 40px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#bottomBarToolbar::slotted(:not(slot):not([unstyled])) {
|
|
78
|
+
margin: 0 0.29em;
|
|
79
|
+
min-width: 40px;
|
|
80
|
+
min-height: 40px;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
background: var(
|
|
84
|
+
--cosmoz-bottom-bar-button-bg-color,
|
|
85
|
+
var(--cosmoz-button-bg-color, #101010)
|
|
86
|
+
);
|
|
87
|
+
color: var(
|
|
88
|
+
--cosmoz-bottom-bar-button-color,
|
|
89
|
+
var(--cosmoz-button-color, #fff)
|
|
90
|
+
);
|
|
91
|
+
border-radius: 6px;
|
|
92
|
+
padding: 0 18px;
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
font-weight: 500;
|
|
95
|
+
line-height: 40px;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
flex: 0 0 auto;
|
|
98
|
+
visibility: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#bottomBarToolbar::slotted(:not(slot)[disabled]) {
|
|
102
|
+
opacity: var(--cosmoz-button-disabled-opacity, 0.15);
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#bottomBarToolbar::slotted(:not(slot):hover) {
|
|
107
|
+
background: var(
|
|
108
|
+
--cosmoz-bottom-bar-button-hover-bg-color,
|
|
109
|
+
var(--cosmoz-button-hover-bg-color, #3a3f44)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#dropdown::part(content) {
|
|
114
|
+
max-width: 300px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:host([hide-actions]) #bottomBarToolbar,
|
|
118
|
+
:host([hide-actions]) #bottomBarMenu,
|
|
119
|
+
:host([hide-actions]) #dropdown {
|
|
120
|
+
display: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
:host(:not([has-menu-items])) cosmoz-dropdown-menu {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
export const openMenu = Symbol('openMenu');
|
|
128
|
+
const openActionsMenu = (host) => {
|
|
129
|
+
const dropdown = host.shadowRoot?.querySelector('#dropdown');
|
|
130
|
+
if (!dropdown || dropdown.hasAttribute('hidden'))
|
|
131
|
+
return;
|
|
132
|
+
//TODO: Clean up when open function is implemented for cosmoz-dropdown-menu
|
|
133
|
+
const cosmozDropdown = dropdown.shadowRoot?.querySelector('cosmoz-dropdown'), button = cosmozDropdown?.shadowRoot?.querySelector('#dropdownButton');
|
|
134
|
+
button?.click();
|
|
135
|
+
};
|
|
136
|
+
const useMenuButtons = (host) => {
|
|
137
|
+
const [buttonStates, setButtonStates] = useState({
|
|
138
|
+
visible: new Set(),
|
|
139
|
+
overflowing: new Set(),
|
|
140
|
+
});
|
|
141
|
+
const allButtons = useMemo(() => [...buttonStates.visible, ...buttonStates.overflowing], [buttonStates]);
|
|
142
|
+
const processedButtons = useMemo(() => allButtons
|
|
143
|
+
.map((btn) => ({
|
|
144
|
+
element: btn,
|
|
145
|
+
priority: Number(btn.dataset.priority ?? 0),
|
|
146
|
+
text: btn.textContent?.trim() || '',
|
|
147
|
+
}))
|
|
148
|
+
.toSorted((a, b) => b.priority - a.priority), [allButtons]);
|
|
149
|
+
const { maxToolbarItems = 1 } = host;
|
|
150
|
+
const toolbarLimit = Math.min(maxToolbarItems, buttonStates.visible.size >= 0
|
|
151
|
+
? buttonStates.visible.size
|
|
152
|
+
: allButtons.length);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
processedButtons.forEach(({ element, priority }, i) => {
|
|
155
|
+
const isVisible = i < toolbarLimit;
|
|
156
|
+
element.style.visibility = isVisible ? 'visible' : 'hidden';
|
|
157
|
+
element.style.order = String(-priority);
|
|
158
|
+
});
|
|
159
|
+
}, [processedButtons, toolbarLimit]);
|
|
160
|
+
const menuButtons = useMemo(() => processedButtons
|
|
161
|
+
.slice(toolbarLimit)
|
|
162
|
+
.sort((a, b) => b.element.compareDocumentPosition(a.element) -
|
|
163
|
+
a.element.compareDocumentPosition(b.element)), [processedButtons, toolbarLimit]);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
host.toggleAttribute('has-menu-items', menuButtons.length > 0);
|
|
166
|
+
}, [menuButtons.length]);
|
|
167
|
+
return { setButtonStates, menuButtons };
|
|
168
|
+
};
|
|
169
|
+
const CosmozBottomBar = (host) => {
|
|
170
|
+
const { active = false } = host;
|
|
171
|
+
useActivity({
|
|
172
|
+
activity: openMenu,
|
|
173
|
+
callback: () => openActionsMenu(host),
|
|
174
|
+
check: () => active && !host.hasAttribute('hide-actions'),
|
|
175
|
+
element: () => host.shadowRoot?.querySelector('#dropdown'),
|
|
176
|
+
}, [active]);
|
|
177
|
+
const { setButtonStates, menuButtons } = useMenuButtons(host);
|
|
178
|
+
const toggle = useMemo(() => toggleSize('height'), []);
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
toggle(host, active);
|
|
181
|
+
}, [active]);
|
|
182
|
+
return html `<div id="bar" part="bar">
|
|
183
|
+
<div id="info" part="info"><slot name="info"></slot></div>
|
|
184
|
+
<div id="buttonContainer">
|
|
185
|
+
<slot id="bottomBarToolbar" ${overflow(setButtonStates)}></slot>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<cosmoz-dropdown-menu id="dropdown">
|
|
189
|
+
<svg
|
|
190
|
+
slot="button"
|
|
191
|
+
width="4"
|
|
192
|
+
height="16"
|
|
193
|
+
viewBox="0 0 4 16"
|
|
194
|
+
fill="none"
|
|
195
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
196
|
+
>
|
|
197
|
+
<path
|
|
198
|
+
fill-rule="evenodd"
|
|
199
|
+
clip-rule="evenodd"
|
|
200
|
+
d="M1.50996e-07 2C1.02714e-07 3.10457 0.89543 4 2 4C3.10457 4 4 3.10457 4 2C4 0.89543 3.10457 -3.91405e-08 2 -8.74228e-08C0.895431 -1.35705e-07 1.99278e-07 0.89543 1.50996e-07 2Z"
|
|
201
|
+
fill="white"
|
|
202
|
+
/>
|
|
203
|
+
<path
|
|
204
|
+
fill-rule="evenodd"
|
|
205
|
+
clip-rule="evenodd"
|
|
206
|
+
d="M1.50996e-07 8C1.02714e-07 9.10457 0.89543 10 2 10C3.10457 10 4 9.10457 4 8C4 6.89543 3.10457 6 2 6C0.895431 6 1.99278e-07 6.89543 1.50996e-07 8Z"
|
|
207
|
+
fill="white"
|
|
208
|
+
/>
|
|
209
|
+
<path
|
|
210
|
+
fill-rule="evenodd"
|
|
211
|
+
clip-rule="evenodd"
|
|
212
|
+
d="M1.50996e-07 14C1.02714e-07 15.1046 0.89543 16 2 16C3.10457 16 4 15.1046 4 14C4 12.8954 3.10457 12 2 12C0.895431 12 1.99278e-07 12.8954 1.50996e-07 14Z"
|
|
213
|
+
fill="white"
|
|
214
|
+
/>
|
|
215
|
+
</svg>
|
|
216
|
+
${map(menuButtons, (menuButton) => html `
|
|
217
|
+
<paper-button @click=${() => menuButton.element.click()}
|
|
218
|
+
>${menuButton.text}</paper-button
|
|
219
|
+
>
|
|
220
|
+
`)}
|
|
221
|
+
</cosmoz-dropdown-menu>
|
|
222
|
+
<slot name="extra" id="extraSlot"></slot>
|
|
223
|
+
</div>`;
|
|
224
|
+
};
|
|
225
|
+
export default CosmozBottomBar;
|
|
226
|
+
customElements.define('cosmoz-bottom-bar', component(CosmozBottomBar, {
|
|
227
|
+
observedAttributes: ['active', 'max-toolbar-items'],
|
|
228
|
+
styleSheets: [style],
|
|
229
|
+
}));
|
|
230
|
+
const tmplt = `
|
|
231
|
+
<slot name="extra" slot="extra"></slot>
|
|
232
|
+
`;
|
|
233
|
+
export const bottomBarSlots = html(Object.assign([tmplt], { raw: [tmplt] })), bottomBarSlotsPolymer = polymerHtml(Object.assign([tmplt], { raw: [tmplt] }));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cosmoz-bottom-bar';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cosmoz-bottom-bar';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AttributePart } from 'lit-html';
|
|
2
|
+
import { AsyncDirective } from 'lit-html/async-directive.js';
|
|
3
|
+
import { DirectiveResult } from 'lit-html/directive.js';
|
|
4
|
+
type OnOverflow = (opts: {
|
|
5
|
+
visible: Set<HTMLElement>;
|
|
6
|
+
overflowing: Set<HTMLElement>;
|
|
7
|
+
}) => void;
|
|
8
|
+
declare class OverflowDirective extends AsyncDirective {
|
|
9
|
+
observer?: IntersectionObserver;
|
|
10
|
+
slot?: HTMLSlotElement;
|
|
11
|
+
cleanup?: () => void;
|
|
12
|
+
render(): symbol;
|
|
13
|
+
update(part: AttributePart, [onOverflow]: [OnOverflow]): symbol;
|
|
14
|
+
}
|
|
15
|
+
interface Overflow {
|
|
16
|
+
(onOverflow: OnOverflow): DirectiveResult<typeof OverflowDirective>;
|
|
17
|
+
}
|
|
18
|
+
declare const overflow: Overflow;
|
|
19
|
+
export default overflow;
|
package/dist/overflow.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { noChange } from 'lit-html';
|
|
2
|
+
import { AsyncDirective } from 'lit-html/async-directive.js';
|
|
3
|
+
import { directive } from 'lit-html/directive.js';
|
|
4
|
+
function isEntryHidden(el) {
|
|
5
|
+
return el.boundingClientRect.height === 0;
|
|
6
|
+
}
|
|
7
|
+
const check = (part) => {
|
|
8
|
+
if (part.element.tagName !== 'SLOT') {
|
|
9
|
+
throw new Error('Overflow directive must be used on a slot element');
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const setupObserver = (slot, onOverflow) => {
|
|
13
|
+
const visible = new Set();
|
|
14
|
+
const overflowing = new Set();
|
|
15
|
+
const observer = new IntersectionObserver((entries) => {
|
|
16
|
+
entries.forEach((entry) => {
|
|
17
|
+
const el = entry.target;
|
|
18
|
+
if (entry.intersectionRatio === 1) {
|
|
19
|
+
visible.add(el);
|
|
20
|
+
overflowing.delete(el);
|
|
21
|
+
}
|
|
22
|
+
else if (isEntryHidden(entry)) {
|
|
23
|
+
visible.delete(el);
|
|
24
|
+
overflowing.delete(el);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
visible.delete(el);
|
|
28
|
+
overflowing.add(el);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
onOverflow({ visible, overflowing });
|
|
32
|
+
}, { root: slot.parentElement, threshold: 1 });
|
|
33
|
+
const observe = () => {
|
|
34
|
+
const elements = Array.from(slot.assignedElements());
|
|
35
|
+
for (const c of elements) {
|
|
36
|
+
observer.observe(c);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
observe();
|
|
40
|
+
slot.addEventListener('slotchange', observe);
|
|
41
|
+
return () => {
|
|
42
|
+
slot.removeEventListener('slotchange', observe);
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
class OverflowDirective extends AsyncDirective {
|
|
47
|
+
observer;
|
|
48
|
+
slot;
|
|
49
|
+
cleanup;
|
|
50
|
+
render() {
|
|
51
|
+
return noChange;
|
|
52
|
+
}
|
|
53
|
+
update(part, [onOverflow]) {
|
|
54
|
+
check(part);
|
|
55
|
+
const hasChanged = this.slot !== part.element;
|
|
56
|
+
if (hasChanged) {
|
|
57
|
+
if (this.cleanup) {
|
|
58
|
+
this.cleanup();
|
|
59
|
+
this.cleanup = undefined;
|
|
60
|
+
}
|
|
61
|
+
this.slot = part.element;
|
|
62
|
+
this.cleanup = setupObserver(this.slot, onOverflow);
|
|
63
|
+
}
|
|
64
|
+
return noChange;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const overflow = directive(OverflowDirective);
|
|
68
|
+
export default overflow;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-bottom-bar",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "A responsive bottom-bar that can house buttons/actions and a menu for the buttons that won't fit the available width.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"polymer",
|
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": "Neovici Development",
|
|
19
|
-
"main": "
|
|
19
|
+
"main": "dist/index.js",
|
|
20
20
|
"exports": {
|
|
21
|
-
".": "./
|
|
22
|
-
"./next": "./src/cosmoz-bottom-bar-next.js"
|
|
23
|
-
},
|
|
24
|
-
"directories": {
|
|
25
|
-
"test": "test"
|
|
21
|
+
".": "./dist/index.js"
|
|
26
22
|
},
|
|
27
23
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
24
|
+
"dist/*",
|
|
25
|
+
"types"
|
|
30
26
|
],
|
|
31
27
|
"scripts": {
|
|
32
|
-
"lint": "eslint --cache
|
|
28
|
+
"lint": "tsc && eslint --cache .",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
30
|
"start": "npm run storybook:start",
|
|
34
31
|
"test": "wtr --coverage",
|
|
35
32
|
"test:watch": "wtr --watch",
|
|
@@ -53,11 +50,6 @@
|
|
|
53
50
|
"publishConfig": {
|
|
54
51
|
"access": "public"
|
|
55
52
|
},
|
|
56
|
-
"commitlint": {
|
|
57
|
-
"extends": [
|
|
58
|
-
"@commitlint/config-conventional"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
53
|
"dependencies": {
|
|
62
54
|
"@neovici/cosmoz-collapse": "^1.5.0",
|
|
63
55
|
"@neovici/cosmoz-dropdown": "^4.0.0 || ^5.0.0",
|
|
@@ -77,23 +69,23 @@
|
|
|
77
69
|
"@polymer/paper-toggle-button": "^3.0.0",
|
|
78
70
|
"@semantic-release/changelog": "^6.0.0",
|
|
79
71
|
"@semantic-release/git": "^10.0.0",
|
|
80
|
-
"@storybook/addon-essentials": "^
|
|
72
|
+
"@storybook/addon-essentials": "^8.6.9",
|
|
81
73
|
"@storybook/addon-links": "^7.0.0",
|
|
82
|
-
"@storybook/builder-vite": "
|
|
74
|
+
"@storybook/builder-vite": "8.6.9",
|
|
83
75
|
"@storybook/storybook-deployer": "^2.8.5",
|
|
84
|
-
"@storybook/web-components": "
|
|
76
|
+
"@storybook/web-components": "8.6.9",
|
|
85
77
|
"@types/mocha": "^10.0.6",
|
|
86
|
-
"@web/storybook-builder": "^0.1
|
|
87
|
-
"@web/storybook-framework-web-components": "^0.
|
|
78
|
+
"@web/storybook-builder": "^0.2.1",
|
|
79
|
+
"@web/storybook-framework-web-components": "^0.2.0",
|
|
88
80
|
"@webcomponents/webcomponentsjs": "^2.5.0",
|
|
89
|
-
"esbuild": "^0.
|
|
81
|
+
"esbuild": "^0.25.0",
|
|
90
82
|
"eslint": "^8.57.1",
|
|
91
83
|
"http-server": "^14.1.1",
|
|
92
84
|
"husky": "^9.1.1",
|
|
93
85
|
"rollup-plugin-esbuild": "^6.1.1",
|
|
94
86
|
"semantic-release": "^24.0.0",
|
|
95
87
|
"sinon": "^19.0.0",
|
|
96
|
-
"storybook": "^
|
|
88
|
+
"storybook": "^8.6.9",
|
|
97
89
|
"typescript": "^5.0.0"
|
|
98
90
|
}
|
|
99
91
|
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
import '@neovici/cosmoz-dropdown';
|
|
3
|
-
import { html } from '@polymer/polymer/lib/utils/html-tag';
|
|
4
|
-
|
|
5
|
-
export default html`
|
|
6
|
-
<style>
|
|
7
|
-
:host {
|
|
8
|
-
display: block;
|
|
9
|
-
bottom: 0;
|
|
10
|
-
left: 0;
|
|
11
|
-
width: 100%;
|
|
12
|
-
max-width: 100%; /* Firefox fix */
|
|
13
|
-
background-color: inherit;
|
|
14
|
-
transition: max-height 0.3s ease;
|
|
15
|
-
flex: none;
|
|
16
|
-
background-color: var(
|
|
17
|
-
--cosmoz-bottom-bar-bg-color,
|
|
18
|
-
rgba(230, 230, 230, 0.8)
|
|
19
|
-
);
|
|
20
|
-
box-shadow: var(--cosmoz-bottom-bar-shadow, none);
|
|
21
|
-
z-index: 1;
|
|
22
|
-
|
|
23
|
-
--cosmoz-dropdown-anchor-spacing: 12px 6px;
|
|
24
|
-
--paper-button: {
|
|
25
|
-
background-color: inherit;
|
|
26
|
-
text-transform: none;
|
|
27
|
-
border: 0;
|
|
28
|
-
border-radius: 0;
|
|
29
|
-
font-size: inherit;
|
|
30
|
-
color: inherit;
|
|
31
|
-
font-weight: inherit;
|
|
32
|
-
margin: 0;
|
|
33
|
-
padding: 0;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
:host([force-open]) {
|
|
37
|
-
transition: none;
|
|
38
|
-
}
|
|
39
|
-
[hidden],
|
|
40
|
-
::slotted([hidden]) {
|
|
41
|
-
display: none !important;
|
|
42
|
-
}
|
|
43
|
-
#bar {
|
|
44
|
-
padding: 0 3%;
|
|
45
|
-
display: flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
}
|
|
48
|
-
#info {
|
|
49
|
-
min-width: 5px;
|
|
50
|
-
padding-right: 3%;
|
|
51
|
-
margin-right: auto;
|
|
52
|
-
white-space: nowrap;
|
|
53
|
-
}
|
|
54
|
-
#bottomBarToolbar::slotted(:not(slot):not([unstyled])) {
|
|
55
|
-
margin: 0 0.29em;
|
|
56
|
-
min-width: 40px;
|
|
57
|
-
min-height: 40px;
|
|
58
|
-
text-overflow: ellipsis;
|
|
59
|
-
white-space: nowrap;
|
|
60
|
-
background: var(
|
|
61
|
-
--cosmoz-bottom-bar-button-bg-color,
|
|
62
|
-
var(--cosmoz-button-bg-color, #101010)
|
|
63
|
-
);
|
|
64
|
-
color: var(
|
|
65
|
-
--cosmoz-bottom-bar-button-color,
|
|
66
|
-
var(--cosmoz-button-color, #fff)
|
|
67
|
-
);
|
|
68
|
-
border-radius: 6px;
|
|
69
|
-
padding: 0 18px;
|
|
70
|
-
font-size: 14px;
|
|
71
|
-
font-weight: 500;
|
|
72
|
-
line-height: 40px;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#bottomBarToolbar::slotted(:not(slot)[disabled]) {
|
|
76
|
-
opacity: var(--cosmoz-button-disabled-opacity, 0.15);
|
|
77
|
-
pointer-events: none;
|
|
78
|
-
}
|
|
79
|
-
#bottomBarToolbar::slotted(:not(slot):hover) {
|
|
80
|
-
background: var(
|
|
81
|
-
--cosmoz-bottom-bar-button-hover-bg-color,
|
|
82
|
-
var(--cosmoz-button-hover-bg-color, #3a3f44)
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
#dropdown::part(content) {
|
|
86
|
-
max-width: 300px;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
:host([hide-actions]) #bottomBarToolbar,
|
|
90
|
-
:host([hide-actions]) #bottomBarMenu,
|
|
91
|
-
:host([hide-actions]) #dropdown {
|
|
92
|
-
display: none;
|
|
93
|
-
}
|
|
94
|
-
</style>
|
|
95
|
-
<div id="bar" style$="[[ _getHeightStyle(computedBarHeight) ]]" part="bar">
|
|
96
|
-
<div id="info" part="info"><slot name="info"></slot></div>
|
|
97
|
-
<slot id="bottomBarToolbar" name="bottom-bar-toolbar"></slot>
|
|
98
|
-
<cosmoz-dropdown-menu
|
|
99
|
-
id="dropdown"
|
|
100
|
-
hidden="[[ !hasMenuItems ]]"
|
|
101
|
-
placement="[[ topPlacement ]]"
|
|
102
|
-
>
|
|
103
|
-
<svg
|
|
104
|
-
slot="button"
|
|
105
|
-
width="4"
|
|
106
|
-
height="16"
|
|
107
|
-
viewBox="0 0 4 16"
|
|
108
|
-
fill="none"
|
|
109
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
110
|
-
>
|
|
111
|
-
<path
|
|
112
|
-
fill-rule="evenodd"
|
|
113
|
-
clip-rule="evenodd"
|
|
114
|
-
d="M1.50996e-07 2C1.02714e-07 3.10457 0.89543 4 2 4C3.10457 4 4 3.10457 4 2C4 0.89543 3.10457 -3.91405e-08 2 -8.74228e-08C0.895431 -1.35705e-07 1.99278e-07 0.89543 1.50996e-07 2Z"
|
|
115
|
-
fill="white"
|
|
116
|
-
/>
|
|
117
|
-
<path
|
|
118
|
-
fill-rule="evenodd"
|
|
119
|
-
clip-rule="evenodd"
|
|
120
|
-
d="M1.50996e-07 8C1.02714e-07 9.10457 0.89543 10 2 10C3.10457 10 4 9.10457 4 8C4 6.89543 3.10457 6 2 6C0.895431 6 1.99278e-07 6.89543 1.50996e-07 8Z"
|
|
121
|
-
fill="white"
|
|
122
|
-
/>
|
|
123
|
-
<path
|
|
124
|
-
fill-rule="evenodd"
|
|
125
|
-
clip-rule="evenodd"
|
|
126
|
-
d="M1.50996e-07 14C1.02714e-07 15.1046 0.89543 16 2 16C3.10457 16 4 15.1046 4 14C4 12.8954 3.10457 12 2 12C0.895431 12 1.99278e-07 12.8954 1.50996e-07 14Z"
|
|
127
|
-
fill="white"
|
|
128
|
-
/>
|
|
129
|
-
</svg>
|
|
130
|
-
<slot id="bottomBarMenu" name="bottom-bar-menu"></slot>
|
|
131
|
-
</cosmoz-dropdown-menu>
|
|
132
|
-
<slot name="extra" id="extraSlot"></slot>
|
|
133
|
-
</div>
|
|
134
|
-
<div hidden style="display:none">
|
|
135
|
-
<slot id="content"></slot>
|
|
136
|
-
</div>
|
|
137
|
-
`;
|
package/cosmoz-bottom-bar.js
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PolymerElement,
|
|
3
|
-
html as polymerHtml,
|
|
4
|
-
} from '@polymer/polymer/polymer-element.js';
|
|
5
|
-
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer';
|
|
6
|
-
import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js';
|
|
7
|
-
import { timeOut } from '@polymer/polymer/lib/utils/async';
|
|
8
|
-
import { html } from 'lit-html';
|
|
9
|
-
import { hauntedPolymer } from '@neovici/cosmoz-utils';
|
|
10
|
-
import { useActivity } from '@neovici/cosmoz-utils/keybindings/use-activity';
|
|
11
|
-
import template from './cosmoz-bottom-bar.html.js';
|
|
12
|
-
|
|
13
|
-
const BOTTOM_BAR_TOOLBAR_SLOT = 'bottom-bar-toolbar',
|
|
14
|
-
BOTTOM_BAR_MENU_SLOT = 'bottom-bar-menu',
|
|
15
|
-
rendered = Symbol('rendered');
|
|
16
|
-
|
|
17
|
-
export const openMenu = Symbol('openMenu');
|
|
18
|
-
|
|
19
|
-
const openActionsMenu = (host) => {
|
|
20
|
-
const dropdown = host.$.dropdown;
|
|
21
|
-
|
|
22
|
-
if (dropdown.hasAttribute('hidden')) return;
|
|
23
|
-
|
|
24
|
-
//TODO: Clean up when open function is implemented for cosmoz-dropdown-menu
|
|
25
|
-
dropdown.shadowRoot
|
|
26
|
-
.querySelector('cosmoz-dropdown')
|
|
27
|
-
.shadowRoot.getElementById('dropdownButton')
|
|
28
|
-
.click();
|
|
29
|
-
},
|
|
30
|
-
useBottomBar = (host) => {
|
|
31
|
-
useActivity(
|
|
32
|
-
{
|
|
33
|
-
activity: openMenu,
|
|
34
|
-
callback: () => openActionsMenu(host),
|
|
35
|
-
check: () => host.active && !host.hasAttribute('hide-actions'),
|
|
36
|
-
element: () => host.$.dropdown,
|
|
37
|
-
},
|
|
38
|
-
[host.active],
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `<cosmoz-bottom-bar>` is a horizontal responsive bottom toolbar containing items that
|
|
44
|
-
* can be used for actions.
|
|
45
|
-
*
|
|
46
|
-
* The items placed inside the `cosmoz-bottom-bar` are distributed into the toolbar in a horizontal container.
|
|
47
|
-
* If the items do not fit the available width, those that do not fit are placed in a dropdown
|
|
48
|
-
* menu triggered by a button in the toolbar.
|
|
49
|
-
* The class specified by the property `toolbarClass` (default `cosmoz-bottom-bar-toolbar`)
|
|
50
|
-
* is applied to items distributed to the toolbar.
|
|
51
|
-
* The class specified in the property `menuClass` (default `cosmoz-bottom-bar-menu`)
|
|
52
|
-
* is applied to items distributed to the menu.
|
|
53
|
-
*
|
|
54
|
-
* ### Usage
|
|
55
|
-
*
|
|
56
|
-
* See demo for example usage
|
|
57
|
-
*
|
|
58
|
-
* @element cosmoz-bottom-bar
|
|
59
|
-
* @demo demo/bottom-bar.html Basic Demo
|
|
60
|
-
* @demo demo/bottom-bar-match-parent.html match parent Demo
|
|
61
|
-
*/
|
|
62
|
-
class CosmozBottomBar extends hauntedPolymer(useBottomBar)(PolymerElement) {
|
|
63
|
-
static get template() {
|
|
64
|
-
return template;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static get properties() {
|
|
68
|
-
return {
|
|
69
|
-
/**
|
|
70
|
-
* Whether the bar is active (shown)
|
|
71
|
-
*/
|
|
72
|
-
active: {
|
|
73
|
-
type: Boolean,
|
|
74
|
-
value: false,
|
|
75
|
-
notify: true,
|
|
76
|
-
reflectToAttribute: true,
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
hideActions: {
|
|
80
|
-
type: Boolean,
|
|
81
|
-
value: false,
|
|
82
|
-
reflectToAttribute: true,
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Bar height (not used when `matchParent` or `matchElementHeight` is set)
|
|
87
|
-
*/
|
|
88
|
-
barHeight: {
|
|
89
|
-
type: Number,
|
|
90
|
-
value: 64,
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Whether to match the height of parent
|
|
95
|
-
*/
|
|
96
|
-
matchParent: {
|
|
97
|
-
type: Boolean,
|
|
98
|
-
value: false,
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Whether this bottom bar has items distributed to the menu
|
|
103
|
-
*/
|
|
104
|
-
hasMenuItems: {
|
|
105
|
-
type: Boolean,
|
|
106
|
-
value: false,
|
|
107
|
-
readOnly: true,
|
|
108
|
-
notify: true,
|
|
109
|
-
},
|
|
110
|
-
|
|
111
|
-
hasExtraItems: {
|
|
112
|
-
type: Boolean,
|
|
113
|
-
value: false,
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Class applied the the selected item
|
|
118
|
-
*/
|
|
119
|
-
selectedClass: {
|
|
120
|
-
type: String,
|
|
121
|
-
value: 'cosmoz-bottom-bar-selected-item',
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Class applied to items distributed to the toolbar
|
|
126
|
-
*/
|
|
127
|
-
toolbarClass: {
|
|
128
|
-
type: String,
|
|
129
|
-
value: 'cosmoz-bottom-bar-toolbar',
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Class applied to items distributed to the menu
|
|
134
|
-
*/
|
|
135
|
-
menuClass: {
|
|
136
|
-
type: String,
|
|
137
|
-
value: 'cosmoz-bottom-bar-menu',
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Maximum number of items in toolbar, regardless of space
|
|
142
|
-
*/
|
|
143
|
-
maxToolbarItems: {
|
|
144
|
-
type: Number,
|
|
145
|
-
value: 1,
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* The actual bar height, depending on if we `matchParent` or set `barHeight`
|
|
150
|
-
*/
|
|
151
|
-
computedBarHeight: {
|
|
152
|
-
type: Number,
|
|
153
|
-
notify: true,
|
|
154
|
-
},
|
|
155
|
-
forceOpen: {
|
|
156
|
-
type: Boolean,
|
|
157
|
-
value: false,
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Whether the bar is visible (has actions and is `active`)
|
|
162
|
-
*/
|
|
163
|
-
visible: {
|
|
164
|
-
type: Boolean,
|
|
165
|
-
notify: true,
|
|
166
|
-
readOnly: true,
|
|
167
|
-
computed:
|
|
168
|
-
'_computeVisible(hasActions, active, hasExtraItems, forceOpen)',
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Whether we have any visible actions
|
|
173
|
-
*/
|
|
174
|
-
hasActions: {
|
|
175
|
-
type: Boolean,
|
|
176
|
-
value: false,
|
|
177
|
-
readOnly: true,
|
|
178
|
-
notify: true,
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Reference element from which to inherit height
|
|
183
|
-
*/
|
|
184
|
-
_matchHeightElement: {
|
|
185
|
-
type: Object,
|
|
186
|
-
computed: '_getHeightMatchingElement(matchParent)',
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
topPlacement: {
|
|
190
|
-
value: 'top-end',
|
|
191
|
-
type: String,
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
static get observers() {
|
|
197
|
-
return ['_showHideBottomBar(visible)'];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
constructor() {
|
|
201
|
-
super();
|
|
202
|
-
this._boundChildrenUpdated = this._childrenUpdated.bind(this);
|
|
203
|
-
this._boundLayoutActions = this._layoutActions.bind(this);
|
|
204
|
-
this._resizeObserver = new ResizeObserver((...args) => {
|
|
205
|
-
cancelAnimationFrame(this._resizeId);
|
|
206
|
-
this._resizeId = requestAnimationFrame(() => this._onResize(...args));
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
connectedCallback() {
|
|
211
|
-
super.connectedCallback();
|
|
212
|
-
|
|
213
|
-
const layoutOnRemove = (info) =>
|
|
214
|
-
info.removedNodes.filter(this._isActionNode) &&
|
|
215
|
-
this._debounceLayoutActions();
|
|
216
|
-
this._nodeObservers = [
|
|
217
|
-
new FlattenedNodesObserver(this.$.content, this._boundChildrenUpdated),
|
|
218
|
-
new FlattenedNodesObserver(this.$.extraSlot, (info) =>
|
|
219
|
-
this.set('hasExtraItems', info.addedNodes.length > 0),
|
|
220
|
-
),
|
|
221
|
-
new FlattenedNodesObserver(this.$.bottomBarToolbar, layoutOnRemove),
|
|
222
|
-
new FlattenedNodesObserver(this.$.bottomBarMenu, layoutOnRemove),
|
|
223
|
-
];
|
|
224
|
-
this._hiddenMutationObserver = new MutationObserver(() =>
|
|
225
|
-
this._debounceLayoutActions(),
|
|
226
|
-
);
|
|
227
|
-
this._resizeObserver.observe(this);
|
|
228
|
-
this.computedBarHeight = this._computeComputedBarHeight(
|
|
229
|
-
this._matchHeightElement,
|
|
230
|
-
this.barHeight,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
disconnectedCallback() {
|
|
235
|
-
super.disconnectedCallback();
|
|
236
|
-
this[rendered] = false;
|
|
237
|
-
|
|
238
|
-
[...this._nodeObservers, this._hiddenMutationObserver].forEach((e) =>
|
|
239
|
-
e.disconnect(e),
|
|
240
|
-
);
|
|
241
|
-
this._resizeObserver.unobserve(this);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
_childrenUpdated(info) {
|
|
245
|
-
const addedNodes = info.addedNodes.filter(this._isActionNode),
|
|
246
|
-
removedNodes = info.removedNodes.filter(this._isActionNode),
|
|
247
|
-
newNodes = addedNodes.filter((node) => !removedNodes.includes(node));
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
(addedNodes.length === 0 && removedNodes.length === 0) ||
|
|
251
|
-
newNodes.length === 0
|
|
252
|
-
) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
newNodes.forEach((node) => {
|
|
256
|
-
this._hiddenMutationObserver.observe(node, {
|
|
257
|
-
attributes: true,
|
|
258
|
-
attributeFilter: ['hidden'],
|
|
259
|
-
});
|
|
260
|
-
this._moveElement(node, false);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
this._debounceLayoutActions();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
_computeComputedBarHeight(matchElementHeight, barHeight) {
|
|
267
|
-
if (matchElementHeight) {
|
|
268
|
-
return matchElementHeight.offsetHeight;
|
|
269
|
-
}
|
|
270
|
-
return barHeight;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
_computeVisible(hasActions, active, hasExtraItems, forceOpen) {
|
|
274
|
-
return forceOpen || ((hasActions || hasExtraItems) && active);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
_debounceLayoutActions() {
|
|
278
|
-
this._layoutDebouncer = Debouncer.debounce(
|
|
279
|
-
this._layoutDebouncer,
|
|
280
|
-
timeOut.after(30),
|
|
281
|
-
this._boundLayoutActions,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
_getHeightMatchingElement(matchParent) {
|
|
286
|
-
if (matchParent) {
|
|
287
|
-
return this.parentElement;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
_getHeightStyle(height) {
|
|
294
|
-
return 'height: ' + height + 'px;';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
_isActionNode(node) {
|
|
298
|
-
return (
|
|
299
|
-
node.nodeType === Node.ELEMENT_NODE &&
|
|
300
|
-
node.getAttribute('slot') !== 'info' &&
|
|
301
|
-
node.tagName !== 'TEMPLATE' &&
|
|
302
|
-
node.tagName !== 'STYLE' &&
|
|
303
|
-
node.tagName !== 'DOM-REPEAT' &&
|
|
304
|
-
node.tagName !== 'DOM-IF' &&
|
|
305
|
-
node.getAttribute('slot') !== 'extra'
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
_getElements() {
|
|
310
|
-
const elements = FlattenedNodesObserver.getFlattenedNodes(this)
|
|
311
|
-
.filter(this._isActionNode)
|
|
312
|
-
.filter((element) => !element.hidden)
|
|
313
|
-
.sort((a, b) => (a.dataset.index ?? 0) - (b.dataset.index ?? 0));
|
|
314
|
-
|
|
315
|
-
if (elements.length === 0) {
|
|
316
|
-
return elements;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const topPriorityAction = elements.reduce(
|
|
320
|
-
(top, element) => {
|
|
321
|
-
return parseInt(top.dataset.priority ?? 0, 10) >=
|
|
322
|
-
parseInt(element.dataset.priority ?? 0, 10)
|
|
323
|
-
? top
|
|
324
|
-
: element;
|
|
325
|
-
},
|
|
326
|
-
{ dataset: { priority: '-1000' } },
|
|
327
|
-
[],
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
return [
|
|
331
|
-
topPriorityAction,
|
|
332
|
-
...elements.filter((e) => e !== topPriorityAction),
|
|
333
|
-
];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Layout the actions available as buttons or menu items
|
|
338
|
-
*
|
|
339
|
-
* If the window is resizing down, just make sure that all buttons fits, and if not,
|
|
340
|
-
* move one to menu and call itself async (to allow re-rendering) and see if we fit.
|
|
341
|
-
* Repeat until the button fits or no buttons are left.
|
|
342
|
-
*
|
|
343
|
-
* If the window is sizing up, try to place a menu item out as a button, call itself
|
|
344
|
-
* async (to allow re-rendering) and see if we fit - if we don't, remove the button again.
|
|
345
|
-
*
|
|
346
|
-
* We also need to keep track of `_scalingUp` between calls since the resize might fire
|
|
347
|
-
* a lot of events, and we don't want to be starting multiple "calculation processes"
|
|
348
|
-
* since this will result in an infinite loop.
|
|
349
|
-
*
|
|
350
|
-
* The actual layouting of actions will be performed by adding or removing the 'button'
|
|
351
|
-
* attribute from the action, which will cause it to match different content insertion
|
|
352
|
-
* points.
|
|
353
|
-
*
|
|
354
|
-
* @returns {void}
|
|
355
|
-
*/
|
|
356
|
-
_layoutActions() {
|
|
357
|
-
const elements = this._getElements(),
|
|
358
|
-
hasActions = elements.length > 0 || this.hasExtraItems;
|
|
359
|
-
this._setHasActions(hasActions);
|
|
360
|
-
|
|
361
|
-
if (!hasActions) {
|
|
362
|
-
// No need to render if we don't have any actions
|
|
363
|
-
return this._setHasMenuItems(false);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const toolbarElements = elements.slice(0, this.maxToolbarItems),
|
|
367
|
-
menuElements = elements.slice(toolbarElements.length);
|
|
368
|
-
toolbarElements.forEach((el) => this._moveElement(el, true));
|
|
369
|
-
menuElements.forEach((el) => this._moveElement(el));
|
|
370
|
-
this._setHasMenuItems(menuElements.length > 0);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
_moveElement(element, toToolbar) {
|
|
374
|
-
const slot = toToolbar ? BOTTOM_BAR_TOOLBAR_SLOT : BOTTOM_BAR_MENU_SLOT,
|
|
375
|
-
tabindex = '0';
|
|
376
|
-
element.setAttribute('slot', slot);
|
|
377
|
-
element.setAttribute('tabindex', tabindex);
|
|
378
|
-
element.classList.toggle(this.menuClass, !toToolbar);
|
|
379
|
-
element.classList.toggle(this.toolbarClass, toToolbar);
|
|
380
|
-
this.updateStyles();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
_onResize([entry]) {
|
|
384
|
-
const hidden =
|
|
385
|
-
entry.borderBoxSize?.[0]?.inlineSize === 0 ||
|
|
386
|
-
entry.contentRect?.width === 0;
|
|
387
|
-
if (hidden) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
this.computedBarHeight = this._computeComputedBarHeight(
|
|
391
|
-
this._matchHeightElement,
|
|
392
|
-
this.barHeight,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
_showHideBottomBar(visible) {
|
|
397
|
-
this.style.transitionDuration = 0;
|
|
398
|
-
this.style.display = '';
|
|
399
|
-
this.style.maxHeight = '';
|
|
400
|
-
|
|
401
|
-
const height = this.computedBarHeight,
|
|
402
|
-
to = !visible ? '0px' : height + 'px';
|
|
403
|
-
|
|
404
|
-
let from = visible ? '0px' : height + 'px';
|
|
405
|
-
|
|
406
|
-
if (!this[rendered]) {
|
|
407
|
-
from = to;
|
|
408
|
-
this[rendered] = true;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
this.style.maxHeight = from;
|
|
412
|
-
|
|
413
|
-
const onEnd = () => {
|
|
414
|
-
this.removeEventListener('transitionend', onEnd);
|
|
415
|
-
this.style.maxHeight = '';
|
|
416
|
-
this.style.display = this.visible ? '' : 'none';
|
|
417
|
-
};
|
|
418
|
-
requestAnimationFrame(() => {
|
|
419
|
-
this.addEventListener('transitionend', onEnd);
|
|
420
|
-
this.style.transitionDuration = '';
|
|
421
|
-
this.style.maxHeight = to;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
customElements.define('cosmoz-bottom-bar', CosmozBottomBar);
|
|
427
|
-
|
|
428
|
-
const tmplt = `
|
|
429
|
-
<slot name="extra" slot="extra"></slot>
|
|
430
|
-
<slot name="bottom-bar-toolbar" slot="bottom-bar-toolbar"></slot>
|
|
431
|
-
<slot name="bottom-bar-menu" slot="bottom-bar-menu"></slot>
|
|
432
|
-
`;
|
|
433
|
-
|
|
434
|
-
export const bottomBarSlots = html(Object.assign([tmplt], { raw: [tmplt] })),
|
|
435
|
-
bottomBarSlotsPolymer = polymerHtml(Object.assign([tmplt], { raw: [tmplt] }));
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
/* eslint-disable max-lines */
|
|
3
|
-
import { html } from 'lit-html';
|
|
4
|
-
import { component, useLayoutEffect } from '@pionjs/pion';
|
|
5
|
-
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
|
|
6
|
-
import { style } from './cosmoz-bottom-bar-next.style.js';
|
|
7
|
-
import { toggleSize } from '@neovici/cosmoz-collapse/toggle';
|
|
8
|
-
import { useActivity } from '@neovici/cosmoz-utils/keybindings/use-activity';
|
|
9
|
-
import '@neovici/cosmoz-dropdown';
|
|
10
|
-
|
|
11
|
-
const BOTTOM_BAR_TOOLBAR_SLOT = 'bottom-bar-toolbar';
|
|
12
|
-
const BOTTOM_BAR_MENU_SLOT = 'bottom-bar-menu';
|
|
13
|
-
|
|
14
|
-
const _moveElement = (element, toToolbar) => {
|
|
15
|
-
const slot = toToolbar ? BOTTOM_BAR_TOOLBAR_SLOT : BOTTOM_BAR_MENU_SLOT;
|
|
16
|
-
const tabindex = '0';
|
|
17
|
-
|
|
18
|
-
element.setAttribute('slot', slot);
|
|
19
|
-
element.setAttribute('tabindex', tabindex);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const _isActionNode = (node) => {
|
|
23
|
-
return (
|
|
24
|
-
node.nodeType === Node.ELEMENT_NODE &&
|
|
25
|
-
node.getAttribute('slot') !== 'info' &&
|
|
26
|
-
node.tagName !== 'TEMPLATE' &&
|
|
27
|
-
node.tagName !== 'STYLE' &&
|
|
28
|
-
node.tagName !== 'DOM-REPEAT' &&
|
|
29
|
-
node.tagName !== 'DOM-IF' &&
|
|
30
|
-
node.getAttribute('slot') !== 'extra'
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const getFlattenedNodes = (element) => {
|
|
35
|
-
const childNodes = [...element.childNodes];
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < element.childNodes.length; i++) {
|
|
38
|
-
const node = element.childNodes[i];
|
|
39
|
-
if (node.tagName === 'SLOT') {
|
|
40
|
-
// remove current slot element
|
|
41
|
-
childNodes.splice(i, 1);
|
|
42
|
-
|
|
43
|
-
// append slot elements to the current index
|
|
44
|
-
const slotElements = node.assignedElements({ flatten: true });
|
|
45
|
-
for (let j = 0; j < slotElements.length; j++) {
|
|
46
|
-
const slotElement = slotElements[j];
|
|
47
|
-
|
|
48
|
-
childNodes.splice(i + j, 0, slotElement);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return childNodes;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const _getElements = (host) => {
|
|
57
|
-
const elements = getFlattenedNodes(host)
|
|
58
|
-
.filter(_isActionNode)
|
|
59
|
-
.filter((element) => !element.hidden)
|
|
60
|
-
.sort((a, b) => (a.dataset.index ?? 0) - (b.dataset.index ?? 0));
|
|
61
|
-
|
|
62
|
-
if (elements.length === 0) {
|
|
63
|
-
return elements;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const topPriorityAction = elements.reduce(
|
|
67
|
-
(top, element) => {
|
|
68
|
-
return parseInt(top.dataset.priority ?? 0, 10) >=
|
|
69
|
-
parseInt(element.dataset.priority ?? 0, 10)
|
|
70
|
-
? top
|
|
71
|
-
: element;
|
|
72
|
-
},
|
|
73
|
-
{ dataset: { priority: '-1000' } },
|
|
74
|
-
[],
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
return [
|
|
78
|
-
topPriorityAction,
|
|
79
|
-
...elements.filter((e) => e !== topPriorityAction),
|
|
80
|
-
];
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Layout the actions available as buttons or menu items
|
|
85
|
-
*
|
|
86
|
-
* If the window is resizing down, just make sure that all buttons fits, and if not,
|
|
87
|
-
* move one to menu and call itself async (to allow re-rendering) and see if we fit.
|
|
88
|
-
* Repeat until the button fits or no buttons are left.
|
|
89
|
-
*
|
|
90
|
-
* If the window is sizing up, try to place a menu item out as a button, call itself
|
|
91
|
-
* async (to allow re-rendering) and see if we fit - if we don't, remove the button again.
|
|
92
|
-
*
|
|
93
|
-
* We also need to keep track of `_scalingUp` between calls since the resize might fire
|
|
94
|
-
* a lot of events, and we don't want to be starting multiple "calculation processes"
|
|
95
|
-
* since this will result in an infinite loop.
|
|
96
|
-
*
|
|
97
|
-
* The actual layouting of actions will be performed by adding or removing the 'button'
|
|
98
|
-
* attribute from the action, which will cause it to match different content insertion
|
|
99
|
-
* points.
|
|
100
|
-
*
|
|
101
|
-
* @param {*} host The current element
|
|
102
|
-
* @param {*} maxToolbarItems Maximum items for the toolbar
|
|
103
|
-
* @returns {void}
|
|
104
|
-
*/
|
|
105
|
-
const _layoutActions = (host, maxToolbarItems) => {
|
|
106
|
-
// eslint-disable-line max-statements
|
|
107
|
-
const elements = _getElements(host);
|
|
108
|
-
const hasActions = elements.length > 0;
|
|
109
|
-
|
|
110
|
-
if (!hasActions) {
|
|
111
|
-
// No need to render if we don't have any actions
|
|
112
|
-
return host.toggleAttribute('has-menu-items', false);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const toolbarElements = elements.slice(0, maxToolbarItems);
|
|
116
|
-
const menuElements = elements.slice(toolbarElements.length);
|
|
117
|
-
|
|
118
|
-
toolbarElements.forEach((el) => _moveElement(el, true));
|
|
119
|
-
menuElements.forEach((el) => _moveElement(el));
|
|
120
|
-
host.toggleAttribute('has-menu-items', menuElements.length > 0);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export const openMenu = Symbol('openMenu');
|
|
124
|
-
|
|
125
|
-
const openActionsMenu = (host) => {
|
|
126
|
-
const dropdown = host.shadowRoot.querySelector('#dropdown');
|
|
127
|
-
|
|
128
|
-
if (dropdown.hasAttribute('hidden')) return;
|
|
129
|
-
|
|
130
|
-
//TODO: Clean up when open function is implemented for cosmoz-dropdown-menu
|
|
131
|
-
dropdown.shadowRoot
|
|
132
|
-
.querySelector('cosmoz-dropdown')
|
|
133
|
-
.shadowRoot.getElementById('dropdownButton')
|
|
134
|
-
.click();
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* `<cosmoz-bottom-bar>` is a horizontal responsive bottom toolbar containing items that
|
|
139
|
-
* can be used for actions.
|
|
140
|
-
*
|
|
141
|
-
* The items placed inside the `cosmoz-bottom-bar` are distributed into the toolbar in a horizontal container.
|
|
142
|
-
* If the items do not fit the available width, those that do not fit are placed in a dropdown
|
|
143
|
-
* menu triggered by a button in the toolbar.
|
|
144
|
-
* The class specified by the property `toolbarClass` (default `cosmoz-bottom-bar-toolbar`)
|
|
145
|
-
* is applied to items distributed to the toolbar.
|
|
146
|
-
* The class specified in the property `menuClass` (default `cosmoz-bottom-bar-menu`)
|
|
147
|
-
* is applied to items distributed to the menu.
|
|
148
|
-
*
|
|
149
|
-
* ### Usage
|
|
150
|
-
*
|
|
151
|
-
* See demo for example usage
|
|
152
|
-
*
|
|
153
|
-
* @element cosmoz-bottom-bar
|
|
154
|
-
* @demo demo/bottom-bar-next.html Basic Demo
|
|
155
|
-
*/
|
|
156
|
-
// eslint-disable-next-line max-statements
|
|
157
|
-
const CosmozBottomBar = ({ active = false, maxToolbarItems = 1 }) => {
|
|
158
|
-
const host = useHost();
|
|
159
|
-
|
|
160
|
-
useActivity(
|
|
161
|
-
{
|
|
162
|
-
activity: openMenu,
|
|
163
|
-
callback: () => openActionsMenu(host),
|
|
164
|
-
check: () => host.active && !host.hasAttribute('hide-actions'),
|
|
165
|
-
element: () => host.shadowRoot.querySelector('#dropdown'),
|
|
166
|
-
},
|
|
167
|
-
[host.active],
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const toggle = toggleSize('height');
|
|
171
|
-
|
|
172
|
-
useLayoutEffect(() => {
|
|
173
|
-
toggle(host, active);
|
|
174
|
-
}, [active]);
|
|
175
|
-
|
|
176
|
-
const slotChangeHandler = () => {
|
|
177
|
-
_layoutActions(host, maxToolbarItems);
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
return html`<div id="bar" part="bar">
|
|
181
|
-
<div id="info" part="info"><slot name="info"></slot></div>
|
|
182
|
-
<slot
|
|
183
|
-
id="bottomBarToolbar"
|
|
184
|
-
name="bottom-bar-toolbar"
|
|
185
|
-
@slotchange=${slotChangeHandler}
|
|
186
|
-
></slot>
|
|
187
|
-
<cosmoz-dropdown-menu id="dropdown">
|
|
188
|
-
<svg
|
|
189
|
-
slot="button"
|
|
190
|
-
width="4"
|
|
191
|
-
height="16"
|
|
192
|
-
viewBox="0 0 4 16"
|
|
193
|
-
fill="none"
|
|
194
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
195
|
-
>
|
|
196
|
-
<path
|
|
197
|
-
fill-rule="evenodd"
|
|
198
|
-
clip-rule="evenodd"
|
|
199
|
-
d="M1.50996e-07 2C1.02714e-07 3.10457 0.89543 4 2 4C3.10457 4 4 3.10457 4 2C4 0.89543 3.10457 -3.91405e-08 2 -8.74228e-08C0.895431 -1.35705e-07 1.99278e-07 0.89543 1.50996e-07 2Z"
|
|
200
|
-
fill="white"
|
|
201
|
-
/>
|
|
202
|
-
<path
|
|
203
|
-
fill-rule="evenodd"
|
|
204
|
-
clip-rule="evenodd"
|
|
205
|
-
d="M1.50996e-07 8C1.02714e-07 9.10457 0.89543 10 2 10C3.10457 10 4 9.10457 4 8C4 6.89543 3.10457 6 2 6C0.895431 6 1.99278e-07 6.89543 1.50996e-07 8Z"
|
|
206
|
-
fill="white"
|
|
207
|
-
/>
|
|
208
|
-
<path
|
|
209
|
-
fill-rule="evenodd"
|
|
210
|
-
clip-rule="evenodd"
|
|
211
|
-
d="M1.50996e-07 14C1.02714e-07 15.1046 0.89543 16 2 16C3.10457 16 4 15.1046 4 14C4 12.8954 3.10457 12 2 12C0.895431 12 1.99278e-07 12.8954 1.50996e-07 14Z"
|
|
212
|
-
fill="white"
|
|
213
|
-
/>
|
|
214
|
-
</svg>
|
|
215
|
-
<slot id="bottomBarMenu" name="bottom-bar-menu"></slot>
|
|
216
|
-
</cosmoz-dropdown-menu>
|
|
217
|
-
<slot name="extra" id="extraSlot"></slot>
|
|
218
|
-
</div>
|
|
219
|
-
<div hidden style="display:none">
|
|
220
|
-
<slot id="content" @slotchange=${slotChangeHandler}></slot>
|
|
221
|
-
</div>`;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
export default CosmozBottomBar;
|
|
225
|
-
|
|
226
|
-
customElements.define(
|
|
227
|
-
'cosmoz-bottom-bar-next',
|
|
228
|
-
component(CosmozBottomBar, {
|
|
229
|
-
observedAttributes: ['active'],
|
|
230
|
-
styleSheets: [style],
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
import { css } from '@neovici/cosmoz-utils';
|
|
3
|
-
|
|
4
|
-
export const style = css`
|
|
5
|
-
:host {
|
|
6
|
-
display: block;
|
|
7
|
-
overflow: hidden;
|
|
8
|
-
bottom: 0;
|
|
9
|
-
left: 0;
|
|
10
|
-
width: 100%;
|
|
11
|
-
max-width: 100%; /* Firefox fix */
|
|
12
|
-
background-color: inherit;
|
|
13
|
-
transition: max-height 0.3s ease;
|
|
14
|
-
flex: none;
|
|
15
|
-
background-color: var(
|
|
16
|
-
--cosmoz-bottom-bar-bg-color,
|
|
17
|
-
rgba(230, 230, 230, 0.8)
|
|
18
|
-
);
|
|
19
|
-
box-shadow: var(--cosmoz-bottom-bar-shadow, none);
|
|
20
|
-
z-index: 1;
|
|
21
|
-
|
|
22
|
-
--cosmoz-dropdown-anchor-spacing: 12px 6px;
|
|
23
|
-
--paper-button: {
|
|
24
|
-
background-color: inherit;
|
|
25
|
-
text-transform: none;
|
|
26
|
-
border: 0;
|
|
27
|
-
border-radius: 0;
|
|
28
|
-
font-size: inherit;
|
|
29
|
-
color: inherit;
|
|
30
|
-
font-weight: inherit;
|
|
31
|
-
margin: 0;
|
|
32
|
-
padding: 0;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
:host([force-open]) {
|
|
36
|
-
transition: none;
|
|
37
|
-
}
|
|
38
|
-
[hidden],
|
|
39
|
-
::slotted([hidden]) {
|
|
40
|
-
display: none !important;
|
|
41
|
-
}
|
|
42
|
-
#bar {
|
|
43
|
-
height: 64px;
|
|
44
|
-
padding: 0 3%;
|
|
45
|
-
display: flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
}
|
|
48
|
-
#info {
|
|
49
|
-
min-width: 5px;
|
|
50
|
-
padding-right: 3%;
|
|
51
|
-
margin-right: auto;
|
|
52
|
-
white-space: nowrap;
|
|
53
|
-
}
|
|
54
|
-
#bottomBarToolbar::slotted(:not(slot):not([unstyled])) {
|
|
55
|
-
margin: 0 0.29em;
|
|
56
|
-
min-width: 40px;
|
|
57
|
-
min-height: 40px;
|
|
58
|
-
text-overflow: ellipsis;
|
|
59
|
-
white-space: nowrap;
|
|
60
|
-
background: var(
|
|
61
|
-
--cosmoz-bottom-bar-button-bg-color,
|
|
62
|
-
var(--cosmoz-button-bg-color, #101010)
|
|
63
|
-
);
|
|
64
|
-
color: var(
|
|
65
|
-
--cosmoz-bottom-bar-button-color,
|
|
66
|
-
var(--cosmoz-button-color, #fff)
|
|
67
|
-
);
|
|
68
|
-
border-radius: 6px;
|
|
69
|
-
padding: 0 18px;
|
|
70
|
-
font-size: 14px;
|
|
71
|
-
font-weight: 500;
|
|
72
|
-
line-height: 40px;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#bottomBarToolbar::slotted(:not(slot)[disabled]) {
|
|
76
|
-
opacity: var(--cosmoz-button-disabled-opacity, 0.15);
|
|
77
|
-
pointer-events: none;
|
|
78
|
-
}
|
|
79
|
-
#bottomBarToolbar::slotted(:not(slot):hover) {
|
|
80
|
-
background: var(
|
|
81
|
-
--cosmoz-bottom-bar-button-hover-bg-color,
|
|
82
|
-
var(--cosmoz-button-hover-bg-color, #3a3f44)
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
#dropdown::part(content) {
|
|
86
|
-
max-width: 300px;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
:host([hide-actions]) #bottomBarToolbar,
|
|
90
|
-
:host([hide-actions]) #bottomBarMenu,
|
|
91
|
-
:host([hide-actions]) #dropdown {
|
|
92
|
-
display: none;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
:host(:not([has-menu-items])) cosmoz-dropdown-menu {
|
|
96
|
-
display: none;
|
|
97
|
-
}
|
|
98
|
-
`;
|