@agnos-ui/core 0.0.1-alpha.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/README.md +15 -0
- package/dist/lib/accordion.d.ts +318 -0
- package/dist/lib/accordion.js +263 -0
- package/dist/lib/alert.d.ts +100 -0
- package/dist/lib/alert.js +66 -0
- package/dist/lib/config.d.ts +71 -0
- package/dist/lib/config.js +53 -0
- package/dist/lib/index.d.ts +11 -0
- package/dist/lib/index.js +11 -0
- package/dist/lib/modal/modal.d.ts +318 -0
- package/dist/lib/modal/modal.js +156 -0
- package/dist/lib/modal/scrollbars.d.ts +2 -0
- package/dist/lib/modal/scrollbars.js +27 -0
- package/dist/lib/pagination.d.ts +464 -0
- package/dist/lib/pagination.js +148 -0
- package/dist/lib/pagination.utils.d.ts +8 -0
- package/dist/lib/pagination.utils.js +110 -0
- package/dist/lib/rating.d.ts +209 -0
- package/dist/lib/rating.js +141 -0
- package/dist/lib/select.d.ts +199 -0
- package/dist/lib/select.js +240 -0
- package/dist/lib/services/checks.d.ts +32 -0
- package/dist/lib/services/checks.js +43 -0
- package/dist/lib/services/directiveUtils.d.ts +95 -0
- package/dist/lib/services/directiveUtils.js +190 -0
- package/dist/lib/services/focustrack.d.ts +19 -0
- package/dist/lib/services/focustrack.js +46 -0
- package/dist/lib/services/index.d.ts +6 -0
- package/dist/lib/services/index.js +6 -0
- package/dist/lib/services/portal.d.ts +6 -0
- package/dist/lib/services/portal.js +33 -0
- package/dist/lib/services/siblingsInert.d.ts +7 -0
- package/dist/lib/services/siblingsInert.js +40 -0
- package/dist/lib/services/stores.d.ts +140 -0
- package/dist/lib/services/stores.js +219 -0
- package/dist/lib/services/writables.d.ts +7 -0
- package/dist/lib/services/writables.js +16 -0
- package/dist/lib/transitions/baseTransitions.d.ts +136 -0
- package/dist/lib/transitions/baseTransitions.js +171 -0
- package/dist/lib/transitions/bootstrap/collapse.d.ts +2 -0
- package/dist/lib/transitions/bootstrap/collapse.js +15 -0
- package/dist/lib/transitions/bootstrap/fade.d.ts +1 -0
- package/dist/lib/transitions/bootstrap/fade.js +7 -0
- package/dist/lib/transitions/bootstrap/index.d.ts +2 -0
- package/dist/lib/transitions/bootstrap/index.js +2 -0
- package/dist/lib/transitions/collapse.d.ts +29 -0
- package/dist/lib/transitions/collapse.js +39 -0
- package/dist/lib/transitions/cssTransitions.d.ts +15 -0
- package/dist/lib/transitions/cssTransitions.js +38 -0
- package/dist/lib/transitions/index.d.ts +5 -0
- package/dist/lib/transitions/index.js +5 -0
- package/dist/lib/transitions/simpleClassTransition.d.ts +29 -0
- package/dist/lib/transitions/simpleClassTransition.js +28 -0
- package/dist/lib/transitions/utils.d.ts +20 -0
- package/dist/lib/transitions/utils.js +83 -0
- package/dist/lib/tsdoc-metadata.json +11 -0
- package/dist/lib/types.d.ts +58 -0
- package/dist/lib/types.js +7 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +2 -0
- package/package.json +52 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { asReadable, batch, computed, writable } from '@amadeus-it-group/tansu';
|
|
2
|
+
import { createHasFocus } from './services/focustrack';
|
|
3
|
+
import { stateStores, writablesForProps } from './services/stores';
|
|
4
|
+
function defaultMatchFn(item, text) {
|
|
5
|
+
return JSON.stringify(item).toLowerCase().includes(text.toLowerCase());
|
|
6
|
+
}
|
|
7
|
+
function defaultItemId(item) {
|
|
8
|
+
return '' + item;
|
|
9
|
+
}
|
|
10
|
+
const defaultConfig = {
|
|
11
|
+
opened: false,
|
|
12
|
+
disabled: false,
|
|
13
|
+
items: [],
|
|
14
|
+
filterText: '',
|
|
15
|
+
loading: false,
|
|
16
|
+
selected: [],
|
|
17
|
+
itemId: defaultItemId,
|
|
18
|
+
matchFn: defaultMatchFn,
|
|
19
|
+
onFilterTextChange: undefined,
|
|
20
|
+
className: '',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Create a SelectWidget with given config props
|
|
24
|
+
* @param config - an optional alert config
|
|
25
|
+
* @returns a SelectWidget
|
|
26
|
+
*/
|
|
27
|
+
export function createSelect(config) {
|
|
28
|
+
// Props
|
|
29
|
+
const [{ opened$: _dirtyOpened$, items$, itemId$, matchFn$, onFilterTextChange$, ...otherProps }, patch] = writablesForProps(defaultConfig, config);
|
|
30
|
+
const { selected$, filterText$ } = otherProps;
|
|
31
|
+
const { hasFocus$, directive: hasFocusDirective } = createHasFocus();
|
|
32
|
+
const opened$ = computed(() => {
|
|
33
|
+
const _dirtyOpened = _dirtyOpened$();
|
|
34
|
+
const hasFocus = hasFocus$();
|
|
35
|
+
if (!hasFocus && _dirtyOpened) {
|
|
36
|
+
_dirtyOpened$.set(false);
|
|
37
|
+
}
|
|
38
|
+
return _dirtyOpened && hasFocus;
|
|
39
|
+
});
|
|
40
|
+
const highlightedIndex$ = (function () {
|
|
41
|
+
const store = writable(0);
|
|
42
|
+
const newStore = asReadable(store, {
|
|
43
|
+
set(index) {
|
|
44
|
+
const { length } = visible$();
|
|
45
|
+
if (index != undefined) {
|
|
46
|
+
if (!length) {
|
|
47
|
+
index = undefined;
|
|
48
|
+
}
|
|
49
|
+
else if (index < 0) {
|
|
50
|
+
index = length - 1;
|
|
51
|
+
}
|
|
52
|
+
else if (index >= length) {
|
|
53
|
+
index = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
store.set(index);
|
|
57
|
+
},
|
|
58
|
+
update(fn) {
|
|
59
|
+
newStore.set(fn(store()));
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return newStore;
|
|
63
|
+
})();
|
|
64
|
+
const visible$ = computed(() => {
|
|
65
|
+
const list = [];
|
|
66
|
+
if (opened$()) {
|
|
67
|
+
const selected = selected$();
|
|
68
|
+
const filterText = filterText$();
|
|
69
|
+
const matchFn = !filterText ? () => true : matchFn$();
|
|
70
|
+
const itemId = itemId$();
|
|
71
|
+
for (const item of items$()) {
|
|
72
|
+
if (matchFn(item, filterText)) {
|
|
73
|
+
list.push({
|
|
74
|
+
item,
|
|
75
|
+
id: itemId(item),
|
|
76
|
+
selected: selected.includes(item),
|
|
77
|
+
select: function () {
|
|
78
|
+
widget.api.select(this);
|
|
79
|
+
}.bind(item),
|
|
80
|
+
unselect: function () {
|
|
81
|
+
widget.api.unselect(this);
|
|
82
|
+
}.bind(item),
|
|
83
|
+
toggle: function () {
|
|
84
|
+
widget.api.toggleItem(this);
|
|
85
|
+
}.bind(item),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return list;
|
|
91
|
+
});
|
|
92
|
+
const highlighted$ = computed(() => {
|
|
93
|
+
const visible = visible$();
|
|
94
|
+
const highlightedIndex = highlightedIndex$();
|
|
95
|
+
return visible.length && highlightedIndex != undefined ? visible[highlightedIndex] : undefined;
|
|
96
|
+
});
|
|
97
|
+
const widget = {
|
|
98
|
+
...stateStores({
|
|
99
|
+
visible$,
|
|
100
|
+
highlighted$,
|
|
101
|
+
opened$,
|
|
102
|
+
...otherProps,
|
|
103
|
+
}),
|
|
104
|
+
patch,
|
|
105
|
+
api: {
|
|
106
|
+
clear() {
|
|
107
|
+
selected$.set([]);
|
|
108
|
+
},
|
|
109
|
+
select(item) {
|
|
110
|
+
widget.api.toggleItem(item, true);
|
|
111
|
+
},
|
|
112
|
+
unselect(item) {
|
|
113
|
+
widget.api.toggleItem(item, false);
|
|
114
|
+
},
|
|
115
|
+
toggleItem(item, selected) {
|
|
116
|
+
if (!items$().includes(item)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
selected$.update((selectedItems) => {
|
|
120
|
+
selectedItems = [...selectedItems];
|
|
121
|
+
const index = selectedItems.indexOf(item);
|
|
122
|
+
if (selected == null) {
|
|
123
|
+
selected = index === -1;
|
|
124
|
+
}
|
|
125
|
+
if (selected && index === -1) {
|
|
126
|
+
selectedItems.push(item);
|
|
127
|
+
}
|
|
128
|
+
else if (!selected && index !== -1) {
|
|
129
|
+
selectedItems.splice(index, 1);
|
|
130
|
+
}
|
|
131
|
+
return selectedItems;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
clearText() {
|
|
135
|
+
// FIXME: not implemented yet!
|
|
136
|
+
},
|
|
137
|
+
highlight(item) {
|
|
138
|
+
const index = visible$().findIndex((itemCtx) => itemCtx.item === item);
|
|
139
|
+
highlightedIndex$.set(index === -1 ? undefined : index);
|
|
140
|
+
},
|
|
141
|
+
highlightFirst() {
|
|
142
|
+
highlightedIndex$.set(0);
|
|
143
|
+
},
|
|
144
|
+
highlightPrevious() {
|
|
145
|
+
highlightedIndex$.update((highlightedIndex) => {
|
|
146
|
+
return highlightedIndex != null ? highlightedIndex - 1 : -1;
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
highlightNext() {
|
|
150
|
+
highlightedIndex$.update((highlightedIndex) => {
|
|
151
|
+
return highlightedIndex != null ? highlightedIndex + 1 : Infinity;
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
highlightLast() {
|
|
155
|
+
highlightedIndex$.set(-1);
|
|
156
|
+
},
|
|
157
|
+
focus(item) {
|
|
158
|
+
// FIXME: not implemented yet!
|
|
159
|
+
},
|
|
160
|
+
focusFirst() {
|
|
161
|
+
// FIXME: not implemented yet!
|
|
162
|
+
},
|
|
163
|
+
focusPrevious() {
|
|
164
|
+
// FIXME: not implemented yet!
|
|
165
|
+
},
|
|
166
|
+
focusNext() {
|
|
167
|
+
// FIXME: not implemented yet!
|
|
168
|
+
},
|
|
169
|
+
focusLast() {
|
|
170
|
+
// FIXME: not implemented yet!
|
|
171
|
+
},
|
|
172
|
+
open: () => widget.api.toggle(true),
|
|
173
|
+
close: () => widget.api.toggle(false),
|
|
174
|
+
toggle(isOpen) {
|
|
175
|
+
_dirtyOpened$.update((value) => (isOpen != null ? isOpen : !value));
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
directives: {
|
|
179
|
+
hasFocusDirective,
|
|
180
|
+
},
|
|
181
|
+
actions: {
|
|
182
|
+
onInput({ target }) {
|
|
183
|
+
const value = target.value;
|
|
184
|
+
batch(() => {
|
|
185
|
+
patch({
|
|
186
|
+
opened: value != null && value !== '',
|
|
187
|
+
filterText: value,
|
|
188
|
+
});
|
|
189
|
+
onFilterTextChange$()?.(value);
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
onInputKeydown(e) {
|
|
193
|
+
const { ctrlKey, key } = e;
|
|
194
|
+
let keyManaged = true;
|
|
195
|
+
switch (key) {
|
|
196
|
+
case 'ArrowDown': {
|
|
197
|
+
const isOpen = opened$();
|
|
198
|
+
if (isOpen) {
|
|
199
|
+
if (ctrlKey) {
|
|
200
|
+
widget.api.highlightLast();
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
widget.api.highlightNext();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
widget.api.open();
|
|
208
|
+
widget.api.highlightFirst();
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'ArrowUp':
|
|
213
|
+
if (ctrlKey) {
|
|
214
|
+
widget.api.highlightFirst();
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
widget.api.highlightPrevious();
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case 'Enter': {
|
|
221
|
+
const itemCtx = highlighted$();
|
|
222
|
+
if (itemCtx) {
|
|
223
|
+
widget.api.toggleItem(itemCtx.item);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'Escape':
|
|
228
|
+
_dirtyOpened$.set(false);
|
|
229
|
+
break;
|
|
230
|
+
default:
|
|
231
|
+
keyManaged = false;
|
|
232
|
+
}
|
|
233
|
+
if (keyManaged) {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
return widget;
|
|
240
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a number type guard
|
|
3
|
+
* @param value the value to check
|
|
4
|
+
* @returns true if the value is a number
|
|
5
|
+
*/
|
|
6
|
+
export declare function isNumber(value: any): value is number;
|
|
7
|
+
/**
|
|
8
|
+
* a boolean type guard
|
|
9
|
+
* @param value the value to check
|
|
10
|
+
* @returns true if the value is a boolean
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBoolean(value: any): value is boolean;
|
|
13
|
+
/**
|
|
14
|
+
* a function type guard
|
|
15
|
+
* @param value the value to check
|
|
16
|
+
* @returns true if the value is a function
|
|
17
|
+
*/
|
|
18
|
+
export declare function isFunction(value: any): value is (...args: any[]) => any;
|
|
19
|
+
/**
|
|
20
|
+
* a string type guard
|
|
21
|
+
* @param value the value to check
|
|
22
|
+
* @returns true if the value is a string
|
|
23
|
+
*/
|
|
24
|
+
export declare function isString(value: any): value is string;
|
|
25
|
+
/**
|
|
26
|
+
* Clamp the value based on a maximum and optional minimum
|
|
27
|
+
* @param value the value to check
|
|
28
|
+
* @param max the max to clamp to
|
|
29
|
+
* @param [min] the min to clamp to
|
|
30
|
+
* @returns the clamped value
|
|
31
|
+
*/
|
|
32
|
+
export declare function clamp(value: number, max: number, min?: number): number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a number type guard
|
|
3
|
+
* @param value the value to check
|
|
4
|
+
* @returns true if the value is a number
|
|
5
|
+
*/
|
|
6
|
+
export function isNumber(value) {
|
|
7
|
+
return typeof value === 'number' && !isNaN(value) && Number.isFinite(value);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* a boolean type guard
|
|
11
|
+
* @param value the value to check
|
|
12
|
+
* @returns true if the value is a boolean
|
|
13
|
+
*/
|
|
14
|
+
export function isBoolean(value) {
|
|
15
|
+
return value === true || value === false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* a function type guard
|
|
19
|
+
* @param value the value to check
|
|
20
|
+
* @returns true if the value is a function
|
|
21
|
+
*/
|
|
22
|
+
export function isFunction(value) {
|
|
23
|
+
return typeof value === 'function';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* a string type guard
|
|
27
|
+
* @param value the value to check
|
|
28
|
+
* @returns true if the value is a string
|
|
29
|
+
*/
|
|
30
|
+
export function isString(value) {
|
|
31
|
+
return typeof value === 'string';
|
|
32
|
+
}
|
|
33
|
+
// TODO should we check that max > min?
|
|
34
|
+
/**
|
|
35
|
+
* Clamp the value based on a maximum and optional minimum
|
|
36
|
+
* @param value the value to check
|
|
37
|
+
* @param max the max to clamp to
|
|
38
|
+
* @param [min] the min to clamp to
|
|
39
|
+
* @returns the clamped value
|
|
40
|
+
*/
|
|
41
|
+
export function clamp(value, max, min = 0) {
|
|
42
|
+
return Math.max(Math.min(value, max), min);
|
|
43
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReadableSignal } from '@amadeus-it-group/tansu';
|
|
2
|
+
import type { Directive } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Binds the given directive to a store that provides its argument.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
*
|
|
8
|
+
* The returned directive can be used without argument, it will ignore any argument passed to it
|
|
9
|
+
* and will call the provided directive with the content of the provided store as its argument,
|
|
10
|
+
* calling its update method when the content of the store changes.
|
|
11
|
+
*
|
|
12
|
+
* @param directive - directive to bind
|
|
13
|
+
* @param directiveArg$ - store containing the argument of the directive
|
|
14
|
+
* @returns The bound directive that can be used with no argument.
|
|
15
|
+
*/
|
|
16
|
+
export declare const bindDirective: <T>(directive: Directive<T>, directiveArg$: ReadableSignal<T>) => Directive;
|
|
17
|
+
/**
|
|
18
|
+
* Returns a directive that ignores any argument passed to it and calls the provided directive without any
|
|
19
|
+
* argument.
|
|
20
|
+
*
|
|
21
|
+
* @param directive - directive to wrap
|
|
22
|
+
* @returns The resulting directive.
|
|
23
|
+
*/
|
|
24
|
+
export declare const bindDirectiveNoArg: <T>(directive: Directive<void | T>) => Directive;
|
|
25
|
+
/**
|
|
26
|
+
* Returns a directive that subscribes to the given store while it is used on a DOM element,
|
|
27
|
+
* and that unsubscribes from it when it is no longer used.
|
|
28
|
+
*
|
|
29
|
+
* @param store - store on which there will be an active subscription while the returned directive is used.
|
|
30
|
+
* @param asyncUnsubscribe - true if unsubscribing from the store should be done asynchronously (which is the default), and
|
|
31
|
+
* false if it should be done synchronously when the directive is destroyed
|
|
32
|
+
* @returns The resulting directive.
|
|
33
|
+
*/
|
|
34
|
+
export declare const directiveSubscribe: (store: ReadableSignal<any>, asyncUnsubscribe?: boolean) => Directive;
|
|
35
|
+
/**
|
|
36
|
+
* Returns a directive that calls the provided function with the arguments passed to the directive
|
|
37
|
+
* on initialization and each time they are updated.
|
|
38
|
+
*
|
|
39
|
+
* @param update - Function called with the directive argument when the directive is initialized and when its argument is updated.
|
|
40
|
+
* @returns The resulting directive.
|
|
41
|
+
*/
|
|
42
|
+
export declare const directiveUpdate: <T>(update: (arg: T) => void) => Directive<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Utility to create a store that contains an array of items.
|
|
45
|
+
* @returns a store containing an array of items.
|
|
46
|
+
*/
|
|
47
|
+
export declare const registrationArray: <T>() => ReadableSignal<T[]> & {
|
|
48
|
+
register: (element: T) => () => void;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Returns a directive and a store. The store contains at any time the array of all the DOM elements on which the directive is
|
|
52
|
+
* currently used.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* If the directive is intended to be used on a single element element, it may be more appropriate to use
|
|
56
|
+
* {@link createStoreDirective} instead.
|
|
57
|
+
*
|
|
58
|
+
* @returns An object with two properties: the `directive` property that is the directive to use on some DOM elements,
|
|
59
|
+
* and the `elements$` property that is the store containing an array of all the elements on which the directive is currently
|
|
60
|
+
* used.
|
|
61
|
+
*/
|
|
62
|
+
export declare const createStoreArrayDirective: () => {
|
|
63
|
+
directive: Directive;
|
|
64
|
+
elements$: ReadableSignal<HTMLElement[]>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Returns a directive and a store. When the directive is used on a DOM element, the store contains that DOM element.
|
|
68
|
+
* When the directive is not used, the store contains null.
|
|
69
|
+
*
|
|
70
|
+
* @remarks
|
|
71
|
+
* If the directive is used on more than one element, an error is displayed in the console and the element is ignored.
|
|
72
|
+
* If the directive is intended to be used on more than one element, please use {@link createStoreArrayDirective} instead.
|
|
73
|
+
*
|
|
74
|
+
* @returns An object with two properties: the `directive` property that is the directive to use on one DOM element,
|
|
75
|
+
* and the `element$` property that is the store containing the element on which the directive is currently used (or null
|
|
76
|
+
* if the store is not currently used).
|
|
77
|
+
*/
|
|
78
|
+
export declare const createStoreDirective: () => {
|
|
79
|
+
directive: Directive;
|
|
80
|
+
element$: ReadableSignal<HTMLElement | null>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Merges multiple directives into a single directive that executes all of them when called.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* All directives receive the same argument upon initialization and update.
|
|
87
|
+
* Directives are created and updated in the same order as they appear in the arguments list,
|
|
88
|
+
* they are destroyed in the reverse order.
|
|
89
|
+
* All calls to the directives (to create, update and destroy them) are wrapped in a call to the
|
|
90
|
+
* batch function of tansu
|
|
91
|
+
*
|
|
92
|
+
* @param args - directives to merge into a single directive.
|
|
93
|
+
* @returns The resulting merged directive.
|
|
94
|
+
*/
|
|
95
|
+
export declare const mergeDirectives: <T>(...args: (Directive | Directive<T>)[]) => Directive<T>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { asReadable, batch, readable, writable } from '@amadeus-it-group/tansu';
|
|
2
|
+
import { noop } from '../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Binds the given directive to a store that provides its argument.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
*
|
|
8
|
+
* The returned directive can be used without argument, it will ignore any argument passed to it
|
|
9
|
+
* and will call the provided directive with the content of the provided store as its argument,
|
|
10
|
+
* calling its update method when the content of the store changes.
|
|
11
|
+
*
|
|
12
|
+
* @param directive - directive to bind
|
|
13
|
+
* @param directiveArg$ - store containing the argument of the directive
|
|
14
|
+
* @returns The bound directive that can be used with no argument.
|
|
15
|
+
*/
|
|
16
|
+
export const bindDirective = (directive, directiveArg$) => {
|
|
17
|
+
return (element) => {
|
|
18
|
+
let firstTime = true;
|
|
19
|
+
let instance;
|
|
20
|
+
const unsubscribe = directiveArg$.subscribe((value) => {
|
|
21
|
+
if (firstTime) {
|
|
22
|
+
firstTime = false;
|
|
23
|
+
instance = directive(element, value);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
instance?.update?.(value);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
destroy() {
|
|
31
|
+
instance?.destroy?.();
|
|
32
|
+
unsubscribe();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const noArg = readable(undefined);
|
|
38
|
+
/**
|
|
39
|
+
* Returns a directive that ignores any argument passed to it and calls the provided directive without any
|
|
40
|
+
* argument.
|
|
41
|
+
*
|
|
42
|
+
* @param directive - directive to wrap
|
|
43
|
+
* @returns The resulting directive.
|
|
44
|
+
*/
|
|
45
|
+
export const bindDirectiveNoArg = (directive) => bindDirective(directive, noArg);
|
|
46
|
+
/**
|
|
47
|
+
* Returns a directive that subscribes to the given store while it is used on a DOM element,
|
|
48
|
+
* and that unsubscribes from it when it is no longer used.
|
|
49
|
+
*
|
|
50
|
+
* @param store - store on which there will be an active subscription while the returned directive is used.
|
|
51
|
+
* @param asyncUnsubscribe - true if unsubscribing from the store should be done asynchronously (which is the default), and
|
|
52
|
+
* false if it should be done synchronously when the directive is destroyed
|
|
53
|
+
* @returns The resulting directive.
|
|
54
|
+
*/
|
|
55
|
+
export const directiveSubscribe = (store, asyncUnsubscribe = true) => () => {
|
|
56
|
+
const unsubscribe = store.subscribe(noop);
|
|
57
|
+
return {
|
|
58
|
+
destroy: async () => {
|
|
59
|
+
if (asyncUnsubscribe) {
|
|
60
|
+
await 0;
|
|
61
|
+
}
|
|
62
|
+
unsubscribe();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Returns a directive that calls the provided function with the arguments passed to the directive
|
|
68
|
+
* on initialization and each time they are updated.
|
|
69
|
+
*
|
|
70
|
+
* @param update - Function called with the directive argument when the directive is initialized and when its argument is updated.
|
|
71
|
+
* @returns The resulting directive.
|
|
72
|
+
*/
|
|
73
|
+
export const directiveUpdate = (update) => (element, arg) => {
|
|
74
|
+
update(arg);
|
|
75
|
+
return {
|
|
76
|
+
update,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const equalOption = { equal: Object.is };
|
|
80
|
+
/**
|
|
81
|
+
* Utility to create a store that contains an array of items.
|
|
82
|
+
* @returns a store containing an array of items.
|
|
83
|
+
*/
|
|
84
|
+
export const registrationArray = () => {
|
|
85
|
+
const elements$ = writable([], equalOption);
|
|
86
|
+
return asReadable(elements$, {
|
|
87
|
+
/**
|
|
88
|
+
* Add the given element to the array.
|
|
89
|
+
* @param element - Element to be added to the array.
|
|
90
|
+
* @returns A function to remove the element from the array.
|
|
91
|
+
*/
|
|
92
|
+
register: (element) => {
|
|
93
|
+
let removed = false;
|
|
94
|
+
elements$.update((currentElements) => [...currentElements, element]);
|
|
95
|
+
return () => {
|
|
96
|
+
if (!removed) {
|
|
97
|
+
removed = true;
|
|
98
|
+
elements$.update((currentElements) => {
|
|
99
|
+
const index = currentElements.indexOf(element);
|
|
100
|
+
if (index > -1) {
|
|
101
|
+
const copy = [...currentElements];
|
|
102
|
+
copy.splice(index, 1);
|
|
103
|
+
return copy;
|
|
104
|
+
}
|
|
105
|
+
return currentElements; // no change
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Returns a directive and a store. The store contains at any time the array of all the DOM elements on which the directive is
|
|
114
|
+
* currently used.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* If the directive is intended to be used on a single element element, it may be more appropriate to use
|
|
118
|
+
* {@link createStoreDirective} instead.
|
|
119
|
+
*
|
|
120
|
+
* @returns An object with two properties: the `directive` property that is the directive to use on some DOM elements,
|
|
121
|
+
* and the `elements$` property that is the store containing an array of all the elements on which the directive is currently
|
|
122
|
+
* used.
|
|
123
|
+
*/
|
|
124
|
+
export const createStoreArrayDirective = () => {
|
|
125
|
+
const elements$ = registrationArray();
|
|
126
|
+
return {
|
|
127
|
+
elements$: asReadable(elements$),
|
|
128
|
+
directive: (element) => ({ destroy: elements$.register(element) }),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Returns a directive and a store. When the directive is used on a DOM element, the store contains that DOM element.
|
|
133
|
+
* When the directive is not used, the store contains null.
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* If the directive is used on more than one element, an error is displayed in the console and the element is ignored.
|
|
137
|
+
* If the directive is intended to be used on more than one element, please use {@link createStoreArrayDirective} instead.
|
|
138
|
+
*
|
|
139
|
+
* @returns An object with two properties: the `directive` property that is the directive to use on one DOM element,
|
|
140
|
+
* and the `element$` property that is the store containing the element on which the directive is currently used (or null
|
|
141
|
+
* if the store is not currently used).
|
|
142
|
+
*/
|
|
143
|
+
export const createStoreDirective = () => {
|
|
144
|
+
const element$ = writable(null, equalOption);
|
|
145
|
+
return {
|
|
146
|
+
element$: asReadable(element$),
|
|
147
|
+
directive: (element) => {
|
|
148
|
+
let valid = false;
|
|
149
|
+
element$.update((currentElement) => {
|
|
150
|
+
if (currentElement) {
|
|
151
|
+
console.error('The directive cannot be used on multiple elements.', currentElement, element);
|
|
152
|
+
return currentElement;
|
|
153
|
+
}
|
|
154
|
+
valid = true;
|
|
155
|
+
return element;
|
|
156
|
+
});
|
|
157
|
+
return valid
|
|
158
|
+
? {
|
|
159
|
+
destroy() {
|
|
160
|
+
element$.update((currentElement) => (element === currentElement ? null : currentElement));
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Merges multiple directives into a single directive that executes all of them when called.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* All directives receive the same argument upon initialization and update.
|
|
172
|
+
* Directives are created and updated in the same order as they appear in the arguments list,
|
|
173
|
+
* they are destroyed in the reverse order.
|
|
174
|
+
* All calls to the directives (to create, update and destroy them) are wrapped in a call to the
|
|
175
|
+
* batch function of tansu
|
|
176
|
+
*
|
|
177
|
+
* @param args - directives to merge into a single directive.
|
|
178
|
+
* @returns The resulting merged directive.
|
|
179
|
+
*/
|
|
180
|
+
export const mergeDirectives = (...args) => (element, arg) => {
|
|
181
|
+
const instances = batch(() => args.map((directive) => directive(element, arg)));
|
|
182
|
+
return {
|
|
183
|
+
update(arg) {
|
|
184
|
+
batch(() => instances.forEach((instance) => instance?.update?.(arg)));
|
|
185
|
+
},
|
|
186
|
+
destroy() {
|
|
187
|
+
batch(() => instances.reverse().forEach((instance) => instance?.destroy?.()));
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReadableSignal } from '@amadeus-it-group/tansu';
|
|
2
|
+
import type { Directive } from '../types';
|
|
3
|
+
export declare const activeElement$: ReadableSignal<Element | null>;
|
|
4
|
+
export interface HasFocus {
|
|
5
|
+
/**
|
|
6
|
+
* Directive to put on some elements.
|
|
7
|
+
*/
|
|
8
|
+
directive: Directive;
|
|
9
|
+
/**
|
|
10
|
+
* Store that contains true if the activeElement is one of the elements which has the directive,
|
|
11
|
+
* or any of their descendants.
|
|
12
|
+
*/
|
|
13
|
+
hasFocus$: ReadableSignal<boolean>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a HasFocus
|
|
17
|
+
* @returns a HasFocus
|
|
18
|
+
*/
|
|
19
|
+
export declare function createHasFocus(): HasFocus;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { computed, readable } from '@amadeus-it-group/tansu';
|
|
2
|
+
import { createStoreArrayDirective } from './directiveUtils';
|
|
3
|
+
const evtFocusIn = 'focusin';
|
|
4
|
+
const evtFocusOut = 'focusout';
|
|
5
|
+
export const activeElement$ = readable(null, {
|
|
6
|
+
onUse({ set }) {
|
|
7
|
+
function setActiveElement() {
|
|
8
|
+
set(document.activeElement);
|
|
9
|
+
}
|
|
10
|
+
setActiveElement();
|
|
11
|
+
const container = document.documentElement;
|
|
12
|
+
function onFocusOut() {
|
|
13
|
+
setTimeout(setActiveElement);
|
|
14
|
+
}
|
|
15
|
+
container.addEventListener(evtFocusIn, setActiveElement);
|
|
16
|
+
container.addEventListener(evtFocusOut, onFocusOut);
|
|
17
|
+
return () => {
|
|
18
|
+
container.removeEventListener(evtFocusIn, setActiveElement);
|
|
19
|
+
container.removeEventListener(evtFocusOut, onFocusOut);
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
equal: Object.is,
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Create a HasFocus
|
|
26
|
+
* @returns a HasFocus
|
|
27
|
+
*/
|
|
28
|
+
export function createHasFocus() {
|
|
29
|
+
const { elements$, directive } = createStoreArrayDirective();
|
|
30
|
+
const hasFocus$ = computed(() => {
|
|
31
|
+
const activeElement = activeElement$();
|
|
32
|
+
if (!activeElement) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (const element of elements$()) {
|
|
36
|
+
if (element === activeElement || element.contains(activeElement)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
directive,
|
|
44
|
+
hasFocus$,
|
|
45
|
+
};
|
|
46
|
+
}
|