@schukai/monster 4.144.0 → 4.145.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/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.145.0"}
|
|
@@ -193,6 +193,19 @@ const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT =
|
|
|
193
193
|
const ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER =
|
|
194
194
|
"data-monster-option-layout-stacked-breakpoint-container";
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* @private
|
|
198
|
+
* @type {string}
|
|
199
|
+
*/
|
|
200
|
+
const ATTRIBUTE_OPTION_LAYOUT_HIDE_WHEN_EMPTY =
|
|
201
|
+
"data-monster-option-layout-hide-when-empty";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @private
|
|
205
|
+
* @type {string}
|
|
206
|
+
*/
|
|
207
|
+
const ATTRIBUTE_AUTO_HIDDEN_EMPTY = "data-monster-empty-hidden";
|
|
208
|
+
|
|
196
209
|
/**
|
|
197
210
|
* @private
|
|
198
211
|
* @type {string}
|
|
@@ -250,6 +263,7 @@ class ControlBar extends CustomElement {
|
|
|
250
263
|
stackedAlignment: undefined,
|
|
251
264
|
stackedBreakpoint: undefined,
|
|
252
265
|
stackedBreakpointContainer: undefined,
|
|
266
|
+
hideWhenEmpty: false,
|
|
253
267
|
},
|
|
254
268
|
popper: {
|
|
255
269
|
placement: "left",
|
|
@@ -309,7 +323,8 @@ class ControlBar extends CustomElement {
|
|
|
309
323
|
path === "layout.alignment" ||
|
|
310
324
|
path === "layout.stackedAlignment" ||
|
|
311
325
|
path === "layout.stackedBreakpoint" ||
|
|
312
|
-
path === "layout.stackedBreakpointContainer"
|
|
326
|
+
path === "layout.stackedBreakpointContainer" ||
|
|
327
|
+
path === "layout.hideWhenEmpty"
|
|
313
328
|
) {
|
|
314
329
|
syncLayoutState.call(this, {
|
|
315
330
|
observe: path === "layout.stackedBreakpointContainer",
|
|
@@ -366,6 +381,7 @@ class ControlBar extends CustomElement {
|
|
|
366
381
|
attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
|
|
367
382
|
attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT);
|
|
368
383
|
attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_BREAKPOINT_CONTAINER);
|
|
384
|
+
attributes.push(ATTRIBUTE_OPTION_LAYOUT_HIDE_WHEN_EMPTY);
|
|
369
385
|
return attributes;
|
|
370
386
|
}
|
|
371
387
|
|
|
@@ -572,6 +588,11 @@ function initEventHandler() {
|
|
|
572
588
|
syncLayoutState.call(self, { observe: true });
|
|
573
589
|
};
|
|
574
590
|
|
|
591
|
+
self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_HIDE_WHEN_EMPTY] =
|
|
592
|
+
() => {
|
|
593
|
+
syncLayoutState.call(self);
|
|
594
|
+
};
|
|
595
|
+
|
|
575
596
|
self[resizeObserverSymbol] = new ResizeObserver((entries) => {
|
|
576
597
|
if (self[layoutStateSymbol]?.suppressResize) {
|
|
577
598
|
return;
|
|
@@ -655,6 +676,8 @@ function runLayout() {
|
|
|
655
676
|
state.running = true;
|
|
656
677
|
|
|
657
678
|
new Processing(() => {
|
|
679
|
+
syncEmptyVisibility.call(this);
|
|
680
|
+
|
|
658
681
|
if (needsObserve) {
|
|
659
682
|
updateResizeObserverObservation.call(this);
|
|
660
683
|
}
|
|
@@ -845,6 +868,97 @@ function rearrangeItems() {
|
|
|
845
868
|
if (!shouldShowSwitch) {
|
|
846
869
|
hide.call(this);
|
|
847
870
|
}
|
|
871
|
+
syncEmptyVisibility.call(this);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* @private
|
|
876
|
+
* @return {void}
|
|
877
|
+
*/
|
|
878
|
+
function syncEmptyVisibility() {
|
|
879
|
+
if (this.getOption("layout.hideWhenEmpty", false) !== true) {
|
|
880
|
+
clearEmptyVisibilityState.call(this);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (hasVisibleControlItem.call(this)) {
|
|
885
|
+
clearEmptyVisibilityState.call(this);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (this.hasAttribute(ATTRIBUTE_AUTO_HIDDEN_EMPTY)) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (this.hasAttribute("hidden")) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this.setAttribute("hidden", "");
|
|
898
|
+
this.setAttribute(ATTRIBUTE_AUTO_HIDDEN_EMPTY, "");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* @private
|
|
903
|
+
* @return {void}
|
|
904
|
+
*/
|
|
905
|
+
function clearEmptyVisibilityState() {
|
|
906
|
+
if (this.hasAttribute(ATTRIBUTE_AUTO_HIDDEN_EMPTY)) {
|
|
907
|
+
this.removeAttribute(ATTRIBUTE_AUTO_HIDDEN_EMPTY);
|
|
908
|
+
if (this.hasAttribute("hidden")) {
|
|
909
|
+
this.removeAttribute("hidden");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @private
|
|
916
|
+
* @return {boolean}
|
|
917
|
+
*/
|
|
918
|
+
function hasVisibleControlItem() {
|
|
919
|
+
return getControlItems.call(this).some((item) => {
|
|
920
|
+
return isControlItemVisible.call(this, item);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* @private
|
|
926
|
+
* @return {HTMLElement[]}
|
|
927
|
+
*/
|
|
928
|
+
function getControlItems() {
|
|
929
|
+
return Array.from(this.children).filter((item) => {
|
|
930
|
+
if (!(item instanceof HTMLElement)) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
return item.slot === "" || item.slot === "popper";
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* @private
|
|
939
|
+
* @param {HTMLElement} item
|
|
940
|
+
* @return {boolean}
|
|
941
|
+
*/
|
|
942
|
+
function isControlItemVisible(item) {
|
|
943
|
+
if (isControlBarSpacerElement(item) || isElementSelfHidden(item)) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const computedStyle = getComputedStyle(item);
|
|
948
|
+
if (computedStyle.opacity === "0") {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const containedControls = getContainedControlElements(item);
|
|
953
|
+
if (containedControls.length === 0) {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return containedControls.some((control) => {
|
|
958
|
+
return (
|
|
959
|
+
!isElementSelfHidden(control) && getComputedStyle(control).opacity !== "0"
|
|
960
|
+
);
|
|
961
|
+
});
|
|
848
962
|
}
|
|
849
963
|
|
|
850
964
|
/**
|
|
@@ -6,6 +6,8 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
|
|
|
6
6
|
let expect = chai.expect;
|
|
7
7
|
chai.use(chaiDom);
|
|
8
8
|
|
|
9
|
+
const waitForLayout = () => new Promise((resolve) => setTimeout(resolve, 120));
|
|
10
|
+
|
|
9
11
|
const html = `
|
|
10
12
|
<div id="test1">
|
|
11
13
|
<monster-button-bar id="bar"></monster-button-bar>
|
|
@@ -80,6 +82,112 @@ describe("ButtonBar", function () {
|
|
|
80
82
|
}, 50);
|
|
81
83
|
});
|
|
82
84
|
|
|
85
|
+
it("should hide an empty button bar when configured to hide empty bars", async function () {
|
|
86
|
+
const mocks = document.getElementById("mocks");
|
|
87
|
+
mocks.innerHTML = `
|
|
88
|
+
<monster-button-bar
|
|
89
|
+
id="empty-auto-hidden-button-bar"
|
|
90
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
91
|
+
></monster-button-bar>
|
|
92
|
+
`;
|
|
93
|
+
const bar = document.getElementById("empty-auto-hidden-button-bar");
|
|
94
|
+
|
|
95
|
+
await waitForLayout();
|
|
96
|
+
|
|
97
|
+
expect(bar.getOption("layout.hideWhenEmpty")).to.equal(true);
|
|
98
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
99
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should reveal an auto-hidden button bar when a button becomes visible", async function () {
|
|
103
|
+
const mocks = document.getElementById("mocks");
|
|
104
|
+
mocks.innerHTML = `
|
|
105
|
+
<monster-button-bar
|
|
106
|
+
id="dynamic-auto-hidden-button-bar"
|
|
107
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
108
|
+
>
|
|
109
|
+
<button id="dynamic-auto-hidden-button" hidden>Run</button>
|
|
110
|
+
</monster-button-bar>
|
|
111
|
+
`;
|
|
112
|
+
const bar = document.getElementById("dynamic-auto-hidden-button-bar");
|
|
113
|
+
const button = document.getElementById("dynamic-auto-hidden-button");
|
|
114
|
+
|
|
115
|
+
await waitForLayout();
|
|
116
|
+
|
|
117
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
118
|
+
|
|
119
|
+
button.removeAttribute("hidden");
|
|
120
|
+
await waitForLayout();
|
|
121
|
+
|
|
122
|
+
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
123
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should keep an auto-hidden button bar visible when buttons overflow into the popper", async function () {
|
|
127
|
+
const mocks = document.getElementById("mocks");
|
|
128
|
+
mocks.innerHTML = `
|
|
129
|
+
<div id="overflow-auto-hidden-wrapper">
|
|
130
|
+
<monster-button-bar
|
|
131
|
+
id="overflow-auto-hidden-button-bar"
|
|
132
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
133
|
+
>
|
|
134
|
+
<button id="overflow-auto-hidden-a" type="button">One</button>
|
|
135
|
+
<button id="overflow-auto-hidden-b" type="button">Two</button>
|
|
136
|
+
</monster-button-bar>
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
const wrapper = document.getElementById("overflow-auto-hidden-wrapper");
|
|
140
|
+
const bar = document.getElementById("overflow-auto-hidden-button-bar");
|
|
141
|
+
const buttons = [
|
|
142
|
+
document.getElementById("overflow-auto-hidden-a"),
|
|
143
|
+
document.getElementById("overflow-auto-hidden-b"),
|
|
144
|
+
];
|
|
145
|
+
const switchButton = bar.shadowRoot.querySelector(
|
|
146
|
+
'[data-monster-role="switch"]',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
wrapper.style.boxSizing = "border-box";
|
|
150
|
+
wrapper.style.width = "30px";
|
|
151
|
+
Object.defineProperty(wrapper, "clientWidth", {
|
|
152
|
+
configurable: true,
|
|
153
|
+
value: 30,
|
|
154
|
+
});
|
|
155
|
+
Object.defineProperty(switchButton, "offsetWidth", {
|
|
156
|
+
configurable: true,
|
|
157
|
+
value: 20,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
for (const button of buttons) {
|
|
161
|
+
Object.defineProperty(button, "offsetWidth", {
|
|
162
|
+
configurable: true,
|
|
163
|
+
value: 48,
|
|
164
|
+
});
|
|
165
|
+
Object.defineProperty(button, "offsetHeight", {
|
|
166
|
+
configurable: true,
|
|
167
|
+
value: 30,
|
|
168
|
+
});
|
|
169
|
+
button.getBoundingClientRect = () => ({
|
|
170
|
+
width: 48,
|
|
171
|
+
height: 30,
|
|
172
|
+
top: 0,
|
|
173
|
+
right: 48,
|
|
174
|
+
bottom: 30,
|
|
175
|
+
left: 0,
|
|
176
|
+
x: 0,
|
|
177
|
+
y: 0,
|
|
178
|
+
toJSON: () => {},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await waitForLayout();
|
|
183
|
+
|
|
184
|
+
expect(buttons.every((button) => button.getAttribute("slot") === "popper"))
|
|
185
|
+
.to.be.true;
|
|
186
|
+
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
187
|
+
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
188
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
189
|
+
});
|
|
190
|
+
|
|
83
191
|
it("should default the button bar layout alignment to left", function () {
|
|
84
192
|
const bar = document.getElementById("bar");
|
|
85
193
|
const buttonBar = bar.shadowRoot.querySelector(
|
|
@@ -6,6 +6,8 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
|
|
|
6
6
|
const expect = chai.expect;
|
|
7
7
|
chai.use(chaiDom);
|
|
8
8
|
|
|
9
|
+
const waitForLayout = () => new Promise((resolve) => setTimeout(resolve, 120));
|
|
10
|
+
|
|
9
11
|
const html = `
|
|
10
12
|
<div id="test1">
|
|
11
13
|
<monster-control-bar id="bar">
|
|
@@ -97,6 +99,119 @@ describe("ControlBar", function () {
|
|
|
97
99
|
expect(controlBar.style.opacity).to.not.equal("0");
|
|
98
100
|
});
|
|
99
101
|
|
|
102
|
+
it("should keep an empty control bar visible by default", async function () {
|
|
103
|
+
const bar = document.getElementById("bar-right");
|
|
104
|
+
|
|
105
|
+
await waitForLayout();
|
|
106
|
+
|
|
107
|
+
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
108
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should hide an empty control bar when the empty option is enabled at runtime", async function () {
|
|
112
|
+
const bar = document.getElementById("bar-right");
|
|
113
|
+
|
|
114
|
+
await waitForLayout();
|
|
115
|
+
|
|
116
|
+
bar.setOption("layout.hideWhenEmpty", true);
|
|
117
|
+
await waitForLayout();
|
|
118
|
+
|
|
119
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
120
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should hide an empty control bar when configured to hide empty bars", async function () {
|
|
124
|
+
const mocks = document.getElementById("mocks");
|
|
125
|
+
mocks.innerHTML = `
|
|
126
|
+
<monster-control-bar
|
|
127
|
+
id="empty-auto-hidden-bar"
|
|
128
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
129
|
+
></monster-control-bar>
|
|
130
|
+
`;
|
|
131
|
+
const bar = document.getElementById("empty-auto-hidden-bar");
|
|
132
|
+
|
|
133
|
+
await waitForLayout();
|
|
134
|
+
|
|
135
|
+
expect(bar.getOption("layout.hideWhenEmpty")).to.equal(true);
|
|
136
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
137
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should update empty control bar visibility when child controls become visible", async function () {
|
|
141
|
+
const mocks = document.getElementById("mocks");
|
|
142
|
+
mocks.innerHTML = `
|
|
143
|
+
<monster-control-bar
|
|
144
|
+
id="dynamic-auto-hidden-bar"
|
|
145
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
146
|
+
>
|
|
147
|
+
<button id="dynamic-auto-hidden-button" hidden>Run</button>
|
|
148
|
+
</monster-control-bar>
|
|
149
|
+
`;
|
|
150
|
+
const bar = document.getElementById("dynamic-auto-hidden-bar");
|
|
151
|
+
const button = document.getElementById("dynamic-auto-hidden-button");
|
|
152
|
+
|
|
153
|
+
await waitForLayout();
|
|
154
|
+
|
|
155
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
156
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
157
|
+
|
|
158
|
+
button.removeAttribute("hidden");
|
|
159
|
+
await waitForLayout();
|
|
160
|
+
|
|
161
|
+
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
162
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
163
|
+
|
|
164
|
+
button.style.display = "none";
|
|
165
|
+
await waitForLayout();
|
|
166
|
+
|
|
167
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
168
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should release empty auto-hidden state when the option is disabled", async function () {
|
|
172
|
+
const mocks = document.getElementById("mocks");
|
|
173
|
+
mocks.innerHTML = `
|
|
174
|
+
<monster-control-bar
|
|
175
|
+
id="option-auto-hidden-bar"
|
|
176
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
177
|
+
></monster-control-bar>
|
|
178
|
+
`;
|
|
179
|
+
const bar = document.getElementById("option-auto-hidden-bar");
|
|
180
|
+
|
|
181
|
+
await waitForLayout();
|
|
182
|
+
|
|
183
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
184
|
+
|
|
185
|
+
bar.setOption("layout.hideWhenEmpty", false);
|
|
186
|
+
await waitForLayout();
|
|
187
|
+
|
|
188
|
+
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
189
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should preserve a manual hidden attribute when empty hiding is disabled", async function () {
|
|
193
|
+
const mocks = document.getElementById("mocks");
|
|
194
|
+
mocks.innerHTML = `
|
|
195
|
+
<monster-control-bar
|
|
196
|
+
id="manual-hidden-bar"
|
|
197
|
+
hidden
|
|
198
|
+
data-monster-option-layout-hide-when-empty="true"
|
|
199
|
+
></monster-control-bar>
|
|
200
|
+
`;
|
|
201
|
+
const bar = document.getElementById("manual-hidden-bar");
|
|
202
|
+
|
|
203
|
+
await waitForLayout();
|
|
204
|
+
|
|
205
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
206
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
207
|
+
|
|
208
|
+
bar.setOption("layout.hideWhenEmpty", false);
|
|
209
|
+
await waitForLayout();
|
|
210
|
+
|
|
211
|
+
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
212
|
+
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|
|
213
|
+
});
|
|
214
|
+
|
|
100
215
|
it("should allow configuring the control bar layout alignment to right", function (done) {
|
|
101
216
|
const bar = document.getElementById("bar-right");
|
|
102
217
|
|