@schukai/monster 4.143.4 → 4.143.6

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.143.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.143.6"}
@@ -155,6 +155,18 @@ const debounceSizeSymbol = Symbol("debounceSize");
155
155
  */
156
156
  const hashChangeSymbol = Symbol("hashChange");
157
157
 
158
+ /**
159
+ * @private
160
+ * @type {symbol}
161
+ */
162
+ const pendingConfigWriteSymbol = Symbol("pendingConfigWrite");
163
+
164
+ /**
165
+ * @private
166
+ * @type {symbol}
167
+ */
168
+ const configWritePromiseSymbol = Symbol("configWritePromise");
169
+
158
170
  /**
159
171
  * The Filter component is used to show and handle the filter values.
160
172
  *
@@ -1089,6 +1101,10 @@ function initTabEvents() {
1089
1101
  return host.getConfig(configKey);
1090
1102
  })
1091
1103
  .then((config) => {
1104
+ if (!isObject(config)) {
1105
+ return;
1106
+ }
1107
+
1092
1108
  for (const [name, query] of Object.entries(config)) {
1093
1109
  if (labels.includes(name)) {
1094
1110
  continue;
@@ -1130,6 +1146,10 @@ function updateFilterTabs() {
1130
1146
  return host.getConfig(configKey);
1131
1147
  })
1132
1148
  .then((config) => {
1149
+ if (!isObject(config)) {
1150
+ return;
1151
+ }
1152
+
1133
1153
  for (const [name, query] of Object.entries(config)) {
1134
1154
  const found = element.querySelector(
1135
1155
  `[data-monster-button-label="${name}"]`,
@@ -1629,12 +1649,51 @@ function updateConfig() {
1629
1649
  if (!(host && hasConfigIdentity.call(this))) {
1630
1650
  return;
1631
1651
  }
1652
+
1632
1653
  const configKey = getFilterConfigKey.call(this);
1654
+ this[pendingConfigWriteSymbol] = {
1655
+ key: configKey,
1656
+ value: this[settingsSymbol].getOptions(),
1657
+ };
1658
+
1659
+ if (this[configWritePromiseSymbol]) {
1660
+ return;
1661
+ }
1662
+
1663
+ writePendingConfig.call(this, host);
1664
+ }
1665
+
1666
+ /**
1667
+ * @private
1668
+ * @param {Host} host
1669
+ */
1670
+ function writePendingConfig(host) {
1671
+ const pending = this[pendingConfigWriteSymbol];
1672
+ delete this[pendingConfigWriteSymbol];
1673
+
1674
+ if (!pending) {
1675
+ return;
1676
+ }
1633
1677
 
1634
1678
  try {
1635
- host.setConfig(configKey, this[settingsSymbol].getOptions());
1679
+ this[configWritePromiseSymbol] = Promise.resolve(
1680
+ host.setConfig(pending.key, pending.value),
1681
+ )
1682
+ .catch((error) => {
1683
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error?.message || error);
1684
+ })
1685
+ .finally(() => {
1686
+ delete this[configWritePromiseSymbol];
1687
+ if (this[pendingConfigWriteSymbol]) {
1688
+ writePendingConfig.call(this, host);
1689
+ }
1690
+ });
1636
1691
  } catch (error) {
1692
+ delete this[configWritePromiseSymbol];
1637
1693
  addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error?.message || error);
1694
+ if (this[pendingConfigWriteSymbol]) {
1695
+ writePendingConfig.call(this, host);
1696
+ }
1638
1697
  }
1639
1698
  }
1640
1699
 
@@ -82,6 +82,8 @@ function parseBracketedKeyValueHash(hashString) {
82
82
  let inSelector = true;
83
83
  let escaped = false;
84
84
  let quotedValueStartChar = "";
85
+ let valueParenthesisDepth = 0;
86
+ let valueClosedByParenthesis = false;
85
87
 
86
88
  for (let i = 0; i < cleanedHashString.length; i++) {
87
89
  const c = cleanedHashString[i];
@@ -166,6 +168,19 @@ function parseBracketedKeyValueHash(hashString) {
166
168
  continue;
167
169
  }
168
170
 
171
+ if (c === "(") {
172
+ valueParenthesisDepth++;
173
+ currentValue += c;
174
+ continue;
175
+ }
176
+
177
+ if (c === ")" && valueParenthesisDepth > 0) {
178
+ valueParenthesisDepth--;
179
+ currentValue += c;
180
+ valueClosedByParenthesis = valueParenthesisDepth === 0;
181
+ continue;
182
+ }
183
+
169
184
  if (c === ",") {
170
185
  inValue = false;
171
186
  inKey = true;
@@ -173,6 +188,8 @@ function parseBracketedKeyValueHash(hashString) {
173
188
  addToResult(currentKey, decodedCurrentValue);
174
189
  currentKey = "";
175
190
  currentValue = "";
191
+ valueParenthesisDepth = 0;
192
+ valueClosedByParenthesis = false;
176
193
  continue;
177
194
  }
178
195
 
@@ -186,6 +203,8 @@ function parseBracketedKeyValueHash(hashString) {
186
203
  currentKey = "";
187
204
  currentValue = "";
188
205
  currentSelector = "";
206
+ valueParenthesisDepth = 0;
207
+ valueClosedByParenthesis = false;
189
208
  continue;
190
209
  }
191
210
 
@@ -199,6 +218,12 @@ function parseBracketedKeyValueHash(hashString) {
199
218
  return selectors;
200
219
  }
201
220
 
221
+ if (inValue && valueParenthesisDepth === 0 && valueClosedByParenthesis) {
222
+ const decodedCurrentValue = decodeURIComponent(currentValue);
223
+ addToResult(currentKey, decodedCurrentValue);
224
+ return selectors;
225
+ }
226
+
202
227
  return {};
203
228
  }
204
229
 
@@ -106,6 +106,16 @@ describe("parseBracketedKeyValueHash", () => {
106
106
  expect(result).to.deep.equal({selector: {key1: 'value,1', key2: 'value,2'}});
107
107
  });
108
108
 
109
+ it('should keep unencoded parentheses inside a percent-encoded filter expression', () => {
110
+ const hashString = '#list-filter-nucleus-pim-product-variants-list(tags=tags%20IN%20(%22gustav-varianten%22)';
111
+ const result = parseBracketedKeyValueHash(hashString);
112
+ expect(result).to.deep.equal({
113
+ 'list-filter-nucleus-pim-product-variants-list': {
114
+ tags: 'tags IN ("gustav-varianten")'
115
+ }
116
+ });
117
+ });
118
+
109
119
  it('should ignore leading hash symbol (#)', () => {
110
120
  const hashString = 'selector(key=value)';
111
121
  const result = parseBracketedKeyValueHash(hashString);
@@ -195,6 +205,18 @@ describe("parseBracketedKeyValueHash", () => {
195
205
  const result = createBracketedKeyValueHash(input, true);
196
206
  expect(result).to.deep.equal("#.example(color=r%22ed,font-size=14px);.other(background=blue)");
197
207
  });
208
+
209
+ it('should round-trip a filter expression with quotes and parentheses', () => {
210
+ const input = {
211
+ 'list-filter-nucleus-pim-product-variants-list': {
212
+ tags: 'tags IN ("gustav-varianten")'
213
+ }
214
+ };
215
+
216
+ const hashString = createBracketedKeyValueHash(input, true);
217
+ const result = parseBracketedKeyValueHash(hashString);
218
+ expect(result).to.deep.equal(input);
219
+ });
198
220
 
199
221
  it('should return an empty string for an empty object', () => {
200
222
  const input = {};