@stimulus-library/mixins 0.9.11
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/UseEventListener.d.ts +19 -0
- package/dist/UseEventListener.js +33 -0
- package/dist/UseFullscreen.d.ts +8 -0
- package/dist/UseFullscreen.js +33 -0
- package/dist/UseGeolocation.d.ts +18 -0
- package/dist/UseGeolocation.js +62 -0
- package/dist/UseInjectedHtml.d.ts +10 -0
- package/dist/UseInjectedHtml.js +37 -0
- package/dist/UseIntersection.d.ts +13 -0
- package/dist/UseIntersection.js +45 -0
- package/dist/UseInterval.d.ts +2 -0
- package/dist/UseInterval.js +13 -0
- package/dist/UseLocalstorage.d.ts +20 -0
- package/dist/UseLocalstorage.js +122 -0
- package/dist/UseMutationObserver.d.ts +2 -0
- package/dist/UseMutationObserver.js +9 -0
- package/dist/UseResizeObserver.d.ts +2 -0
- package/dist/UseResizeObserver.js +9 -0
- package/dist/UseTemporaryContent.d.ts +5 -0
- package/dist/UseTemporaryContent.js +38 -0
- package/dist/createMixin.d.ts +2 -0
- package/dist/createMixin.js +11 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/installClassMethods.d.ts +5 -0
- package/dist/installClassMethods.js +27 -0
- package/dist/useClickOutside.d.ts +4 -0
- package/dist/useClickOutside.js +17 -0
- package/dist/useEventBus.d.ts +7 -0
- package/dist/useEventBus.js +17 -0
- package/dist/useHover.d.ts +4 -0
- package/dist/useHover.js +29 -0
- package/dist/useTimeout.d.ts +2 -0
- package/dist/useTimeout.js +20 -0
- package/dist/useTrixModifiers.d.ts +10 -0
- package/dist/useTrixModifiers.js +71 -0
- package/package.json +50 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useEventListener(controller: Controller, element: Document | Window | HTMLElement, eventNameOrNames: string | string[], handler: (...args: any[]) => void, opts?: AddEventListenerOptions & {
|
|
3
|
+
debounce?: number;
|
|
4
|
+
}): {
|
|
5
|
+
setup: () => void;
|
|
6
|
+
teardown: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function useEventListeners(controller: Controller, element: Document | Window | HTMLElement, eventNameOrNames: string | string[], handler: (...args: any[]) => void, opts?: AddEventListenerOptions & {
|
|
9
|
+
debounce?: number;
|
|
10
|
+
}): {
|
|
11
|
+
setup: () => void;
|
|
12
|
+
teardown: () => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function useCollectionEventListener(controller: Controller, elements: Array<Document | Window | HTMLElement>, eventNameOrNames: string | string[], handler: (...args: any[]) => void, opts?: AddEventListenerOptions & {
|
|
15
|
+
debounce?: number;
|
|
16
|
+
}): (() => void)[];
|
|
17
|
+
export declare function useCollectionEventListeners(controller: Controller, elements: Array<Document | Window | HTMLElement>, eventNameOrNames: string | string[], handler: (...args: any[]) => void, opts?: AddEventListenerOptions & {
|
|
18
|
+
debounce?: number;
|
|
19
|
+
}): (() => void)[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { debounce, wrapArray } from "@stimulus-library/utilities";
|
|
2
|
+
import { useMixin } from "./createMixin";
|
|
3
|
+
export function useEventListener(controller, element, eventNameOrNames, handler, opts) {
|
|
4
|
+
if (opts?.debounce) {
|
|
5
|
+
handler = debounce(handler.bind(controller), opts.debounce);
|
|
6
|
+
delete opts.debounce;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
handler = handler.bind(controller);
|
|
10
|
+
}
|
|
11
|
+
let eventNames = wrapArray(eventNameOrNames);
|
|
12
|
+
let setup = () => eventNames.forEach(eventName => element.addEventListener(eventName, handler, opts));
|
|
13
|
+
let teardown = () => eventNames.forEach(eventName => element.removeEventListener(eventName, handler));
|
|
14
|
+
useMixin(controller, setup, teardown);
|
|
15
|
+
return { setup, teardown };
|
|
16
|
+
}
|
|
17
|
+
export function useEventListeners(controller, element, eventNameOrNames, handler, opts) {
|
|
18
|
+
return useEventListener(controller, element, eventNameOrNames, handler, opts);
|
|
19
|
+
}
|
|
20
|
+
export function useCollectionEventListener(controller, elements, eventNameOrNames, handler, opts) {
|
|
21
|
+
let handlers = [];
|
|
22
|
+
elements.forEach(el => {
|
|
23
|
+
let { setup, teardown } = useEventListener(controller, el, eventNameOrNames, handler, opts);
|
|
24
|
+
handlers.push({ setup, teardown });
|
|
25
|
+
});
|
|
26
|
+
return [
|
|
27
|
+
() => handlers.forEach(h => h.setup()),
|
|
28
|
+
() => handlers.forEach(h => h.teardown()),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function useCollectionEventListeners(controller, elements, eventNameOrNames, handler, opts) {
|
|
32
|
+
return useCollectionEventListener(controller, elements, eventNameOrNames, handler, opts);
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useFullscreen(controller: Controller, el?: Element): {
|
|
3
|
+
isFullscreen: () => boolean;
|
|
4
|
+
enter: () => Promise<void>;
|
|
5
|
+
exit: () => Promise<void>;
|
|
6
|
+
toggle: () => Promise<void>;
|
|
7
|
+
teardown: () => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useFullscreen(controller, el) {
|
|
3
|
+
let element = el || document.documentElement;
|
|
4
|
+
let fullscreenOpen = document.fullscreenElement !== null;
|
|
5
|
+
const updateFullscreenState = () => fullscreenOpen = document.fullscreenElement !== null;
|
|
6
|
+
const isFullscreen = () => fullscreenOpen;
|
|
7
|
+
const toggle = async () => fullscreenOpen ? await exit() : await enter();
|
|
8
|
+
let setup = () => document.addEventListener('fullscreenchange', updateFullscreenState);
|
|
9
|
+
let teardown = () => document.removeEventListener('fullscreenchange', updateFullscreenState);
|
|
10
|
+
const exit = async () => {
|
|
11
|
+
if (document.exitFullscreen) {
|
|
12
|
+
fullscreenOpen = false;
|
|
13
|
+
await document.exitFullscreen();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const enter = async () => {
|
|
17
|
+
if (fullscreenOpen) {
|
|
18
|
+
await exit();
|
|
19
|
+
}
|
|
20
|
+
if (element.requestFullscreen) {
|
|
21
|
+
await element.requestFullscreen();
|
|
22
|
+
fullscreenOpen = true;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
useMixin(controller, setup, teardown);
|
|
26
|
+
return {
|
|
27
|
+
isFullscreen,
|
|
28
|
+
enter,
|
|
29
|
+
exit,
|
|
30
|
+
toggle,
|
|
31
|
+
teardown,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export interface GeolocationOptions extends Partial<PositionOptions> {
|
|
3
|
+
}
|
|
4
|
+
export interface GeolocationData {
|
|
5
|
+
locatedAt: number | null;
|
|
6
|
+
error: GeolocationPositionError | null;
|
|
7
|
+
coords: {
|
|
8
|
+
accuracy: number;
|
|
9
|
+
latitude: number;
|
|
10
|
+
longitude: number;
|
|
11
|
+
altitude: number | null;
|
|
12
|
+
altitudeAccuracy: number | null;
|
|
13
|
+
heading: number | null;
|
|
14
|
+
speed: number | null;
|
|
15
|
+
};
|
|
16
|
+
teardown: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function useGeolocation(controller: Controller, options?: GeolocationOptions, update?: (...args: any[]) => void, error?: (...args: any[]) => void): GeolocationData;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { reactive } from "@stimulus-library/utilities";
|
|
2
|
+
import { useMixin } from "./createMixin";
|
|
3
|
+
export function useGeolocation(controller, options = {}, update, error) {
|
|
4
|
+
// Ensure passed functions are bound to the correct controller scope
|
|
5
|
+
if (update) {
|
|
6
|
+
update = update.bind(controller);
|
|
7
|
+
}
|
|
8
|
+
if (error) {
|
|
9
|
+
error = error.bind(controller);
|
|
10
|
+
}
|
|
11
|
+
// Default options to pass to the navigator.geolocation.watchPosition() method
|
|
12
|
+
const { enableHighAccuracy = true, maximumAge = 30000, timeout = 27000, } = options;
|
|
13
|
+
const isSupported = navigator && 'geolocation' in navigator;
|
|
14
|
+
// Create a reactive object to store the geolocation data
|
|
15
|
+
const values = reactive({
|
|
16
|
+
locatedAt: null,
|
|
17
|
+
error: null,
|
|
18
|
+
coords: {
|
|
19
|
+
accuracy: 0,
|
|
20
|
+
latitude: Infinity,
|
|
21
|
+
longitude: Infinity,
|
|
22
|
+
altitude: null,
|
|
23
|
+
altitudeAccuracy: null,
|
|
24
|
+
heading: null,
|
|
25
|
+
speed: null,
|
|
26
|
+
},
|
|
27
|
+
teardown: () => {
|
|
28
|
+
if (watcher) {
|
|
29
|
+
navigator.geolocation.clearWatch(watcher);
|
|
30
|
+
watcher = null;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
let setup = () => {
|
|
35
|
+
if (isSupported) {
|
|
36
|
+
watcher = navigator.geolocation.watchPosition((position) => {
|
|
37
|
+
// Update reactive values
|
|
38
|
+
values.locatedAt = position.timestamp;
|
|
39
|
+
values.coords = position.coords;
|
|
40
|
+
values.error = null;
|
|
41
|
+
// Fire user callback if provided
|
|
42
|
+
if (update) {
|
|
43
|
+
update(position);
|
|
44
|
+
}
|
|
45
|
+
}, (err) => {
|
|
46
|
+
// Update reactive values
|
|
47
|
+
values.error = err;
|
|
48
|
+
// Fire user callback if provided
|
|
49
|
+
if (error) {
|
|
50
|
+
error(err);
|
|
51
|
+
}
|
|
52
|
+
}, {
|
|
53
|
+
enableHighAccuracy,
|
|
54
|
+
maximumAge,
|
|
55
|
+
timeout,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
let watcher = null;
|
|
60
|
+
useMixin(controller, setup, values.teardown);
|
|
61
|
+
return values;
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useInjectedFragment(controller: Controller, targetElement: HTMLElement, insertPosition: InsertPosition, fragment: DocumentFragment, options?: {
|
|
3
|
+
cleanup?: boolean;
|
|
4
|
+
}): [ChildNode[], () => void];
|
|
5
|
+
export declare function useInjectedHTML(controller: Controller, targetElement: HTMLElement, insertPosition: InsertPosition, html: string, options?: {
|
|
6
|
+
cleanup?: boolean;
|
|
7
|
+
}): [ChildNode[], () => void];
|
|
8
|
+
export declare function useInjectedElement(controller: Controller, targetElement: HTMLElement, insertPosition: InsertPosition, element: HTMLElement, options?: {
|
|
9
|
+
cleanup?: boolean;
|
|
10
|
+
}): [ChildNode, () => void];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useInjectedFragment(controller, targetElement, insertPosition, fragment, options = {}) {
|
|
3
|
+
let nodes = Array.from(fragment.childNodes);
|
|
4
|
+
let setup = () => {
|
|
5
|
+
let parent = targetElement.parentElement;
|
|
6
|
+
if (["beforebegin", "afterend"].includes(insertPosition) && parent == null) {
|
|
7
|
+
throw new Error("Cannot insert beforebegin into a node with no parent");
|
|
8
|
+
}
|
|
9
|
+
switch (insertPosition) {
|
|
10
|
+
case 'beforeend':
|
|
11
|
+
targetElement.append(fragment);
|
|
12
|
+
break;
|
|
13
|
+
case "afterbegin":
|
|
14
|
+
targetElement.prepend(fragment);
|
|
15
|
+
break;
|
|
16
|
+
case "beforebegin":
|
|
17
|
+
parent.insertBefore(fragment, targetElement);
|
|
18
|
+
break;
|
|
19
|
+
case "afterend":
|
|
20
|
+
parent.insertBefore(fragment, targetElement);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let teardown = options.cleanup ? () => nodes.forEach(node => node.remove()) : () => void 0;
|
|
25
|
+
useMixin(controller, setup, teardown);
|
|
26
|
+
return [nodes, teardown];
|
|
27
|
+
}
|
|
28
|
+
export function useInjectedHTML(controller, targetElement, insertPosition, html, options = {}) {
|
|
29
|
+
const fragment = document.createRange().createContextualFragment(html);
|
|
30
|
+
return useInjectedFragment(controller, targetElement, insertPosition, fragment, options);
|
|
31
|
+
}
|
|
32
|
+
export function useInjectedElement(controller, targetElement, insertPosition, element, options = {}) {
|
|
33
|
+
const fragment = new DocumentFragment();
|
|
34
|
+
fragment.append(element);
|
|
35
|
+
let [nodes, teardown] = useInjectedFragment(controller, targetElement, insertPosition, fragment, options);
|
|
36
|
+
return [nodes[0], teardown];
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useIntersectionObserver(controller: Controller, handler: IntersectionObserverCallback, options?: IntersectionObserverInit): {
|
|
3
|
+
observer: IntersectionObserver;
|
|
4
|
+
teardown: () => void;
|
|
5
|
+
observe: (element: HTMLElement) => void | undefined;
|
|
6
|
+
unobserve: (element: HTMLElement) => void | undefined;
|
|
7
|
+
};
|
|
8
|
+
export declare function useIntersection(controller: Controller, element: HTMLElement, appear?: null | ((entry: IntersectionObserverEntry) => void), disappear?: null | ((entry: IntersectionObserverEntry) => void), options?: IntersectionObserverInit): {
|
|
9
|
+
observer: IntersectionObserver;
|
|
10
|
+
observe: () => void | undefined;
|
|
11
|
+
unobserve: () => void | undefined;
|
|
12
|
+
teardown: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useIntersectionObserver(controller, handler, options) {
|
|
3
|
+
handler = handler.bind(controller);
|
|
4
|
+
let observer = new IntersectionObserver(handler, options);
|
|
5
|
+
let teardown = () => {
|
|
6
|
+
observer?.disconnect();
|
|
7
|
+
observer = null;
|
|
8
|
+
};
|
|
9
|
+
let observe = (element) => observer?.observe(element);
|
|
10
|
+
let unobserve = (element) => observer?.unobserve(element);
|
|
11
|
+
return {
|
|
12
|
+
observer,
|
|
13
|
+
teardown,
|
|
14
|
+
observe,
|
|
15
|
+
unobserve,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function useIntersection(controller, element, appear, disappear, options) {
|
|
19
|
+
if (appear) {
|
|
20
|
+
appear = appear.bind(controller);
|
|
21
|
+
}
|
|
22
|
+
if (disappear) {
|
|
23
|
+
disappear = disappear.bind(controller);
|
|
24
|
+
}
|
|
25
|
+
let opts = options ?? {};
|
|
26
|
+
let processEntries = (entries) => {
|
|
27
|
+
entries.forEach((entry) => {
|
|
28
|
+
if (entry.isIntersecting) {
|
|
29
|
+
appear && appear(entry);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
disappear && disappear(entry);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
let { observer, observe, unobserve, teardown } = useIntersectionObserver(controller, processEntries, opts);
|
|
37
|
+
let setup = () => observe(element);
|
|
38
|
+
useMixin(controller, setup, teardown);
|
|
39
|
+
return {
|
|
40
|
+
observer,
|
|
41
|
+
observe: () => observe(element),
|
|
42
|
+
unobserve: () => unobserve(element),
|
|
43
|
+
teardown,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useInterval(controller, handler, interval) {
|
|
3
|
+
handler = handler.bind(controller);
|
|
4
|
+
let intervalHandle = null;
|
|
5
|
+
let setup = () => intervalHandle = setInterval(handler, interval);
|
|
6
|
+
let teardown = () => {
|
|
7
|
+
if (intervalHandle !== null) {
|
|
8
|
+
clearInterval(intervalHandle);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
useMixin(controller, setup, teardown);
|
|
12
|
+
return teardown;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export type Serializer<T> = {
|
|
3
|
+
deserialize(raw: string): T;
|
|
4
|
+
serialize(value: T): string;
|
|
5
|
+
isEmpty(value: T): boolean;
|
|
6
|
+
};
|
|
7
|
+
export interface LocalStorageProxy<T> {
|
|
8
|
+
get value(): T;
|
|
9
|
+
set value(value: T);
|
|
10
|
+
read(): T;
|
|
11
|
+
write(value: T): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
isEmpty(): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set', Serializer<any>> & {
|
|
16
|
+
[idx: string]: Serializer<any>;
|
|
17
|
+
};
|
|
18
|
+
export declare function useLocalStorage<T>(controller: Controller, key: string, defaultValue: T, opts?: {
|
|
19
|
+
writeDefaults: boolean;
|
|
20
|
+
}): LocalStorageProxy<T>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { reactive } from "@stimulus-library/utilities";
|
|
2
|
+
export const StorageSerializers = {
|
|
3
|
+
boolean: {
|
|
4
|
+
deserialize: (v) => v === 'true',
|
|
5
|
+
serialize: (v) => String(v),
|
|
6
|
+
isEmpty: (v) => v === '' || v === null,
|
|
7
|
+
},
|
|
8
|
+
object: {
|
|
9
|
+
deserialize: (v) => JSON.parse(v),
|
|
10
|
+
serialize: (v) => JSON.stringify(v),
|
|
11
|
+
isEmpty: (v) => {
|
|
12
|
+
const values = Object.values(JSON.parse(v));
|
|
13
|
+
return values.length === 0 || values.every(v => v === '' || v === null);
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
number: {
|
|
17
|
+
deserialize: (v) => Number.parseFloat(v),
|
|
18
|
+
serialize: (v) => String(v),
|
|
19
|
+
isEmpty: (v) => v === '' || v === null,
|
|
20
|
+
},
|
|
21
|
+
any: {
|
|
22
|
+
deserialize: (v) => v,
|
|
23
|
+
serialize: (v) => String(v),
|
|
24
|
+
isEmpty: (v) => v === '' || v === null,
|
|
25
|
+
},
|
|
26
|
+
string: {
|
|
27
|
+
deserialize: (v) => v,
|
|
28
|
+
serialize: (v) => String(v),
|
|
29
|
+
isEmpty: (v) => v === '' || v === null,
|
|
30
|
+
},
|
|
31
|
+
map: {
|
|
32
|
+
deserialize: (v) => new Map(JSON.parse(v)),
|
|
33
|
+
serialize: (v) => JSON.stringify(Array.from(v.entries())),
|
|
34
|
+
isEmpty: (v) => {
|
|
35
|
+
const values = Array.from(v.values());
|
|
36
|
+
return values.length === 0 || values.every(v => v === '' || v === null);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
set: {
|
|
40
|
+
deserialize: (v) => new Set(JSON.parse(v)),
|
|
41
|
+
serialize: (v) => JSON.stringify(Array.from(v.entries())),
|
|
42
|
+
isEmpty: (v) => {
|
|
43
|
+
const values = Array.from(v.values());
|
|
44
|
+
return values.length === 0 || values.every(v => v === '' || v === null);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export function useLocalStorage(controller, key, defaultValue, opts = { writeDefaults: true }) {
|
|
49
|
+
let type;
|
|
50
|
+
let { writeDefaults } = opts;
|
|
51
|
+
if (defaultValue == null) {
|
|
52
|
+
type = 'any';
|
|
53
|
+
}
|
|
54
|
+
else if (defaultValue instanceof Set) {
|
|
55
|
+
type = 'set';
|
|
56
|
+
}
|
|
57
|
+
else if (defaultValue instanceof Map) {
|
|
58
|
+
type = 'map';
|
|
59
|
+
}
|
|
60
|
+
else if (typeof defaultValue === 'boolean') {
|
|
61
|
+
type = 'boolean';
|
|
62
|
+
}
|
|
63
|
+
else if (typeof defaultValue === 'string') {
|
|
64
|
+
type = 'string';
|
|
65
|
+
}
|
|
66
|
+
else if (typeof defaultValue === 'object') {
|
|
67
|
+
type = 'object';
|
|
68
|
+
}
|
|
69
|
+
else if (Array.isArray(defaultValue)) {
|
|
70
|
+
type = 'object';
|
|
71
|
+
}
|
|
72
|
+
else if (!Number.isNaN(defaultValue)) {
|
|
73
|
+
type = 'number';
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
type = 'any';
|
|
77
|
+
}
|
|
78
|
+
let data = reactive({
|
|
79
|
+
value: defaultValue,
|
|
80
|
+
});
|
|
81
|
+
let storage = localStorage;
|
|
82
|
+
const serializer = StorageSerializers[type];
|
|
83
|
+
const read = () => {
|
|
84
|
+
const rawValue = storage.getItem(key);
|
|
85
|
+
if (rawValue == null) {
|
|
86
|
+
data.value = defaultValue;
|
|
87
|
+
if (writeDefaults && defaultValue !== null) {
|
|
88
|
+
storage.setItem(key, serializer.serialize(defaultValue));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
data.value = serializer.deserialize(rawValue);
|
|
93
|
+
}
|
|
94
|
+
return data.value;
|
|
95
|
+
};
|
|
96
|
+
const write = (value) => {
|
|
97
|
+
storage.setItem(key, serializer.serialize(value));
|
|
98
|
+
data.value = value;
|
|
99
|
+
};
|
|
100
|
+
const clear = () => {
|
|
101
|
+
storage.removeItem(key);
|
|
102
|
+
data.value = defaultValue;
|
|
103
|
+
return data.value;
|
|
104
|
+
};
|
|
105
|
+
const isEmpty = () => {
|
|
106
|
+
let rawValue = storage.getItem(key);
|
|
107
|
+
return serializer.isEmpty(rawValue);
|
|
108
|
+
};
|
|
109
|
+
read();
|
|
110
|
+
return {
|
|
111
|
+
get value() {
|
|
112
|
+
return read();
|
|
113
|
+
},
|
|
114
|
+
set value(value) {
|
|
115
|
+
write(value);
|
|
116
|
+
},
|
|
117
|
+
read,
|
|
118
|
+
clear,
|
|
119
|
+
write,
|
|
120
|
+
isEmpty,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useMutationObserver(controller, element, handler, options) {
|
|
3
|
+
handler = handler.bind(controller);
|
|
4
|
+
let observer = new MutationObserver(handler);
|
|
5
|
+
let setup = () => observer.observe(element, options);
|
|
6
|
+
let teardown = () => observer.disconnect();
|
|
7
|
+
useMixin(controller, setup, teardown);
|
|
8
|
+
return teardown;
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useResizeObserver(controller, element, handler, options) {
|
|
3
|
+
handler = handler.bind(controller);
|
|
4
|
+
let observer = new ResizeObserver(handler);
|
|
5
|
+
let setup = () => observer.observe(element, options);
|
|
6
|
+
let teardown = () => observer.disconnect();
|
|
7
|
+
useMixin(controller, setup, teardown);
|
|
8
|
+
return teardown;
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useTemporaryContent(controller: Controller, target: HTMLElement, content: string, timeout?: number, teardownCallback?: () => void): {
|
|
3
|
+
teardown: () => void;
|
|
4
|
+
update(newContent: string): void;
|
|
5
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
import { useTimeout } from "./useTimeout";
|
|
3
|
+
import { isHTMLInputElement } from "@stimulus-library/utilities";
|
|
4
|
+
export function useTemporaryContent(controller, target, content, timeout, teardownCallback) {
|
|
5
|
+
const setContent = (element, text) => {
|
|
6
|
+
if (isHTMLInputElement(element)) {
|
|
7
|
+
element.value = text;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
element.textContent = text;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const getContent = (element) => {
|
|
14
|
+
return isHTMLInputElement(element) ? element.value : element.innerHTML;
|
|
15
|
+
};
|
|
16
|
+
let cleanupTimeout = () => void 0;
|
|
17
|
+
let originalText = getContent(target);
|
|
18
|
+
const teardown = () => {
|
|
19
|
+
setContent(target, originalText);
|
|
20
|
+
cleanupTimeout();
|
|
21
|
+
if (teardownCallback) {
|
|
22
|
+
teardownCallback.call(controller);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const setup = () => {
|
|
26
|
+
setContent(target, content);
|
|
27
|
+
if (timeout !== undefined) {
|
|
28
|
+
cleanupTimeout = useTimeout(controller, teardown, timeout);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
useMixin(controller, setup, teardown);
|
|
32
|
+
return {
|
|
33
|
+
teardown,
|
|
34
|
+
update(newContent) {
|
|
35
|
+
setContent(target, newContent);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function useMixin(controller, setup, teardown) {
|
|
2
|
+
const controllerDisconnect = controller.disconnect.bind(controller);
|
|
3
|
+
setup();
|
|
4
|
+
Object.assign(controller, {
|
|
5
|
+
disconnect() {
|
|
6
|
+
teardown();
|
|
7
|
+
controllerDisconnect();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
return controllerDisconnect;
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './installClassMethods';
|
|
2
|
+
export * from './useClickOutside';
|
|
3
|
+
export * from './useEventBus';
|
|
4
|
+
export * from './useEventListener';
|
|
5
|
+
export * from './useFullscreen';
|
|
6
|
+
export * from './useGeolocation';
|
|
7
|
+
export * from './useHover';
|
|
8
|
+
export * from './useInjectedHtml';
|
|
9
|
+
export * from './useIntersection';
|
|
10
|
+
export * from './useInterval';
|
|
11
|
+
export * from './useLocalstorage';
|
|
12
|
+
export * from './useMutationObserver';
|
|
13
|
+
export * from './useResizeObserver';
|
|
14
|
+
export * from './useTemporaryContent';
|
|
15
|
+
export * from './useTimeout';
|
|
16
|
+
export * from './useTrixModifiers';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './installClassMethods';
|
|
2
|
+
export * from './useClickOutside';
|
|
3
|
+
export * from './useEventBus';
|
|
4
|
+
export * from './useEventListener';
|
|
5
|
+
export * from './useFullscreen';
|
|
6
|
+
export * from './useGeolocation';
|
|
7
|
+
export * from './useHover';
|
|
8
|
+
export * from './useInjectedHtml';
|
|
9
|
+
export * from './useIntersection';
|
|
10
|
+
export * from './useInterval';
|
|
11
|
+
export * from './useLocalstorage';
|
|
12
|
+
export * from './useMutationObserver';
|
|
13
|
+
export * from './useResizeObserver';
|
|
14
|
+
export * from './useTemporaryContent';
|
|
15
|
+
export * from './useTimeout';
|
|
16
|
+
export * from './useTrixModifiers';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import { controllerMethod, pascalCase } from "@stimulus-library/utilities";
|
|
3
|
+
export class InstallClassMethodComposableController extends Controller {
|
|
4
|
+
}
|
|
5
|
+
function addMethodsForClassDefinition(controller, name) {
|
|
6
|
+
let defaultElement = controller.element;
|
|
7
|
+
let hasClass = () => controller[`has${pascalCase(name)}Class`] == true;
|
|
8
|
+
let classes = () => controller[`${name}Classes`];
|
|
9
|
+
let defaultClasses = () => controllerMethod(controller, `default${pascalCase(name)}Classes`).call(controller) || [];
|
|
10
|
+
let classOrDefault = () => hasClass() ? classes() : defaultClasses();
|
|
11
|
+
if (controller[`${name}Classes`] == undefined) {
|
|
12
|
+
Object.defineProperty(controller, `${name}Classes`, {
|
|
13
|
+
get: () => hasClass() ? controller[`${name}Class`].split(' ') : defaultClasses(),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
let methods = {
|
|
17
|
+
[`add${pascalCase(name)}Classes`]: (element = defaultElement) => element.classList.add(...classOrDefault()),
|
|
18
|
+
[`remove${pascalCase(name)}Classes`]: (element = defaultElement) => element.classList.remove(...classOrDefault()),
|
|
19
|
+
[`${name}ClassesPresent`]: (element = defaultElement) => classOrDefault().every((klass) => element.classList.contains(klass)),
|
|
20
|
+
};
|
|
21
|
+
Object.assign(controller, methods);
|
|
22
|
+
}
|
|
23
|
+
export function installClassMethods(controller) {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
let classes = controller.constructor.classes || [];
|
|
26
|
+
classes.forEach((classDefinition) => addMethodsForClassDefinition(controller, classDefinition));
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isElementInViewport } from "@stimulus-library/utilities";
|
|
2
|
+
import { useEventListener } from "./useEventListener";
|
|
3
|
+
import { useMixin } from "./createMixin";
|
|
4
|
+
export function useClickOutside(controller, element, callback) {
|
|
5
|
+
callback = callback.bind(controller);
|
|
6
|
+
const handler = (event) => {
|
|
7
|
+
if (element.contains(event.target) || (!isElementInViewport(element))) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
callback(event);
|
|
11
|
+
};
|
|
12
|
+
let { teardown } = useEventListener(controller, window, ["click", "touchend"], handler);
|
|
13
|
+
useMixin(controller, () => void 0, teardown);
|
|
14
|
+
return {
|
|
15
|
+
teardown,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export declare function useEventBus(controller: Controller, eventNameOrNames: string | string[], handler: (...args: any[]) => void, opts?: {
|
|
3
|
+
debounce?: number;
|
|
4
|
+
}): {
|
|
5
|
+
setup: () => void;
|
|
6
|
+
teardown: () => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { debounce, EventBus, wrapArray } from "@stimulus-library/utilities";
|
|
2
|
+
import { useMixin } from "./createMixin";
|
|
3
|
+
export function useEventBus(controller, eventNameOrNames, handler, opts) {
|
|
4
|
+
let options = opts;
|
|
5
|
+
if (options?.debounce) {
|
|
6
|
+
handler = debounce(handler.bind(controller), options.debounce);
|
|
7
|
+
delete options.debounce;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
handler = handler.bind(controller);
|
|
11
|
+
}
|
|
12
|
+
let eventNames = wrapArray(eventNameOrNames);
|
|
13
|
+
let setup = () => eventNames.forEach(eventName => EventBus.on(eventName, handler));
|
|
14
|
+
let teardown = () => eventNames.forEach(eventName => EventBus.off(eventName, handler));
|
|
15
|
+
useMixin(controller, setup, teardown);
|
|
16
|
+
return { setup, teardown };
|
|
17
|
+
}
|
package/dist/useHover.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEventListener } from "./useEventListener";
|
|
2
|
+
import { useMixin } from "./createMixin";
|
|
3
|
+
export function useHover(controller, element, enter, leave) {
|
|
4
|
+
let teardownEnter = null;
|
|
5
|
+
let teardownLeave = null;
|
|
6
|
+
if (enter) {
|
|
7
|
+
enter = enter.bind(controller);
|
|
8
|
+
let { teardown: _teardownEnter } = useEventListener(controller, element, "mouseenter", enter);
|
|
9
|
+
teardownEnter = _teardownEnter;
|
|
10
|
+
}
|
|
11
|
+
if (leave) {
|
|
12
|
+
leave = leave.bind(controller);
|
|
13
|
+
let { teardown: _teardownLeave } = useEventListener(controller, element, "mouseleave", leave);
|
|
14
|
+
teardownLeave = _teardownLeave;
|
|
15
|
+
}
|
|
16
|
+
let setup = () => void 0;
|
|
17
|
+
let teardown = () => {
|
|
18
|
+
if (teardownEnter) {
|
|
19
|
+
teardownEnter();
|
|
20
|
+
}
|
|
21
|
+
if (teardownLeave) {
|
|
22
|
+
teardownLeave();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
useMixin(controller, setup, teardown);
|
|
26
|
+
return {
|
|
27
|
+
teardown,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMixin } from "./createMixin";
|
|
2
|
+
export function useTimeout(controller, handler, timeout) {
|
|
3
|
+
let controllerDisconnect;
|
|
4
|
+
let timeoutHandle = null;
|
|
5
|
+
handler = handler.bind(controller);
|
|
6
|
+
let newHandler = () => {
|
|
7
|
+
handler();
|
|
8
|
+
timeoutHandle = null;
|
|
9
|
+
Object.assign(controller, { disconnect: controllerDisconnect });
|
|
10
|
+
};
|
|
11
|
+
let setup = () => timeoutHandle = setTimeout(newHandler, timeout);
|
|
12
|
+
let teardown = () => {
|
|
13
|
+
if (timeoutHandle !== null) {
|
|
14
|
+
clearTimeout(timeoutHandle);
|
|
15
|
+
timeoutHandle = null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
controllerDisconnect = useMixin(controller, setup, teardown);
|
|
19
|
+
return teardown;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
export interface TrixElementsPayload {
|
|
3
|
+
toolbar: HTMLElement;
|
|
4
|
+
editor: HTMLElement;
|
|
5
|
+
}
|
|
6
|
+
export declare class TrixComposableController extends Controller {
|
|
7
|
+
install?: (elements: TrixElementsPayload) => void;
|
|
8
|
+
uninstall?: (elements: TrixElementsPayload) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function useTrixModifiers(controller: TrixComposableController): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { controllerMethod } from "@stimulus-library/utilities";
|
|
2
|
+
import { Controller } from "@hotwired/stimulus";
|
|
3
|
+
export class TrixComposableController extends Controller {
|
|
4
|
+
}
|
|
5
|
+
export function useTrixModifiers(controller) {
|
|
6
|
+
// keep a copy of the lifecycle function of the controller
|
|
7
|
+
const controllerDisconnect = controller.disconnect.bind(controller);
|
|
8
|
+
let observing = false;
|
|
9
|
+
let observerCallback = (entries, observer) => {
|
|
10
|
+
entries.forEach(mutation => {
|
|
11
|
+
if (mutation.type === 'childList' && Array.from(mutation.addedNodes).some((el) => el.tagName === 'TRIX-TOOLBAR')) {
|
|
12
|
+
attemptSetup();
|
|
13
|
+
observer.disconnect();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
let pasteHandler = (event) => controllerMethod(controller, 'pasteEvent').call(controller, event);
|
|
18
|
+
let observer = new MutationObserver(observerCallback);
|
|
19
|
+
let attemptSetup = () => {
|
|
20
|
+
if (controller.element.tagName !== 'TRIX-EDITOR') {
|
|
21
|
+
throw new Error('Expected controller to be mounted on an instance of <trix-editor>');
|
|
22
|
+
}
|
|
23
|
+
let editor = controller.element;
|
|
24
|
+
let editorParent = controller.element.parentElement;
|
|
25
|
+
if (editorParent == null) {
|
|
26
|
+
throw new Error('Could not traverse DOM tree from <trix-editor>');
|
|
27
|
+
}
|
|
28
|
+
editor.addEventListener('trix-paste', pasteHandler);
|
|
29
|
+
let toolbar = editorParent.querySelector('trix-toolbar');
|
|
30
|
+
if (!observing && !toolbar) {
|
|
31
|
+
// toolbar is not in the DOM yet, wait for it to arrive before running setup
|
|
32
|
+
observing = true;
|
|
33
|
+
observer.observe(editorParent, { childList: true });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
else if (!toolbar) {
|
|
37
|
+
// Fallback, in case this runs twice, or mutation observer logic fails
|
|
38
|
+
throw new Error('Could not find an instance of <trix-toolbar> that is a sibling of this <trix-editor>');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Do not need MutationObserver, all elements are present and correct
|
|
42
|
+
observer.disconnect();
|
|
43
|
+
}
|
|
44
|
+
controllerMethod(controller, 'install').call(controller, { toolbar, editor });
|
|
45
|
+
};
|
|
46
|
+
let teardown = () => {
|
|
47
|
+
if (controller.element.tagName !== 'TRIX-EDITOR') {
|
|
48
|
+
throw new Error('Expected controller to be mounted on an instance of <trix-editor>');
|
|
49
|
+
}
|
|
50
|
+
let editor = controller.element;
|
|
51
|
+
let editorParent = controller.element.parentElement;
|
|
52
|
+
if (editorParent == null) {
|
|
53
|
+
throw new Error('Could not traverse DOM tree from <trix-editor>');
|
|
54
|
+
}
|
|
55
|
+
editor.removeEventListener('trix-paste', pasteHandler);
|
|
56
|
+
let toolbar = editorParent.querySelector('trix-toolbar');
|
|
57
|
+
if (!toolbar) {
|
|
58
|
+
throw new Error('Could not find <trix-toolbar> that is a sibling of this <trix-editor> element');
|
|
59
|
+
}
|
|
60
|
+
controllerMethod(controller, 'uninstall').call(controller, { toolbar, editor });
|
|
61
|
+
};
|
|
62
|
+
attemptSetup();
|
|
63
|
+
Object.assign(controller, {
|
|
64
|
+
disconnect() {
|
|
65
|
+
observer.disconnect();
|
|
66
|
+
teardown();
|
|
67
|
+
controllerMethod(controller, 'uninstall').call({ toolbar, editor: controller.element });
|
|
68
|
+
controllerDisconnect();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stimulus-library/mixins",
|
|
3
|
+
"description": "A library of useful controllers for Stimulus",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"stimulusjs",
|
|
6
|
+
"stimulus-js",
|
|
7
|
+
"stimulus library",
|
|
8
|
+
"stimulus controller",
|
|
9
|
+
"ruby on rails",
|
|
10
|
+
"ruby-on-rails"
|
|
11
|
+
],
|
|
12
|
+
"version": "0.9.11",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Sub-Xaero",
|
|
16
|
+
"url": "https://github.com/Sub-Xaero/"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://sub-xaero.github.io/stimulus-library/",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Sub-Xaero/stimulus-library"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"module": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"prepack": "npm run build",
|
|
32
|
+
"release": "standard-version",
|
|
33
|
+
"test": "cypress run",
|
|
34
|
+
"test:treeshake": "agadoo dist"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@hotwired/stimulus": "^3.0.0",
|
|
38
|
+
"@stimulus-library/utilities": "*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"agadoo": "^3.0.0",
|
|
42
|
+
"cypress": "^12.5.1",
|
|
43
|
+
"fast-glob": "^3.2.12",
|
|
44
|
+
"lerna": "^6.5.1",
|
|
45
|
+
"rimraf": "^4.1.2",
|
|
46
|
+
"standard-version": "^9.5.0",
|
|
47
|
+
"typescript": "^4.9.5",
|
|
48
|
+
"vite": "^4.1.1"
|
|
49
|
+
}
|
|
50
|
+
}
|