v-calendar-3 1.0.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.
Files changed (147) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +142 -0
  3. package/dist/cjs/index.css +1133 -0
  4. package/dist/cjs/index.js +10699 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/package.json +1 -0
  7. package/dist/es/index.js +10678 -0
  8. package/dist/es/index.js.map +1 -0
  9. package/dist/es/style.css +1134 -0
  10. package/dist/iife/index.js +2 -0
  11. package/dist/iife/index.js.map +1 -0
  12. package/dist/mjs/index.mjs +10678 -0
  13. package/dist/mjs/index.mjs.map +1 -0
  14. package/dist/mjs/style.css +1134 -0
  15. package/dist/style.css +1134 -0
  16. package/dist/types/components/BaseIcon/BaseIcon.vue.d.ts +41 -0
  17. package/dist/types/components/BaseIcon/icons/IconChevronDown.vue.d.ts +3 -0
  18. package/dist/types/components/BaseIcon/icons/IconChevronLeft.vue.d.ts +3 -0
  19. package/dist/types/components/BaseIcon/icons/IconChevronRight.vue.d.ts +3 -0
  20. package/dist/types/components/BaseIcon/icons/IconClock.vue.d.ts +3 -0
  21. package/dist/types/components/BaseIcon/icons/index.d.ts +4 -0
  22. package/dist/types/components/BaseSelect/BaseSelect.vue.d.ts +26 -0
  23. package/dist/types/components/Calendar/Calendar.vue.d.ts +2526 -0
  24. package/dist/types/components/Calendar/CalendarDay.vue.d.ts +83 -0
  25. package/dist/types/components/Calendar/CalendarDayPopover.vue.d.ts +3 -0
  26. package/dist/types/components/Calendar/CalendarHeader.vue.d.ts +13 -0
  27. package/dist/types/components/Calendar/CalendarNav.vue.d.ts +4 -0
  28. package/dist/types/components/Calendar/CalendarNavPopover.vue.d.ts +3 -0
  29. package/dist/types/components/Calendar/CalendarPage.vue.d.ts +3 -0
  30. package/dist/types/components/Calendar/CalendarPageProvider.vue.d.ts +17 -0
  31. package/dist/types/components/Calendar/CalendarSlot.vue.d.ts +15 -0
  32. package/dist/types/components/Calendar/CalendarTitleSelect.vue.d.ts +15 -0
  33. package/dist/types/components/CalendarGrid/CalendarCell.vue.d.ts +9 -0
  34. package/dist/types/components/CalendarGrid/CalendarCellPopover.vue.d.ts +209 -0
  35. package/dist/types/components/CalendarGrid/CalendarDayCell.vue.d.ts +8 -0
  36. package/dist/types/components/CalendarGrid/CalendarEventDetails.vue.d.ts +15 -0
  37. package/dist/types/components/CalendarGrid/CalendarEventEdit.vue.d.ts +26 -0
  38. package/dist/types/components/CalendarGrid/CalendarGrid.vue.d.ts +134 -0
  39. package/dist/types/components/CalendarGrid/CalendarGridWeek.vue.d.ts +8 -0
  40. package/dist/types/components/CalendarGrid/CalendarViewSelect.vue.d.ts +3 -0
  41. package/dist/types/components/CalendarGrid/CalendarWeekCell.vue.d.ts +10 -0
  42. package/dist/types/components/CalendarGrid/Constraints.d.ts +16 -0
  43. package/dist/types/components/DatePicker/DatePicker.vue.d.ts +15251 -0
  44. package/dist/types/components/DatePicker/DatePickerBase.vue.d.ts +3 -0
  45. package/dist/types/components/DatePicker/DatePickerPopover.vue.d.ts +3 -0
  46. package/dist/types/components/DatePicker/TimePicker.vue.d.ts +10089 -0
  47. package/dist/types/components/Popover/Popover.vue.d.ts +72 -0
  48. package/dist/types/components/Popover/PopoverRow.vue.d.ts +17 -0
  49. package/dist/types/components/index.d.ts +4 -0
  50. package/dist/types/index.d.cts +40981 -0
  51. package/dist/types/index.d.mts +40981 -0
  52. package/dist/types/index.d.ts +40981 -0
  53. package/dist/types/use/base.d.ts +67 -0
  54. package/dist/types/use/calendar.d.ts +2422 -0
  55. package/dist/types/use/calendarGrid.d.ts +9370 -0
  56. package/dist/types/use/datePicker.d.ts +20210 -0
  57. package/dist/types/use/page.d.ts +20 -0
  58. package/dist/types/use/slots.d.ts +5 -0
  59. package/dist/types/use/timePicker.d.ts +10089 -0
  60. package/dist/types/utils/attribute.d.ts +49 -0
  61. package/dist/types/utils/cache.d.ts +10 -0
  62. package/dist/types/utils/calendar/event.d.ts +111 -0
  63. package/dist/types/utils/config/index.d.ts +5 -0
  64. package/dist/types/utils/date/helpers.d.ts +161 -0
  65. package/dist/types/utils/date/range.d.ts +74 -0
  66. package/dist/types/utils/date/repeat.d.ts +34 -0
  67. package/dist/types/utils/date/rules.d.ts +82 -0
  68. package/dist/types/utils/defaults/index.d.ts +30 -0
  69. package/dist/types/utils/defaults/locales.d.ts +9 -0
  70. package/dist/types/utils/glyph.d.ts +58 -0
  71. package/dist/types/utils/helpers.d.ts +49 -0
  72. package/dist/types/utils/locale.d.ts +48 -0
  73. package/dist/types/utils/page.d.ts +95 -0
  74. package/dist/types/utils/plugins/index.d.ts +4 -0
  75. package/dist/types/utils/popovers.d.ts +47 -0
  76. package/dist/types/utils/theme.d.ts +17 -0
  77. package/dist/types/utils/touch.d.ts +8 -0
  78. package/dist/types/utils/useDisplayMode.d.ts +12 -0
  79. package/dist/types/utils/watchers.d.ts +6 -0
  80. package/package.json +115 -0
  81. package/src/components/BaseIcon/BaseIcon.vue +29 -0
  82. package/src/components/BaseIcon/icons/IconChevronDown.vue +5 -0
  83. package/src/components/BaseIcon/icons/IconChevronLeft.vue +5 -0
  84. package/src/components/BaseIcon/icons/IconChevronRight.vue +5 -0
  85. package/src/components/BaseIcon/icons/IconClock.vue +11 -0
  86. package/src/components/BaseIcon/icons/index.ts +4 -0
  87. package/src/components/BaseSelect/BaseSelect.vue +136 -0
  88. package/src/components/Calendar/Calendar.vue +116 -0
  89. package/src/components/Calendar/CalendarDay.vue +422 -0
  90. package/src/components/Calendar/CalendarDayPopover.vue +44 -0
  91. package/src/components/Calendar/CalendarHeader.vue +203 -0
  92. package/src/components/Calendar/CalendarNav.vue +325 -0
  93. package/src/components/Calendar/CalendarNavPopover.vue +24 -0
  94. package/src/components/Calendar/CalendarPage.vue +140 -0
  95. package/src/components/Calendar/CalendarPageProvider.vue +15 -0
  96. package/src/components/Calendar/CalendarSlot.vue +36 -0
  97. package/src/components/Calendar/CalendarTitleSelect.vue +72 -0
  98. package/src/components/CalendarGrid/CalendarCell.vue +112 -0
  99. package/src/components/CalendarGrid/CalendarCellPopover.vue +93 -0
  100. package/src/components/CalendarGrid/CalendarDayCell.vue +57 -0
  101. package/src/components/CalendarGrid/CalendarEventDetails.vue +71 -0
  102. package/src/components/CalendarGrid/CalendarEventEdit.vue +195 -0
  103. package/src/components/CalendarGrid/CalendarGrid.vue +715 -0
  104. package/src/components/CalendarGrid/CalendarGridWeek.vue +60 -0
  105. package/src/components/CalendarGrid/CalendarViewSelect.vue +34 -0
  106. package/src/components/CalendarGrid/CalendarWeekCell.vue +52 -0
  107. package/src/components/CalendarGrid/Constraints.ts +40 -0
  108. package/src/components/DatePicker/DatePicker.vue +27 -0
  109. package/src/components/DatePicker/DatePickerBase.vue +53 -0
  110. package/src/components/DatePicker/DatePickerPopover.vue +37 -0
  111. package/src/components/DatePicker/TimePicker.vue +167 -0
  112. package/src/components/Popover/Popover.vue +572 -0
  113. package/src/components/Popover/PopoverRow.vue +94 -0
  114. package/src/components/index.ts +4 -0
  115. package/src/index.ts +24 -0
  116. package/src/styles/index.css +48 -0
  117. package/src/styles/theme.css +322 -0
  118. package/src/styles/transitions.css +77 -0
  119. package/src/use/base.ts +144 -0
  120. package/src/use/calendar.ts +796 -0
  121. package/src/use/calendarGrid.ts +954 -0
  122. package/src/use/datePicker.ts +909 -0
  123. package/src/use/page.ts +78 -0
  124. package/src/use/slots.ts +17 -0
  125. package/src/use/timePicker.ts +194 -0
  126. package/src/utils/attribute.ts +107 -0
  127. package/src/utils/cache.ts +30 -0
  128. package/src/utils/calendar/event.ts +335 -0
  129. package/src/utils/config/index.ts +11 -0
  130. package/src/utils/date/helpers.ts +1039 -0
  131. package/src/utils/date/range.ts +274 -0
  132. package/src/utils/date/repeat.ts +135 -0
  133. package/src/utils/date/rules.ts +290 -0
  134. package/src/utils/defaults/index.ts +77 -0
  135. package/src/utils/defaults/locales.ts +123 -0
  136. package/src/utils/defaults/masks.json +15 -0
  137. package/src/utils/defaults/touch.json +5 -0
  138. package/src/utils/glyph.ts +305 -0
  139. package/src/utils/helpers.ts +181 -0
  140. package/src/utils/locale.ts +275 -0
  141. package/src/utils/page.ts +608 -0
  142. package/src/utils/plugins/index.ts +24 -0
  143. package/src/utils/popovers.ts +216 -0
  144. package/src/utils/theme.ts +58 -0
  145. package/src/utils/touch.ts +72 -0
  146. package/src/utils/useDisplayMode.ts +198 -0
  147. package/src/utils/watchers.ts +26 -0
@@ -0,0 +1,216 @@
1
+ import type { Placement } from '@popperjs/core';
2
+ import type { ComponentPublicInstance, Directive, DirectiveBinding } from 'vue';
3
+ import { elementContains, on, resolveDocument, resolveEl } from './helpers';
4
+
5
+ export type PopoverVisibility = 'click' | 'hover' | 'hover-focus' | 'focus';
6
+
7
+ export interface PopoverOptions {
8
+ id: PropertyKey;
9
+ visibility: PopoverVisibility;
10
+ isInteractive: boolean;
11
+ autoHide: boolean;
12
+ force: boolean;
13
+ target: unknown;
14
+ placement: Placement;
15
+ modifiers: any;
16
+ data: any;
17
+ showDelay: number;
18
+ hideDelay: number;
19
+ }
20
+
21
+ export interface PopoverState {
22
+ isVisible: boolean;
23
+ target: unknown;
24
+ data: any;
25
+ transition: string;
26
+ placement: Placement;
27
+ direction: string;
28
+ positionFixed: false;
29
+ modifiers: any[];
30
+ isInteractive: boolean;
31
+ visibility: PopoverVisibility;
32
+ isHovered: boolean;
33
+ isFocused: boolean;
34
+ autoHide: boolean;
35
+ force: boolean;
36
+ }
37
+
38
+ export interface PopoverEvent {
39
+ detail: Partial<PopoverOptions>;
40
+ }
41
+
42
+ export interface PopoverEventHandlers {
43
+ click: (e: MouseEvent) => void;
44
+ mousemove: (e: MouseEvent) => void;
45
+ mouseleave: (e: MouseEvent) => void;
46
+ focusin: (e: MouseEvent) => void;
47
+ focusout: (e: MouseEvent) => void;
48
+ }
49
+
50
+ function dispatchPopoverEvent(
51
+ name: 'show-popover' | 'hide-popover' | 'toggle-popover',
52
+ opts: Partial<PopoverOptions>,
53
+ ) {
54
+ const ownerDocument = resolveDocument(opts.target);
55
+ const CustomEventConstructor = ownerDocument?.defaultView?.CustomEvent;
56
+ if (!ownerDocument || !CustomEventConstructor) return;
57
+ ownerDocument.dispatchEvent(
58
+ new CustomEventConstructor(name, {
59
+ detail: opts,
60
+ }),
61
+ );
62
+ }
63
+
64
+ export function showPopover(opts: Partial<PopoverOptions>) {
65
+ dispatchPopoverEvent('show-popover', opts);
66
+ }
67
+
68
+ export function hidePopover(opts: Partial<PopoverOptions>) {
69
+ dispatchPopoverEvent('hide-popover', opts);
70
+ }
71
+
72
+ export function togglePopover(opts: Partial<PopoverOptions>) {
73
+ dispatchPopoverEvent('toggle-popover', opts);
74
+ }
75
+
76
+ export function getPopoverEventHandlers(
77
+ opts: Partial<PopoverOptions>,
78
+ ): Partial<PopoverEventHandlers> {
79
+ const { visibility } = opts;
80
+ const click = visibility === 'click';
81
+ const hover = visibility === 'hover';
82
+ const hoverFocus = visibility === 'hover-focus';
83
+ const focus = visibility === 'focus';
84
+ opts.autoHide = !click;
85
+ let hovered = false;
86
+ let focused = false;
87
+
88
+ const clickHandler = (e: MouseEvent) => {
89
+ if (click) {
90
+ togglePopover({
91
+ ...opts,
92
+ target: opts.target || (e.currentTarget as HTMLElement),
93
+ });
94
+ e.stopPropagation();
95
+ }
96
+ };
97
+ const mouseMoveHandler = (e: MouseEvent) => {
98
+ if (!hovered) {
99
+ hovered = true;
100
+ if (hover || hoverFocus) {
101
+ showPopover({
102
+ ...opts,
103
+ target: opts.target || (e.currentTarget as HTMLElement),
104
+ });
105
+ }
106
+ }
107
+ };
108
+ const mouseLeaveHandler = (e: MouseEvent) => {
109
+ if (hovered) {
110
+ hovered = false;
111
+ if (hover || (hoverFocus && !focused)) {
112
+ hidePopover({
113
+ ...opts,
114
+ target: opts.target || (e.currentTarget as HTMLElement),
115
+ });
116
+ }
117
+ }
118
+ };
119
+ const focusInHandler = (e: FocusEvent) => {
120
+ if (!focused) {
121
+ focused = true;
122
+ if (focus || hoverFocus) {
123
+ showPopover({
124
+ ...opts,
125
+ target: opts.target || (e.currentTarget as HTMLElement),
126
+ });
127
+ }
128
+ }
129
+ };
130
+ const focusOutHandler = (e: FocusEvent) => {
131
+ if (
132
+ focused &&
133
+ !elementContains(e.currentTarget as Node, e.relatedTarget as Element)
134
+ ) {
135
+ focused = false;
136
+ if (focus || (hoverFocus && !hovered)) {
137
+ hidePopover({
138
+ ...opts,
139
+ target: opts.target || (e.currentTarget as HTMLElement),
140
+ });
141
+ }
142
+ }
143
+ };
144
+
145
+ const handlers: Record<string, Function> = {};
146
+ switch (opts.visibility) {
147
+ case 'click':
148
+ handlers.click = clickHandler;
149
+ break;
150
+ case 'hover':
151
+ handlers.mousemove = mouseMoveHandler;
152
+ handlers.mouseleave = mouseLeaveHandler;
153
+ break;
154
+ case 'focus':
155
+ handlers.focusin = focusInHandler;
156
+ handlers.focusout = focusOutHandler;
157
+ break;
158
+ case 'hover-focus':
159
+ handlers.mousemove = mouseMoveHandler;
160
+ handlers.mouseleave = mouseLeaveHandler;
161
+ handlers.focusin = focusInHandler;
162
+ handlers.focusout = focusOutHandler;
163
+ break;
164
+ }
165
+ return handlers;
166
+ }
167
+
168
+ const removeHandlers = (target: Element | ComponentPublicInstance | string) => {
169
+ const el = resolveEl(target);
170
+ if (el == null) return;
171
+ const handlers = (el as any).popoverHandlers;
172
+ if (!handlers || !handlers.length) return;
173
+ handlers.forEach((handler: Function) => handler());
174
+ delete (el as any).popoverHandlers;
175
+ };
176
+
177
+ const addHandlers = (
178
+ target: Element | ComponentPublicInstance | string,
179
+ opts: Partial<PopoverOptions>,
180
+ ) => {
181
+ const el = resolveEl(target);
182
+ if (el == null) return;
183
+ const remove: Function[] = [];
184
+ const handlers = getPopoverEventHandlers(opts);
185
+ Object.entries(handlers).forEach(([event, handler]) => {
186
+ remove.push(on(el, event, handler as EventListener));
187
+ });
188
+ (el as any).popoverHandlers = remove;
189
+ };
190
+
191
+ export const popoverDirective: Directive = {
192
+ mounted(el: any, binding: DirectiveBinding<PopoverOptions>) {
193
+ const { value } = binding;
194
+ if (!value) return;
195
+ addHandlers(el, value);
196
+ },
197
+ updated(el: any, binding: DirectiveBinding<PopoverOptions>) {
198
+ const { oldValue, value } = binding;
199
+ const oldVisibility = oldValue?.visibility;
200
+ const newVisibility = value?.visibility;
201
+ if (oldVisibility !== newVisibility) {
202
+ if (oldVisibility) {
203
+ removeHandlers(el);
204
+ if (!newVisibility) hidePopover({ ...oldValue, target: el });
205
+ }
206
+ if (newVisibility) {
207
+ addHandlers(el, value);
208
+ }
209
+ }
210
+ },
211
+ unmounted(el: Element, binding: DirectiveBinding<PopoverOptions>) {
212
+ const { value } = binding;
213
+ removeHandlers(el);
214
+ hidePopover({ ...value, target: el, hideDelay: 0, force: true });
215
+ },
216
+ };
@@ -0,0 +1,58 @@
1
+ import type { DateRangeCell } from './date/range';
2
+ import { Attribute } from './attribute';
3
+ import {
4
+ type Bar,
5
+ BarRenderer,
6
+ type Content,
7
+ ContentRenderer,
8
+ type Dot,
9
+ DotRenderer,
10
+ type Glyph,
11
+ type GlyphRenderer,
12
+ type Highlight,
13
+ HighlightRenderer,
14
+ } from './glyph';
15
+
16
+ export interface Glyphs {
17
+ highlights: Highlight[];
18
+ dots: Dot[];
19
+ bars: Bar[];
20
+ content: Content[];
21
+ }
22
+
23
+ export class Theme {
24
+ color: string;
25
+ renderers: GlyphRenderer<Glyph>[] = [
26
+ new ContentRenderer(),
27
+ new HighlightRenderer(),
28
+ new DotRenderer(),
29
+ new BarRenderer(),
30
+ ];
31
+
32
+ constructor(color: string) {
33
+ this.color = color;
34
+ }
35
+
36
+ normalizeGlyphs(attr: Attribute) {
37
+ this.renderers.forEach(renderer => {
38
+ const type = renderer.type as keyof Attribute;
39
+ if (attr[type] != null) {
40
+ // @ts-ignore
41
+ attr[type] = renderer.normalizeConfig(this.color, attr[type]);
42
+ }
43
+ });
44
+ }
45
+
46
+ prepareRender(glyphs: Partial<Glyphs> = {}) {
47
+ this.renderers.forEach(renderer => {
48
+ renderer.prepareRender(glyphs);
49
+ });
50
+ return glyphs;
51
+ }
52
+
53
+ render(cell: DateRangeCell<Attribute>, glyphs: Partial<Glyphs>) {
54
+ this.renderers.forEach(renderer => {
55
+ renderer.render(cell, glyphs);
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,72 @@
1
+ import { isFunction, off, on } from './helpers';
2
+ import type { CustomElement } from './helpers';
3
+
4
+ interface SwipeHandlerOptions {
5
+ maxSwipeTime: number;
6
+ minHorizontalSwipeDistance: number;
7
+ maxVerticalSwipeDistance: number;
8
+ }
9
+
10
+ export const addHorizontalSwipeHandler = (
11
+ element: CustomElement,
12
+ handler: Function,
13
+ {
14
+ maxSwipeTime,
15
+ minHorizontalSwipeDistance,
16
+ maxVerticalSwipeDistance,
17
+ }: SwipeHandlerOptions,
18
+ ) => {
19
+ if (!element || !element.addEventListener || !isFunction(handler)) {
20
+ return null;
21
+ }
22
+ // State variables
23
+ let startX = 0;
24
+ let startY = 0;
25
+ let startTime: number | null = null;
26
+ let isSwiping = false;
27
+ function pointerStart(event: PointerEvent) {
28
+ if (!event.isPrimary || event.pointerType === 'mouse') return;
29
+ startX = event.screenX;
30
+ startY = event.screenY;
31
+ startTime = Date.now();
32
+ isSwiping = true;
33
+ }
34
+
35
+ function pointerEnd(event: PointerEvent) {
36
+ if (!isSwiping || !startTime) return;
37
+ isSwiping = false;
38
+ const deltaX = event.screenX - startX;
39
+ const deltaY = event.screenY - startY;
40
+ const deltaTime = Date.now() - startTime;
41
+ if (deltaTime < maxSwipeTime) {
42
+ if (
43
+ Math.abs(deltaX) >= minHorizontalSwipeDistance &&
44
+ Math.abs(deltaY) <= maxVerticalSwipeDistance
45
+ ) {
46
+ const arg = { toLeft: false, toRight: false };
47
+ if (deltaX < 0) {
48
+ // Swipe to the left
49
+ arg.toLeft = true;
50
+ } else {
51
+ // Swipe to the right
52
+ arg.toRight = true;
53
+ }
54
+ handler(arg);
55
+ }
56
+ }
57
+ }
58
+
59
+ function pointerCancel() {
60
+ isSwiping = false;
61
+ }
62
+
63
+ on(element, 'pointerdown', pointerStart, { passive: true });
64
+ on(element, 'pointerup', pointerEnd, { passive: true });
65
+ on(element, 'pointercancel', pointerCancel, { passive: true });
66
+
67
+ return () => {
68
+ off(element, 'pointerdown', pointerStart);
69
+ off(element, 'pointerup', pointerEnd);
70
+ off(element, 'pointercancel', pointerCancel);
71
+ };
72
+ };
@@ -0,0 +1,198 @@
1
+ import {
2
+ type ComputedRef,
3
+ type Ref,
4
+ computed,
5
+ getCurrentInstance,
6
+ onMounted,
7
+ onUnmounted,
8
+ ref,
9
+ watch,
10
+ } from 'vue';
11
+
12
+ export interface DarkModeClassConfig {
13
+ selector?: string;
14
+ darkClass?: string;
15
+ }
16
+
17
+ export type DarkModeConfig = boolean | 'system' | DarkModeClassConfig;
18
+
19
+ interface DisplayModeState {
20
+ isDark: Ref<boolean>;
21
+ displayMode: ComputedRef<'dark' | 'light'>;
22
+ }
23
+
24
+ type LegacyMediaQueryList = MediaQueryList & {
25
+ addListener?: (listener: (event: MediaQueryListEvent) => void) => void;
26
+ removeListener?: (listener: (event: MediaQueryListEvent) => void) => void;
27
+ };
28
+
29
+ function getElementTheme(element: Element): 'dark' | 'light' | null {
30
+ const attributeTheme =
31
+ element.getAttribute('data-bs-theme') ?? element.getAttribute('data-theme');
32
+ if (attributeTheme === 'dark' || attributeTheme === 'light') {
33
+ return attributeTheme;
34
+ }
35
+
36
+ const colorScheme = (element as HTMLElement).style?.colorScheme
37
+ ?.trim()
38
+ .toLowerCase();
39
+ if (!colorScheme) return null;
40
+
41
+ const supportsDark = colorScheme.split(/\s+/).includes('dark');
42
+ const supportsLight = colorScheme.split(/\s+/).includes('light');
43
+ if (supportsDark === supportsLight) return null;
44
+ return supportsDark ? 'dark' : 'light';
45
+ }
46
+
47
+ function getAncestorTheme(
48
+ element: Element | null,
49
+ documentElement: Element,
50
+ ): 'dark' | 'light' | null {
51
+ for (let current = element; current; current = current.parentElement) {
52
+ const theme = getElementTheme(current);
53
+ if (theme) return theme;
54
+ }
55
+ return getElementTheme(documentElement);
56
+ }
57
+
58
+ export function useDisplayMode(config: Ref<DarkModeConfig>): DisplayModeState {
59
+ const instance = getCurrentInstance();
60
+ const isDark = ref(false);
61
+ const displayMode = computed(() => (isDark.value ? 'dark' : 'light'));
62
+
63
+ let mounted = false;
64
+ let mediaQuery: LegacyMediaQueryList | null = null;
65
+ let mutationObserver: MutationObserver | null = null;
66
+
67
+ const mediaListener = (event: MediaQueryListEvent) => {
68
+ isDark.value = event.matches;
69
+ };
70
+
71
+ function getRootNode(): Node | null {
72
+ return (instance?.proxy?.$el as Node | undefined) ?? null;
73
+ }
74
+
75
+ function getOwnerDocument(): Document | null {
76
+ return (
77
+ getRootNode()?.ownerDocument ??
78
+ (typeof document === 'undefined' ? null : document)
79
+ );
80
+ }
81
+
82
+ function removeMediaQuery() {
83
+ if (!mediaQuery) return;
84
+ if (mediaQuery.removeEventListener) {
85
+ mediaQuery.removeEventListener('change', mediaListener);
86
+ } else {
87
+ mediaQuery.removeListener?.(mediaListener);
88
+ }
89
+ mediaQuery = null;
90
+ }
91
+
92
+ function removeMutationObserver() {
93
+ mutationObserver?.disconnect();
94
+ mutationObserver = null;
95
+ }
96
+
97
+ function cleanup() {
98
+ removeMediaQuery();
99
+ removeMutationObserver();
100
+ }
101
+
102
+ function setupMediaQuery(ownerWindow: Window) {
103
+ if (mediaQuery || typeof ownerWindow.matchMedia !== 'function') return;
104
+ mediaQuery = ownerWindow.matchMedia(
105
+ '(prefers-color-scheme: dark)',
106
+ ) as LegacyMediaQueryList;
107
+ if (mediaQuery.addEventListener) {
108
+ mediaQuery.addEventListener('change', mediaListener);
109
+ } else {
110
+ mediaQuery.addListener?.(mediaListener);
111
+ }
112
+ isDark.value = mediaQuery.matches;
113
+ }
114
+
115
+ function setupSystem(ownerDocument: Document) {
116
+ const rootNode = getRootNode();
117
+ const ElementConstructor = ownerDocument.defaultView?.Element;
118
+ const rootElement =
119
+ ElementConstructor && rootNode instanceof ElementConstructor
120
+ ? rootNode
121
+ : (rootNode?.parentElement ?? null);
122
+
123
+ const update = () => {
124
+ const theme = getAncestorTheme(
125
+ rootElement,
126
+ ownerDocument.documentElement,
127
+ );
128
+ if (theme) {
129
+ removeMediaQuery();
130
+ isDark.value = theme === 'dark';
131
+ return;
132
+ }
133
+ const ownerWindow = ownerDocument.defaultView;
134
+ if (ownerWindow) setupMediaQuery(ownerWindow);
135
+ };
136
+
137
+ update();
138
+ const Observer = ownerDocument.defaultView?.MutationObserver;
139
+ if (Observer) {
140
+ mutationObserver = new Observer(update);
141
+ mutationObserver.observe(ownerDocument.documentElement, {
142
+ attributes: true,
143
+ attributeFilter: ['class', 'style', 'data-bs-theme', 'data-theme'],
144
+ subtree: true,
145
+ });
146
+ }
147
+ }
148
+
149
+ function setupClass(ownerDocument: Document, value: DarkModeClassConfig) {
150
+ const { selector = ':root', darkClass = 'dark' } = value;
151
+ const element = ownerDocument.querySelector(selector);
152
+ if (!element || !darkClass) {
153
+ isDark.value = false;
154
+ return;
155
+ }
156
+
157
+ const update = () => {
158
+ isDark.value = element.classList.contains(darkClass);
159
+ };
160
+ update();
161
+
162
+ const Observer = ownerDocument.defaultView?.MutationObserver;
163
+ if (Observer) {
164
+ mutationObserver = new Observer(update);
165
+ mutationObserver.observe(element, {
166
+ attributes: true,
167
+ attributeFilter: ['class'],
168
+ });
169
+ }
170
+ }
171
+
172
+ function setup() {
173
+ cleanup();
174
+ const value = config.value;
175
+ if (typeof value === 'boolean') {
176
+ isDark.value = value;
177
+ return;
178
+ }
179
+ if (!mounted) return;
180
+
181
+ const ownerDocument = getOwnerDocument();
182
+ if (!ownerDocument) return;
183
+ if (value === 'system') {
184
+ setupSystem(ownerDocument);
185
+ } else {
186
+ setupClass(ownerDocument, value);
187
+ }
188
+ }
189
+
190
+ watch(config, setup, { immediate: true });
191
+ onMounted(() => {
192
+ mounted = true;
193
+ setup();
194
+ });
195
+ onUnmounted(cleanup);
196
+
197
+ return { isDark, displayMode };
198
+ }
@@ -0,0 +1,26 @@
1
+ export const createWatcherController = () => {
2
+ const watchSkippers: Partial<Record<string, number>> = {};
3
+
4
+ const skipWatcher = (watcher: string, durationMs = 10) => {
5
+ watchSkippers[watcher] = Date.now() + durationMs;
6
+ };
7
+
8
+ const unskipWatcher = (watcher: string) => {
9
+ delete watchSkippers[watcher];
10
+ };
11
+
12
+ const handleWatcher = (watcher: string, handler: Function) => {
13
+ if (watcher in watchSkippers) {
14
+ const dateTime = watchSkippers[watcher] as number;
15
+ if (Date.now() < dateTime) return;
16
+ delete watchSkippers[watcher];
17
+ }
18
+ handler();
19
+ };
20
+
21
+ return { skipWatcher, unskipWatcher, handleWatcher };
22
+ };
23
+
24
+ const defaultController = createWatcherController();
25
+
26
+ export const { skipWatcher, unskipWatcher, handleWatcher } = defaultController;