@htmlplus/element 3.2.6 → 3.3.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/client.d.ts CHANGED
@@ -141,6 +141,19 @@ declare function Listen(type: string, options?: ListenOptions): (target: HTMLPlu
141
141
  */
142
142
  declare function Method(): (target: HTMLPlusElement, key: PropertyKey, descriptor: PropertyDescriptor) => void;
143
143
 
144
+ type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
145
+ type Overwrite<T, U> = DistributiveOmit<T, keyof U> & U;
146
+ type GenerateStringUnion<T> = Extract<{
147
+ [Key in keyof T]: true extends T[Key] ? Key : never;
148
+ }[keyof T], string>;
149
+ type OverridesConfigBreakpointCreator<T extends string, U = {}> = GenerateStringUnion<Overwrite<Record<T, true>, U>>;
150
+ type OverridesConfig<Breakpoint extends string, Properties = {}> = {
151
+ [Key in Breakpoint]?: Partial<Properties>;
152
+ };
153
+ interface ElementPropertiesAutoGenerated {
154
+ }
155
+ declare function Overrides(): (target: HTMLPlusElement, key: string) => void;
156
+
144
157
  /**
145
158
  * The configuration for property decorator.
146
159
  */
@@ -222,16 +235,25 @@ declare const classes: (input: any, smart?: boolean) => string;
222
235
  * TODO
223
236
  */
224
237
  interface Config {
238
+ breakpoints?: {
239
+ [key: string]: {
240
+ type: 'container' | 'media';
241
+ min?: number;
242
+ max?: number;
243
+ };
244
+ };
225
245
  event?: {
226
246
  resolver?: (parameters: any) => CustomEvent | undefined;
227
247
  };
228
- asset?: {
248
+ assets?: {
229
249
  [key: string]: any;
230
250
  };
231
- element?: {
251
+ elements?: {
232
252
  [key: string]: {
233
- property?: {
234
- [key: string]: any;
253
+ properties?: {
254
+ [key: string]: {
255
+ default?: unknown;
256
+ };
235
257
  };
236
258
  };
237
259
  };
@@ -252,11 +274,19 @@ interface ConfigOptions {
252
274
  /**
253
275
  * TODO
254
276
  */
255
- declare const getConfig: (...keys: string[]) => any;
277
+ declare const getConfig: (namespace: string) => Config;
278
+ /**
279
+ * TODO
280
+ */
281
+ declare const getConfigCreator: (namespace: string) => () => Config;
282
+ /**
283
+ * TODO
284
+ */
285
+ declare const setConfig: (namespace: string, config: Config, options?: ConfigOptions) => void;
256
286
  /**
257
287
  * TODO
258
288
  */
259
- declare const setConfig: (config: Config, options?: ConfigOptions) => void;
289
+ declare const setConfigCreator: (namespace: string) => (config: Config, options?: ConfigOptions) => void;
260
290
 
261
291
  /**
262
292
  * Indicates whether the [Direction](https://mdn.io/css-direction)
@@ -324,5 +354,5 @@ declare const attributes: any;
324
354
  declare const html: any;
325
355
  declare const styles: any;
326
356
 
327
- export { Bind, Consumer, Debounce, Direction, Element$1 as Element, Event, Host, IsRTL, Listen, Method, Property, Provider, Query, QueryAll, Slots$1 as Slots, State, Style, Watch, attributes as a, classes, direction, dispatch, getConfig, html as h, host, isCSSColor, isRTL, off, on, query, queryAll, styles as s, setConfig, slots, toCSSColor, toCSSUnit, toUnit };
328
- export type { Config, ConfigOptions, EventEmitter, EventOptions, ListenOptions, PropertyOptions };
357
+ export { Bind, Consumer, Debounce, Direction, Element$1 as Element, Event, Host, IsRTL, Listen, Method, Overrides, Property, Provider, Query, QueryAll, Slots$1 as Slots, State, Style, Watch, attributes as a, classes, direction, dispatch, getConfig, getConfigCreator, html as h, host, isCSSColor, isRTL, off, on, query, queryAll, styles as s, setConfig, setConfigCreator, slots, toCSSColor, toCSSUnit, toUnit };
358
+ export type { Config, ConfigOptions, ElementPropertiesAutoGenerated, EventEmitter, EventOptions, ListenOptions, OverridesConfig, OverridesConfigBreakpointCreator, PropertyOptions };
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { kebabCase, pascalCase } from 'change-case';
2
- import { API_HOST, STATIC_TAG, API_STACKS, API_REQUEST, API_CONNECTED, LIFECYCLE_UPDATE, STATIC_STYLE, API_STYLE, LIFECYCLE_UPDATED, API_RENDER_COMPLETED, METHOD_RENDER, TYPE_BOOLEAN, TYPE_NUMBER, TYPE_NULL, TYPE_DATE, TYPE_ARRAY, TYPE_OBJECT, TYPE_UNDEFINED, KEY, LIFECYCLE_CONNECTED, LIFECYCLE_DISCONNECTED, LIFECYCLE_CONSTRUCTED, LIFECYCLE_ADOPTED, LIFECYCLE_READY } from './constants.js';
2
+ import { API_HOST, STATIC_TAG, API_STACKS, API_REQUEST, API_CONNECTED, LIFECYCLE_UPDATE, STATIC_STYLE, API_STYLE, LIFECYCLE_UPDATED, API_RENDER_COMPLETED, METHOD_RENDER, TYPE_BOOLEAN, TYPE_NUMBER, TYPE_NULL, TYPE_DATE, TYPE_ARRAY, TYPE_OBJECT, TYPE_UNDEFINED, TYPE_STRING, KEY, LIFECYCLE_CONNECTED, LIFECYCLE_DISCONNECTED, LIFECYCLE_CONSTRUCTED, LIFECYCLE_ADOPTED, LIFECYCLE_READY } from './constants.js';
3
3
 
4
4
  /**
5
5
  * Indicates the host of the element.
@@ -163,13 +163,6 @@ const classes = (input, smart) => {
163
163
  return result.filter((item) => item).join(' ');
164
164
  };
165
165
 
166
- /**
167
- * Indicates whether the current code is running on a server.
168
- */
169
- const isServer = () => {
170
- return !(typeof window != 'undefined' && window.document);
171
- };
172
-
173
166
  const merge = (target, ...sources) => {
174
167
  for (const source of sources) {
175
168
  if (!source)
@@ -192,31 +185,31 @@ const merge = (target, ...sources) => {
192
185
  return target;
193
186
  };
194
187
 
195
- const DEFAULTS = {
196
- element: {}
188
+ /**
189
+ * TODO
190
+ */
191
+ const getConfig = (namespace) => {
192
+ return globalThis[`$htmlplus:${namespace}$`] || {};
197
193
  };
198
194
  /**
199
195
  * TODO
200
196
  */
201
- const getConfig = (...keys) => {
202
- if (isServer())
203
- return;
204
- let config = window[`$htmlplus$`];
205
- for (const key of keys) {
206
- if (!config)
207
- break;
208
- config = config[key];
209
- }
210
- return config;
197
+ const getConfigCreator = (namespace) => () => {
198
+ return getConfig(namespace);
211
199
  };
212
200
  /**
213
201
  * TODO
214
202
  */
215
- const setConfig = (config, options) => {
216
- if (isServer())
217
- return;
218
- const previous = options?.override ? {} : window[`$htmlplus$`];
219
- window[`$htmlplus$`] = merge({}, DEFAULTS, previous, config);
203
+ const setConfig = (namespace, config, options) => {
204
+ const previous = options?.override ? {} : globalThis[`$htmlplus:${namespace}$`];
205
+ const next = merge({}, previous, config);
206
+ globalThis[`$htmlplus:${namespace}$`] = next;
207
+ };
208
+ /**
209
+ * TODO
210
+ */
211
+ const setConfigCreator = (namespace) => (config, options) => {
212
+ return setConfig(namespace, config, options);
220
213
  };
221
214
 
222
215
  const defineProperty = Object.defineProperty;
@@ -253,6 +246,10 @@ const getTag = (target) => {
253
246
  return target.constructor[STATIC_TAG] ?? target[STATIC_TAG];
254
247
  };
255
248
 
249
+ const getNamespace = (target) => {
250
+ return getTag(target)?.split('-')?.at(0);
251
+ };
252
+
256
253
  /**
257
254
  * Determines whether the given input string is a valid
258
255
  * [CSS Color](https://mdn.io/color-value) or not.
@@ -268,6 +265,13 @@ const isCSSColor = (input) => {
268
265
  */
269
266
  const isRTL = (target) => direction(target) == 'rtl';
270
267
 
268
+ /**
269
+ * Indicates whether the current code is running on a server.
270
+ */
271
+ const isServer = () => {
272
+ return !(typeof window != 'undefined' && window.document);
273
+ };
274
+
271
275
  const shadowRoot = (target) => {
272
276
  return host(target)?.shadowRoot;
273
277
  };
@@ -1266,7 +1270,20 @@ const toProperty = (input, type) => {
1266
1270
  return undefined;
1267
1271
  }
1268
1272
  }
1269
- return input;
1273
+ if (TYPE_STRING & type || type === String) {
1274
+ return input;
1275
+ }
1276
+ // TODO
1277
+ // if (CONSTANTS.TYPE_BIGINT & type || type === BigInt) { }
1278
+ // if (CONSTANTS.TYPE_ENUM & type || type === TODO) { }
1279
+ // if (CONSTANTS.TYPE_FUNCTION & type || type === Function) { }
1280
+ try {
1281
+ // TODO
1282
+ return JSON.parse(input);
1283
+ }
1284
+ catch {
1285
+ return input;
1286
+ }
1270
1287
  };
1271
1288
 
1272
1289
  /**
@@ -1526,7 +1543,31 @@ const proxy = (constructor) => {
1526
1543
  }
1527
1544
  connectedCallback() {
1528
1545
  // TODO: experimental for global config
1529
- Object.assign(this.#instance, getConfig('element', getTag(this.#instance), 'property'));
1546
+ (() => {
1547
+ const namespace = getNamespace(this.#instance);
1548
+ const tag = getTag(this.#instance);
1549
+ const properties = getConfig(namespace).elements?.[tag]?.properties;
1550
+ if (!properties)
1551
+ return;
1552
+ const defaults = Object.fromEntries(Object.entries(properties).map(([key, value]) => [key, value?.default]));
1553
+ Object.assign(this, defaults);
1554
+ })();
1555
+ // TODO
1556
+ (() => {
1557
+ const key = Object.keys(this).find((key) => key.startsWith('__reactProps'));
1558
+ const props = this[key];
1559
+ if (!props)
1560
+ return;
1561
+ for (const [key, value] of Object.entries(props)) {
1562
+ if (this[key] != undefined)
1563
+ continue;
1564
+ if (key == 'children')
1565
+ continue;
1566
+ if (typeof value != 'object')
1567
+ continue;
1568
+ this[key] = value;
1569
+ }
1570
+ })();
1530
1571
  this.#instance[API_CONNECTED] = true;
1531
1572
  call(this.#instance, LIFECYCLE_CONNECTED);
1532
1573
  requestUpdate(this.#instance, undefined, undefined, () => {
@@ -1579,7 +1620,8 @@ function Event(options = {}) {
1579
1620
  break;
1580
1621
  }
1581
1622
  let event;
1582
- event ||= getConfig('event', 'resolver')?.({ detail, element, framework, options, type });
1623
+ const resolver = getConfig(getNamespace(target)).event?.resolver;
1624
+ event ||= resolver?.({ detail, element, framework, options, type });
1583
1625
  event && element.dispatchEvent(event);
1584
1626
  event ||= dispatch(this, type, { ...options, detail });
1585
1627
  return event;
@@ -1648,6 +1690,165 @@ function Method() {
1648
1690
  };
1649
1691
  }
1650
1692
 
1693
+ const CONTAINER_DATA = Symbol();
1694
+ const getContainers = (breakpoints) => {
1695
+ return Object.entries(breakpoints || {}).reduce((result, [key, breakpoint]) => {
1696
+ if (breakpoint.type !== 'container')
1697
+ return result;
1698
+ result[key] = {
1699
+ min: breakpoint.min,
1700
+ max: breakpoint.max
1701
+ };
1702
+ return result;
1703
+ }, {});
1704
+ };
1705
+ const getMedias = (breakpoints) => {
1706
+ return Object.entries(breakpoints || {}).reduce((result, [key, breakpoint]) => {
1707
+ if (breakpoint.type !== 'media')
1708
+ return result;
1709
+ const parts = [];
1710
+ const min = 'min' in breakpoint ? breakpoint.min : undefined;
1711
+ const max = 'max' in breakpoint ? breakpoint.max : undefined;
1712
+ if (min !== undefined)
1713
+ parts.push(`(min-width: ${min}px)`);
1714
+ if (max !== undefined)
1715
+ parts.push(`(max-width: ${max}px)`);
1716
+ const query = parts.join(' and ');
1717
+ if (query)
1718
+ result[key] = query;
1719
+ return result;
1720
+ }, {});
1721
+ };
1722
+ const matchContainer = (element, container) => {
1723
+ const $element = element;
1724
+ const getData = () => {
1725
+ if ($element[CONTAINER_DATA])
1726
+ return $element[CONTAINER_DATA];
1727
+ const listeners = new Set();
1728
+ const observer = new ResizeObserver(() => {
1729
+ listeners.forEach((listener) => listener());
1730
+ });
1731
+ observer.observe(element);
1732
+ $element[CONTAINER_DATA] = { listeners, observer };
1733
+ return $element[CONTAINER_DATA];
1734
+ };
1735
+ const getMatches = () => {
1736
+ const width = element.offsetWidth;
1737
+ const matches = (container.min === undefined || width >= container.min) &&
1738
+ (container.max === undefined || width <= container.max);
1739
+ return matches;
1740
+ };
1741
+ const addEventListener = (type, listener) => {
1742
+ getData().listeners.add(listener);
1743
+ };
1744
+ const removeEventListener = (type, listener) => {
1745
+ const data = getData();
1746
+ data.listeners.delete(listener);
1747
+ if (data.listeners.size !== 0)
1748
+ return;
1749
+ data.observer.disconnect();
1750
+ delete $element[CONTAINER_DATA];
1751
+ };
1752
+ return {
1753
+ get matches() {
1754
+ return getMatches();
1755
+ },
1756
+ addEventListener,
1757
+ removeEventListener
1758
+ };
1759
+ };
1760
+ function Overrides() {
1761
+ return function (target, key) {
1762
+ const DISPOSERS = Symbol();
1763
+ const breakpoints = getConfig(getNamespace(target)).breakpoints || {};
1764
+ const containers = getContainers(breakpoints);
1765
+ const medias = getMedias(breakpoints);
1766
+ wrapMethod('after', target, LIFECYCLE_UPDATE, function (states) {
1767
+ if (!states.has(key))
1768
+ return;
1769
+ const disposers = (this[DISPOSERS] ??= new Map());
1770
+ const overrides = this[key] || {};
1771
+ const activeKeys = new Set(disposers.keys());
1772
+ const overrideKeys = Object.keys(overrides);
1773
+ const containerKeys = overrideKeys.filter((breakpoint) => breakpoint in containers);
1774
+ const mediaKeys = overrideKeys.filter((breakpoint) => breakpoint in medias);
1775
+ let timeout;
1776
+ let next = {};
1777
+ const apply = (key) => {
1778
+ clearTimeout(timeout);
1779
+ Object.assign(next, overrides[key]);
1780
+ timeout = setTimeout(() => {
1781
+ Object.assign(host(this), overrides[key]);
1782
+ next = {};
1783
+ }, 0);
1784
+ };
1785
+ for (const overrideKey of overrideKeys) {
1786
+ if (activeKeys.delete(overrideKey))
1787
+ continue;
1788
+ const breakpoint = breakpoints[overrideKey];
1789
+ if (!breakpoint)
1790
+ continue;
1791
+ switch (breakpoint.type) {
1792
+ case 'container': {
1793
+ const container = containers[overrideKey];
1794
+ if (!container)
1795
+ break;
1796
+ const containerQueryList = matchContainer(host(this), container);
1797
+ const change = () => {
1798
+ for (const containerKey of containerKeys) {
1799
+ if (matchContainer(host(this), containers[containerKey]).matches) {
1800
+ apply(containerKey);
1801
+ }
1802
+ }
1803
+ };
1804
+ containerQueryList.addEventListener('change', change);
1805
+ const disposer = () => {
1806
+ containerQueryList.removeEventListener('change', change);
1807
+ };
1808
+ disposers.set(overrideKey, disposer);
1809
+ if (!containerQueryList.matches)
1810
+ break;
1811
+ change();
1812
+ break;
1813
+ }
1814
+ case 'media': {
1815
+ const media = medias[overrideKey];
1816
+ if (!media)
1817
+ break;
1818
+ const mediaQueryList = window.matchMedia(media);
1819
+ const change = () => {
1820
+ for (const mediaKey of mediaKeys) {
1821
+ if (window.matchMedia(medias[mediaKey]).matches) {
1822
+ apply(mediaKey);
1823
+ }
1824
+ }
1825
+ };
1826
+ mediaQueryList.addEventListener('change', change);
1827
+ const disposer = () => {
1828
+ mediaQueryList.removeEventListener('change', change);
1829
+ };
1830
+ disposers.set(overrideKey, disposer);
1831
+ if (!mediaQueryList.matches)
1832
+ break;
1833
+ change();
1834
+ break;
1835
+ }
1836
+ }
1837
+ }
1838
+ for (const activeKey of activeKeys) {
1839
+ const disposer = disposers.get(activeKey);
1840
+ disposer();
1841
+ disposers.delete(activeKey);
1842
+ }
1843
+ });
1844
+ wrapMethod('after', target, LIFECYCLE_DISCONNECTED, function () {
1845
+ const disposers = (this[DISPOSERS] ??= new Map());
1846
+ disposers.forEach((disposer) => disposer());
1847
+ disposers.clear();
1848
+ });
1849
+ };
1850
+ }
1851
+
1651
1852
  /**
1652
1853
  * Creates a reactive property, reflecting a corresponding attribute value,
1653
1854
  * and updates the element when the property is set.
@@ -1916,4 +2117,4 @@ const attributes = attributes$2;
1916
2117
  const html = html$1;
1917
2118
  const styles = styles$1;
1918
2119
 
1919
- export { Bind, Consumer, Debounce, Direction, Element, Event, Host, IsRTL, Listen, Method, Property, Provider, Query, QueryAll, Slots, State, Style, Watch, attributes as a, classes, direction, dispatch, getConfig, html as h, host, isCSSColor, isRTL, off, on, query, queryAll, styles as s, setConfig, slots, toCSSColor, toCSSUnit, toUnit };
2120
+ export { Bind, Consumer, Debounce, Direction, Element, Event, Host, IsRTL, Listen, Method, Overrides, Property, Provider, Query, QueryAll, Slots, State, Style, Watch, attributes as a, classes, direction, dispatch, getConfig, getConfigCreator, html as h, host, isCSSColor, isRTL, off, on, query, queryAll, styles as s, setConfig, setConfigCreator, slots, toCSSColor, toCSSUnit, toUnit };
@@ -751,9 +751,7 @@ const customElement = (options) => {
751
751
 
752
752
  namespace JSX {
753
753
  interface IntrinsicElements {
754
- "${context.elementTagName}": ${context.className}Events & ${context.className}Attributes & {
755
- [key: string]: any;
756
- };
754
+ "${context.elementTagName}": ${context.className}Events & ${context.className}Attributes & React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
757
755
  }
758
756
  }
759
757
  }
@@ -761,9 +759,7 @@ const customElement = (options) => {
761
759
  declare module "react" {
762
760
  namespace JSX {
763
761
  interface IntrinsicElements {
764
- "${context.elementTagName}": ${context.className}Events & ${context.className}Attributes & {
765
- [key: string]: any;
766
- };
762
+ "${context.elementTagName}": ${context.className}Events & ${context.className}Attributes & React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
767
763
  }
768
764
  }
769
765
  }
@@ -774,6 +770,22 @@ const customElement = (options) => {
774
770
  preserveComments: true
775
771
  });
776
772
  path.node.body.push(...ast);
773
+ },
774
+ // TODO
775
+ TSTypeReference(path) {
776
+ if (path.node.typeName?.name != 'OverridesConfig')
777
+ return;
778
+ const property = path.findParent((path) => path.isTSPropertySignature());
779
+ if (!property)
780
+ return;
781
+ const name = property.node.key.name || property.node.key.extra.rawValue;
782
+ if (!path.node.typeParameters?.params)
783
+ return;
784
+ path.node.typeParameters.params[1] = t.tsTypeReference(t.identifier('Omit'), t.tsTypeParameterInstantiation([
785
+ t.tsTypeReference(t.identifier(`${context.className}Properties`)),
786
+ t.tsLiteralType(t.stringLiteral(name))
787
+ ]));
788
+ path.skip();
777
789
  }
778
790
  });
779
791
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htmlplus/element",
3
- "version": "3.2.6",
3
+ "version": "3.3.0",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "author": "Masood Abdolian <m.abdolian@gmail.com>",