@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.
@@ -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,2 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useInterval(controller: Controller, handler: (...args: any[]) => void, interval: number): () => void;
@@ -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,2 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useMutationObserver(controller: Controller, element: HTMLElement, handler: (entries: MutationRecord[]) => void, options: MutationObserverInit): () => void;
@@ -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,2 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useResizeObserver(controller: Controller, element: HTMLElement, handler: (entries: ResizeObserverEntry[]) => void, options: ResizeObserverOptions): () => void;
@@ -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,2 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useMixin(controller: Controller, setup: () => void, teardown: () => void): () => void;
@@ -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
+ }
@@ -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,5 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare class InstallClassMethodComposableController extends Controller {
3
+ [index: string]: any;
4
+ }
5
+ export declare function installClassMethods(controller: InstallClassMethodComposableController): void;
@@ -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,4 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useClickOutside(controller: Controller, element: HTMLElement, callback: (event: Event) => void): {
3
+ teardown: () => void;
4
+ };
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useHover(controller: Controller, element: HTMLElement, enter?: ((event: Event) => void) | null, leave?: ((event: Event) => void) | null): {
3
+ teardown: () => void;
4
+ };
@@ -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,2 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export declare function useTimeout(controller: Controller, handler: (...args: any[]) => void, timeout: number): () => void;
@@ -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
+ }