arrmatura 4.2.2 → 5.0.2

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.
Files changed (93) hide show
  1. package/README.md +30 -25
  2. package/commons/auth/UserService.ts +107 -0
  3. package/commons/auth/index.ts +4 -0
  4. package/commons/auth/user.xml +66 -0
  5. package/commons/components/buttons.xml +23 -0
  6. package/commons/components/index.ts +13 -0
  7. package/commons/components/inputs.xml +33 -0
  8. package/commons/components/layouts.xml +77 -0
  9. package/commons/components/media.xml +21 -0
  10. package/commons/components/modal.xml +37 -0
  11. package/commons/components/navigation.xml +59 -0
  12. package/commons/components/popups.xml +12 -0
  13. package/commons/components/searchbar.xml +40 -0
  14. package/commons/components/table.xml +120 -0
  15. package/commons/components/toasts.xml +13 -0
  16. package/commons/components/viewport.xml +48 -0
  17. package/commons/forms/FieldItem.xml +20 -0
  18. package/commons/forms/FormController.ts +73 -0
  19. package/commons/forms/Item.ts +68 -0
  20. package/commons/forms/ItemCollection.ts +152 -0
  21. package/commons/forms/ItemController.ts +62 -0
  22. package/commons/forms/MetadataService.ts +45 -0
  23. package/commons/forms/MultiEnumField.xml +44 -0
  24. package/commons/forms/MultivalueController.ts +49 -0
  25. package/commons/forms/SmartareaField.xml +51 -0
  26. package/commons/forms/SuggestionController.ts +80 -0
  27. package/commons/forms/SuggestionField.xml +28 -0
  28. package/commons/forms/fields.xml +138 -0
  29. package/commons/forms/form.xml +37 -0
  30. package/commons/forms/index.ts +29 -0
  31. package/commons/index.ts +6 -0
  32. package/commons/pipes/index.ts +21 -0
  33. package/commons/pipes/showCounts.ts +16 -0
  34. package/commons/services/ErrorHandlingService.ts +11 -0
  35. package/commons/services/FirebaseService.ts +185 -0
  36. package/commons/services/GSheetsService.ts +92 -0
  37. package/commons/services/HeartbeatService.ts +35 -0
  38. package/commons/services/Invoke.ts +13 -0
  39. package/commons/services/JsonpService.ts +18 -0
  40. package/commons/services/Loader.ts +17 -0
  41. package/commons/services/LocalStorage.ts +46 -0
  42. package/commons/services/NavigationService.ts +55 -0
  43. package/commons/services/Service.ts +68 -0
  44. package/commons/services/ServiceWorker.ts +111 -0
  45. package/commons/services/ToastService.ts +49 -0
  46. package/commons/services/index.ts +14 -0
  47. package/core/Arrmatura.ts +79 -0
  48. package/core/CNode.ts +112 -0
  49. package/core/Component.ts +431 -0
  50. package/core/Platform.ts +3 -0
  51. package/core/cnodes/composition.ts +100 -0
  52. package/core/cnodes/conditionals.ts +51 -0
  53. package/core/cnodes/elementary.ts +45 -0
  54. package/core/cnodes/iterations.ts +94 -0
  55. package/core/cnodes/routing.ts +25 -0
  56. package/core/compileNode.ts +39 -0
  57. package/core/expression.ts +73 -0
  58. package/core/index.ts +2 -0
  59. package/core/register.ts +43 -0
  60. package/core/utils.ts +25 -0
  61. package/core-web/WebPlatform.ts +25 -0
  62. package/core-web/attributes.ts +197 -0
  63. package/core-web/elements.ts +64 -0
  64. package/core-web/index.ts +19 -0
  65. package/core-web/reflow.ts +30 -0
  66. package/core-web/type.ts +19 -0
  67. package/docs/index.js +191 -4077
  68. package/lib/arrayGroupBy.ts +19 -0
  69. package/lib/arraySortBy.ts +49 -0
  70. package/lib/arrayToObject.ts +18 -0
  71. package/lib/codus.ts +25 -0
  72. package/lib/consts.ts +3 -0
  73. package/lib/createListeners.ts +12 -0
  74. package/lib/curry.ts +8 -0
  75. package/lib/dateFormat.ts +69 -0
  76. package/lib/dateOf.ts +31 -0
  77. package/lib/dateParseISO8601.ts +27 -0
  78. package/lib/fx.ts +62 -0
  79. package/lib/guid.ts +7 -0
  80. package/lib/index.ts +19 -0
  81. package/lib/l10n.ts +40 -0
  82. package/lib/nextId.ts +9 -0
  83. package/lib/noop.ts +8 -0
  84. package/lib/obj.ts +62 -0
  85. package/lib/predicat.ts +26 -0
  86. package/lib/resources.ru.ts +20 -0
  87. package/lib/resources.ts +20 -0
  88. package/lib/scalar.ts +25 -0
  89. package/lib/str.ts +76 -0
  90. package/lib/urls.ts +91 -0
  91. package/lib/xml.ts +117 -0
  92. package/package.json +17 -8
  93. package/docs/index.js.map +0 -1
@@ -0,0 +1,92 @@
1
+ import { arrayToObject } from 'arrayToObject';
2
+ import { urlAppendParams } from 'urls';
3
+ import { JsonpService } from './JsonpService';
4
+ import { LocalStorage } from './LocalStorage';
5
+ import { Service } from './Service';
6
+
7
+ export class GSheetsService extends Service {
8
+ url: string = '';
9
+ kind: string = 'sheet';
10
+ remote = new JsonpService();
11
+ persistence: 'local' | 'transient' = 'transient';
12
+ local?: LocalStorage;
13
+
14
+ init() {
15
+ this.local = new LocalStorage(this.persistence);
16
+ return this.syncAll();
17
+ }
18
+
19
+ syncAll() {
20
+ const now = Date.now();
21
+ const { since, items: items0 = {} } = this.local?.get(this.kind) || { since: 0, items: {} };
22
+ return !this.url
23
+ ? null
24
+ : {
25
+ isLoading: true,
26
+ '*': fetch(urlAppendParams(this.url, { since }), { redirect: 'follow' })
27
+ .then((res) => res.json())
28
+ .then(({ items: items1, error }) => {
29
+ if (error) throw error;
30
+
31
+ this.log('sync' + this.kind, now, items1);
32
+ const items = { ...items0, ...arrayToObject(items1) };
33
+ this.local?.set(this.kind, { since: now, items });
34
+ return { items, isLoading: false };
35
+ })
36
+ .catch((err) => {
37
+ this.toast({ mode: 'error', message: 'Ошибка: ' + JSON.stringify(err?.message || err || 'unknown') });
38
+ return { isLoading: false };
39
+ }),
40
+ };
41
+ }
42
+
43
+ getAll() {
44
+ const { items } = this.local?.get(this.kind) || {};
45
+ return items;
46
+ }
47
+
48
+ getItems() {
49
+ let items = this.getAll();
50
+ return Object.values(items || {});
51
+ }
52
+
53
+ async update(delta: Delta) {
54
+ const now = Date.now();
55
+ const { since, items: items0 = [] } = this.local?.get(this.kind) || { since: 0, items: {} };
56
+
57
+ this.log('update', delta);
58
+
59
+ return {
60
+ busy: true,
61
+ '*': fetch(this.url, {
62
+ method: 'POST',
63
+ mode: 'cors',
64
+ redirect: 'follow',
65
+ body: JSON.stringify({ action: 'upsert', delta }),
66
+ })
67
+ .then((res) => res.json())
68
+ .then(({ error, ...item }) => {
69
+ if (error) throw error;
70
+ // items: items1 }
71
+ this.log('update sync' + this.kind, now, item.id);
72
+ const items = { ...items0, [item.id]: item };
73
+ this.toast('Сохранено успешно');
74
+
75
+ this.local?.set(this.kind, { since, items });
76
+ return { busy: false }; //items,
77
+ })
78
+ .catch((err) => {
79
+ this.toast({ mode: 'error', message: 'Ошибка: update ' + JSON.stringify(err?.message || err || 'unknown') });
80
+ return { busy: false };
81
+ }),
82
+ };
83
+ }
84
+
85
+ async onUpsert(data) {
86
+ return this.update(data);
87
+ }
88
+
89
+ async onDelete({ id }) {
90
+ return this.update({ [id]: { status: 'deleted' } });
91
+ }
92
+ }
@@ -0,0 +1,35 @@
1
+ export class HeartbeatService {
2
+ interval: any;
3
+ watcher?: Procedure;
4
+
5
+ constructor(watcher?: Procedure) {
6
+ this.setWatcher(watcher);
7
+ }
8
+
9
+ setWatcher(watcher?: Procedure) {
10
+ this.watcher = watcher;
11
+ }
12
+
13
+ startWatch() {
14
+ if (!this.interval) {
15
+ this.interval = setInterval(() => {
16
+ this.watcher?.();
17
+ }, 1000);
18
+ }
19
+ }
20
+
21
+ stopWatch() {
22
+ if (this.interval) {
23
+ clearInterval(this.interval);
24
+ this.interval = 0;
25
+ }
26
+ }
27
+
28
+ setActive(active: boolean) {
29
+ if (!active) {
30
+ this.stopWatch();
31
+ } else {
32
+ this.startWatch();
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ export class Invoke {
2
+ init() {
3
+ if (this.delay) {
4
+ setTimeout(() => {
5
+ if (!this.isDone) {
6
+ this.action(this.data);
7
+ }
8
+ }, this.delay * 1000);
9
+ } else {
10
+ this.action(this.data);
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,18 @@
1
+ import { urlAppendParams } from 'urls';
2
+
3
+ let count = 0;
4
+
5
+ export class JsonpService {
6
+ fetch(url: string) {
7
+ return new Promise((resolve, reject) => {
8
+ const jsonp: string = 'jsonp_' + ++count;
9
+ const a = window.document.createElement('script');
10
+ a.async = true;
11
+ a.onerror = reject;
12
+ a.src = urlAppendParams(url, { jsonp });
13
+ window[jsonp] = resolve;
14
+ const m = window.document.getElementsByTagName('body')[0];
15
+ m?.parentNode?.insertBefore(a, m);
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,17 @@
1
+ export class Loader {
2
+ init() {
3
+ this.reload();
4
+ }
5
+
6
+ setTrigger(val) {
7
+ this.trigger = val;
8
+ this.reload();
9
+ }
10
+
11
+ reload() {
12
+ const { from, data, into } = this;
13
+ if (from && into) {
14
+ from({ data, callback: (error, result) => into({ error, ...result }) });
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,46 @@
1
+ const parse = (s: string | null) => (s ? JSON.parse(s) : undefined);
2
+
3
+ class MemoryStore {
4
+ map = new Map<string, string>();
5
+
6
+ getItem(key: string) {
7
+ return this.map.get(key) || null;
8
+ }
9
+
10
+ setItem(key: string, value: string): void {
11
+ this.map.set(key, value);
12
+ }
13
+ removeItem(key: string): void {
14
+ this.map.delete(key);
15
+ }
16
+ }
17
+
18
+ export class LocalStorage {
19
+ cache: Hash = {};
20
+ storage: MemoryStore | Storage;
21
+
22
+ constructor(mode?: 'local' | 'transient') {
23
+ this.storage = mode === 'local' ? window.localStorage : new MemoryStore();
24
+ }
25
+
26
+ get(key: string) {
27
+ return this.cache[key] || (this.cache[key] = parse(this.storage.getItem(key)));
28
+ }
29
+
30
+ set(key: string, val: unknown) {
31
+ this.cache[key] = val;
32
+ if (val) {
33
+ this.storage.setItem(key, JSON.stringify(val));
34
+ } else {
35
+ this.storage.removeItem(key);
36
+ }
37
+ }
38
+
39
+ assign(delta: Delta) {
40
+ Object.entries(delta).forEach(([key, val = null]) => this.set(key, val));
41
+ }
42
+
43
+ transform(key: string, fn: Pipe) {
44
+ return this.set(key, fn(this.get(key)));
45
+ }
46
+ }
@@ -0,0 +1,55 @@
1
+ import { urlParse, urlStringify } from 'urls';
2
+ import { Service } from './Service';
3
+
4
+ export class NavigationService extends Service {
5
+ prevHash: string = '';
6
+ host: any;
7
+ path: any;
8
+ params: any;
9
+
10
+ init() {
11
+ this.prevHash;
12
+ window.addEventListener('hashchange', () => this.hashchange());
13
+ setTimeout(() => this.hashchange(), 0);
14
+ }
15
+
16
+ hashchange() {
17
+ const hash = window.location.hash.slice(1);
18
+ if (hash[0] === '/' && hash !== this.prevHash) {
19
+ this.emit('this.hash', { value: hash.slice(1) });
20
+ this.prevHash = hash;
21
+ } else if (!this.prevHash) {
22
+ this.emit('this.hash', { value: 'main' });
23
+ }
24
+ }
25
+
26
+ done() {
27
+ window.removeEventListener('hashchange', this.hashchange);
28
+ }
29
+
30
+ setHash(d) {
31
+ const { host, path = ['*'], params } = urlParse('/' + d);
32
+ const state = {
33
+ host: (!host || host === '*' ? this.host : host) || 'main',
34
+ path: path[0] === '*' ? this.path : path,
35
+ params: params.reset ? { ...params, reset: null } : { ...this.params, ...params },
36
+ };
37
+ window.location.hash = this.prevHash = urlStringify(state).slice(1);
38
+ this.up(state);
39
+ }
40
+
41
+ getRoute() {
42
+ return {
43
+ host: this.host,
44
+ path: this.path,
45
+ params: this.params,
46
+ };
47
+ }
48
+
49
+ onHash({ value }) {
50
+ this.setHash('/' + value);
51
+ }
52
+ onParams(params) {
53
+ this.setHash({ params: { ...params, data: null } });
54
+ }
55
+ }
@@ -0,0 +1,68 @@
1
+ import { Component } from '../../core/Component';
2
+ import { Toast } from './ToastService';
3
+
4
+ export class Service {
5
+ $: Component;
6
+ constructor(state, $: Component) {
7
+ this.state = state;
8
+ this.$ = $;
9
+ }
10
+
11
+ /**
12
+ * Framework
13
+ */
14
+ lookupService(ref: string) {
15
+ return this.$.getByRef(ref);
16
+ }
17
+
18
+ up(...args) {
19
+ return this.$.up(...args);
20
+ }
21
+
22
+ defer(...args) {
23
+ return this.$.defer(...args);
24
+ }
25
+
26
+ emit(...args) {
27
+ return this.$.emit(...args);
28
+ }
29
+
30
+ res(...args) {
31
+ return this.$.res(...args);
32
+ }
33
+
34
+ log(...args) {
35
+ console.log(this.$.ref + ': ', ...args);
36
+ return args[0];
37
+ }
38
+
39
+ /**
40
+ * Error handling
41
+ */
42
+
43
+ handleError({ message = '', code = '' }) {
44
+ // may overriden from props
45
+ const handler = this.lookupService('errorHandler');
46
+ if (handler) {
47
+ handler.handleError({ message, code, source: this.$ });
48
+ } else {
49
+ console.error(this.$.ref + ': ERROR: ', message, code);
50
+ }
51
+ }
52
+
53
+ toast(message: string | Partial<Toast> = 'ok') {
54
+ // may overriden from props
55
+ this.emit('toasters.send', typeof message === 'string' ? { message } : message);
56
+ }
57
+
58
+ safe(p, def) {
59
+ return p.catch((error) => {
60
+ this.handleError(error);
61
+ return def ? def(error) : { error };
62
+ });
63
+ }
64
+
65
+ onClearError() {
66
+ return { error: null };
67
+ }
68
+ }
@@ -0,0 +1,111 @@
1
+ import { Service } from './Service';
2
+
3
+ export class ServiceWorker extends Service {
4
+ get api() {
5
+ return navigator.serviceWorker;
6
+ }
7
+
8
+ init() {
9
+ try {
10
+ Function.assert(this.api, 'Service Workers are not supported');
11
+
12
+ const { source = '/service-worker.js', scope = '/', push } = this;
13
+
14
+ this.api
15
+ .register(source, { scope })
16
+ .then((registration) => this.registered(registration))
17
+ .then(() => this.ready(() => this.log('Service Worker Ready')));
18
+
19
+ this.api.addEventListener('message', (ev) => this.onMessage(ev));
20
+
21
+ if (push) {
22
+ this.subscribe();
23
+ }
24
+ } catch (error) {
25
+ this.fallback(error);
26
+ }
27
+ }
28
+
29
+ // ensures that `fn` executed when api is ready
30
+ ready(fn) {
31
+ return this.api.ready.then(fn);
32
+ }
33
+
34
+ fallback(error) {
35
+ this.log(error);
36
+ }
37
+
38
+ // hook on registered
39
+ registered(registration) {
40
+ this.log('Service Worker Registered');
41
+ return registration;
42
+ }
43
+ /**
44
+ * Push
45
+ */
46
+
47
+ withPushManager(fn) {
48
+ return this.ready(({ pushManager }) => pushManager).then(fn);
49
+ }
50
+
51
+ unsubscribe() {
52
+ this.withPushManager((pushManager) => pushManager.getSubscription())
53
+ .then((ss) => ss && ss.unsubscribe())
54
+ .then(() => this.saveSubscription())
55
+ .catch(function (e) {
56
+ console.log('Error thrown while unsubscribing from push messaging.', e);
57
+ });
58
+ }
59
+
60
+ subscribe() {
61
+ function urlBase64ToUint8Array(base64String) {
62
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
63
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
64
+
65
+ const rawData = window.atob(base64);
66
+ return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
67
+ }
68
+ if (Notification.permission === 'granted') {
69
+ /* do our magic */
70
+ } else if (Notification.permission === 'blocked') {
71
+ /* the user has previously denied push. Can't reprompt. */
72
+ } else {
73
+ /* show a prompt to the user */
74
+ }
75
+ const applicationServerKey = urlBase64ToUint8Array(this.vapidPublicKey);
76
+ this.withPushManager((pushManager) =>
77
+ pushManager
78
+ .getSubscription()
79
+ // .then((ss) => ss && ss.unsubscribe())
80
+ .then(
81
+ (ss) =>
82
+ ss ||
83
+ pushManager.subscribe({
84
+ userVisibleOnly: true,
85
+ applicationServerKey,
86
+ })
87
+ )
88
+ .then((subscription) => this.saveSubscription(subscription.toJSON()))
89
+ .catch((err) => {
90
+ if (Notification.permission === 'denied') {
91
+ this.log('The user has blocked notifications.');
92
+ }
93
+ this.handleError(err);
94
+ })
95
+ );
96
+ }
97
+
98
+ // to be overriden from props
99
+ saveSubscription(ss) {
100
+ this.subscription = ss;
101
+ }
102
+
103
+ /**
104
+ * Intercommunication between service worker.
105
+ */
106
+
107
+ // handles a message posted from Service worker.
108
+ onMessage(payload) {
109
+ this.log('onMessage', payload);
110
+ }
111
+ }
@@ -0,0 +1,49 @@
1
+ import { HeartbeatService } from './HeartbeatService';
2
+ import { Service } from './Service';
3
+
4
+ export type Toast = {
5
+ message: string;
6
+ mode: 'error' | 'warning' | 'info' | '';
7
+ id: string;
8
+ ttl: number;
9
+ };
10
+
11
+ export class ToastService extends Service {
12
+ items: Toast[] = [];
13
+
14
+ heartbeat = new HeartbeatService(() => this.emit('this.tick'));
15
+
16
+ setItems(items: Toast[]) {
17
+ this.heartbeat.setActive(!!items.length);
18
+ this.items = items;
19
+ }
20
+
21
+ onSend(data: Toast, { items }: this) {
22
+ const now = Date.now();
23
+ const id = String(data.id || now);
24
+ const close = () => this.emit('this.close', { id });
25
+ const toast = {
26
+ ...data,
27
+ mode: data?.message?.startsWith('error') ? 'error' : data.mode,
28
+ id,
29
+ close,
30
+ ttl: now + 3000,
31
+ };
32
+
33
+ return {
34
+ items: [toast, ...items.filter((e) => e.id !== id)],
35
+ };
36
+ }
37
+
38
+ onTick(_: unknown, { items }: this) {
39
+ const now = Date.now();
40
+
41
+ return { items: items.filter((e: Toast) => e.ttl > now) };
42
+ }
43
+
44
+ onClose({ id }: Datum, { items }: this) {
45
+ return {
46
+ items: items.filter((e) => e.id !== id),
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,14 @@
1
+ export { NavigationService } from './NavigationService';
2
+ export { ServiceWorker } from './ServiceWorker';
3
+ export { Service } from './Service';
4
+ export { ToastService } from './ToastService';
5
+ export { Loader } from './Loader';
6
+ export { Invoke } from './Invoke';
7
+ export { ErrorHandlingService } from './ErrorHandlingService';
8
+ export { FormController } from '../forms/FormController';
9
+ export * from './GSheetsService';
10
+ export * from '../forms/ItemCollection';
11
+ export * from '../forms/ItemController';
12
+ export * from './FirebaseService';
13
+ export * from '../forms/MetadataService';
14
+ export * from '../forms/MultivalueController';
@@ -0,0 +1,79 @@
1
+ import { CNode } from './CNode';
2
+ import { compileNodes } from './compileNode';
3
+ import { Component } from './Component';
4
+ import { xmlParse } from '../lib/xml';
5
+ import { Platform } from './Platform';
6
+ import { registerTypes } from './register';
7
+
8
+ export class Arrmatura extends Component<CArrmaturaNode> implements IArrmatura {
9
+ rootElement: any;
10
+ reflowId?: unknown;
11
+
12
+ get displayName(): string {
13
+ return 'Arrmatura';
14
+ }
15
+ get arrmatura(): IArrmatura {
16
+ return this;
17
+ }
18
+
19
+ requestReflow() {
20
+ if (this.reflowId) return;
21
+ this.reflowId = setTimeout(() => {
22
+ Platform.instance.reflow(this);
23
+ this.reflowId = undefined;
24
+ }, 10);
25
+ }
26
+
27
+ res(key: string): any {
28
+ const [id, ...deep] = key.split('.');
29
+ const { resources = {} } = this.node;
30
+ const target = id ? resources?.[id] : resources;
31
+ if (!target || deep.length === 0) {
32
+ return target;
33
+ }
34
+ if (deep.length === 1) {
35
+ return target[deep[0]];
36
+ }
37
+ return deep.reduce((r, k) => (r ? r[k] : null), target);
38
+ }
39
+
40
+ pipe(key: string) {
41
+ const [id, ...deep] = key.split('.');
42
+ const target = this.node.functions?.[id];
43
+ if (!target || deep.length === 0) {
44
+ return target;
45
+ }
46
+ if (deep.length === 1) {
47
+ return (target as Hash)[deep[0]];
48
+ }
49
+ return deep.reduce((r, k) => (r ? (r as Hash)[k] : null), target);
50
+ }
51
+ }
52
+
53
+ export class CArrmaturaNode extends CNode<Arrmatura> {
54
+ functions?: Hash;
55
+ resources: Hash<any> | undefined;
56
+
57
+ constructor(template: string, { types, functions, resources }: ArrmaturaOptions) {
58
+ super();
59
+
60
+ registerTypes(types);
61
+
62
+ this.functions = functions;
63
+ this.resources = resources;
64
+
65
+ this.content = compileNodes(xmlParse(template));
66
+ }
67
+
68
+ get ComponentConstructor() {
69
+ return Arrmatura;
70
+ }
71
+
72
+ launch() {
73
+ const arrmatura = this.createComponent() as Arrmatura;
74
+
75
+ arrmatura.up({}, true);
76
+
77
+ console.log(arrmatura.toString());
78
+ }
79
+ }