@omnitend/dashboard-for-laravel 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/extended/DXForm.vue.d.ts +28 -6
- package/dist/components/extended/DXTable.vue.d.ts +39 -1
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +7 -2
- package/dist/composables/useContainerWidth.d.ts +96 -0
- package/dist/dashboard-for-laravel.js +8138 -7998
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +40 -7
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +80 -2
- package/resources/js/components/extended/DXDashboardSidebar.vue +1 -1
- package/resources/js/components/extended/DXForm.vue +83 -9
- package/resources/js/components/extended/DXTable.vue +217 -12
- package/resources/js/components/extended/DXTableEditorModal.vue +7 -2
- package/resources/js/components/extended/DXTablePagination.vue +19 -3
- package/resources/js/components/extended/DXTableShell.vue +70 -1
- package/resources/js/composables/useContainerWidth.ts +303 -0
- package/resources/js/index.ts +7 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContainerWidth — container-query-style responsiveness for Vue components.
|
|
3
|
+
*
|
|
4
|
+
* Bootstrap's breakpoints are **media** queries: they only ever see the WINDOW.
|
|
5
|
+
* A dashboard page is narrowed by the persistent sidebar (and a form inside a
|
|
6
|
+
* modal is narrower still), so a component can have very little room while the
|
|
7
|
+
* viewport is wide — and no media query ever fires. This composable closes that
|
|
8
|
+
* gap by observing an element's **own rendered width** with a `ResizeObserver`
|
|
9
|
+
* and exposing it reactively, so the template can pick a layout from the space
|
|
10
|
+
* it actually has.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { containerRef, width, isBelow, isNarrowerThan } = useContainerWidth({
|
|
14
|
+
* threshold: 640,
|
|
15
|
+
* });
|
|
16
|
+
* // <div ref="containerRef"> … </div>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* One observer can drive several independent decisions — `isBelow` uses the
|
|
20
|
+
* configured `threshold`, and `isNarrowerThan(n)` answers for any other width:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const stacked = computed(() => isNarrowerThan(576));
|
|
24
|
+
* const hideDetailColumn = computed(() => isNarrowerThan(900));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import {
|
|
28
|
+
getCurrentScope,
|
|
29
|
+
onScopeDispose,
|
|
30
|
+
ref,
|
|
31
|
+
watch,
|
|
32
|
+
type Ref,
|
|
33
|
+
} from "vue";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What to observe: a template ref, or a getter (so the element can be resolved
|
|
37
|
+
* lazily / conditionally — e.g. `() => enabled ? el.value : null` to observe
|
|
38
|
+
* nothing at all).
|
|
39
|
+
*/
|
|
40
|
+
export type ContainerWidthTarget =
|
|
41
|
+
| Ref<HTMLElement | null | undefined>
|
|
42
|
+
| (() => HTMLElement | null | undefined);
|
|
43
|
+
|
|
44
|
+
export interface UseContainerWidthOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Width (px) that `isBelow` compares against. Omit if you only need
|
|
47
|
+
* `width` / `isNarrowerThan`. Pass a getter (`() => props.threshold`) when
|
|
48
|
+
* it can change at runtime — `isBelow` re-evaluates when it does.
|
|
49
|
+
*/
|
|
50
|
+
threshold?: number | (() => number | undefined);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hysteresis band (px) applied to `isBelow` only, to stop a layout flipping
|
|
54
|
+
* back and forth at the boundary. Once `isBelow` is true it stays true
|
|
55
|
+
* until the width reaches `threshold + hysteresis`.
|
|
56
|
+
*
|
|
57
|
+
* Defaults to `0` so `isBelow` means exactly `width < threshold` unless you
|
|
58
|
+
* ask for otherwise. Opt in whenever the layout you drive from it can
|
|
59
|
+
* change the observed width — the classic loop is: narrow layout is taller
|
|
60
|
+
* → an ancestor with `overflow:auto` gains a vertical scrollbar → the
|
|
61
|
+
* container loses ~15-17px → it crosses back over the threshold → repeat.
|
|
62
|
+
* A band wider than a scrollbar (24px is comfortable) breaks that cycle.
|
|
63
|
+
* (`isNarrowerThan` is deliberately raw — it holds no per-width state.)
|
|
64
|
+
*/
|
|
65
|
+
hysteresis?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Width assumed before the first measurement, and permanently where there
|
|
69
|
+
* is no `ResizeObserver` (SSR, or a very old browser).
|
|
70
|
+
*
|
|
71
|
+
* Defaults to `0` — i.e. "assume the narrowest until proven otherwise", so
|
|
72
|
+
* an unmeasured render picks the STACKED layout. That is the mobile-first
|
|
73
|
+
* choice and the safe one: a stacked layout is legible at every width,
|
|
74
|
+
* whereas a wide layout guessed wrongly truncates content. It also means
|
|
75
|
+
* the server-rendered markup matches the first client render.
|
|
76
|
+
*/
|
|
77
|
+
initialWidth?: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Which box to report. `content-box` (default) is the space actually
|
|
81
|
+
* available to children — what a layout decision usually wants.
|
|
82
|
+
* `border-box` matches `getBoundingClientRect().width`.
|
|
83
|
+
*/
|
|
84
|
+
box?: "content-box" | "border-box";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extra debounce (ms) before publishing a new width. Defaults to `0`:
|
|
88
|
+
* `ResizeObserver` already batches to one delivery per frame, and the
|
|
89
|
+
* callback reads the size the observer *hands* it (no forced layout), so
|
|
90
|
+
* there is nothing to thrash. Set it only for genuinely expensive
|
|
91
|
+
* downstream work — a timer adds a frame of visible lag.
|
|
92
|
+
*/
|
|
93
|
+
debounce?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UseContainerWidthReturn {
|
|
97
|
+
/** Bind to the element you want measured (`<div ref="containerRef">`). */
|
|
98
|
+
containerRef: Ref<HTMLElement | null>;
|
|
99
|
+
/** The observed width in px. `initialWidth` until first measured. */
|
|
100
|
+
width: Readonly<Ref<number>>;
|
|
101
|
+
/** False until a real measurement has landed (always false without a `ResizeObserver`). */
|
|
102
|
+
hasMeasured: Readonly<Ref<boolean>>;
|
|
103
|
+
/** `width < threshold`, with `hysteresis` applied. False when no `threshold` was given. */
|
|
104
|
+
isBelow: Readonly<Ref<boolean>>;
|
|
105
|
+
/** Raw `width < px`, for driving other decisions off the same observer. */
|
|
106
|
+
isNarrowerThan: (px: number) => boolean;
|
|
107
|
+
/** Disconnect early. Called automatically when the owning scope is disposed. */
|
|
108
|
+
stop: () => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useContainerWidth(
|
|
112
|
+
options?: UseContainerWidthOptions,
|
|
113
|
+
): UseContainerWidthReturn;
|
|
114
|
+
export function useContainerWidth(
|
|
115
|
+
target: ContainerWidthTarget,
|
|
116
|
+
options?: UseContainerWidthOptions,
|
|
117
|
+
): UseContainerWidthReturn;
|
|
118
|
+
export function useContainerWidth(
|
|
119
|
+
targetOrOptions?: ContainerWidthTarget | UseContainerWidthOptions,
|
|
120
|
+
maybeOptions?: UseContainerWidthOptions,
|
|
121
|
+
): UseContainerWidthReturn {
|
|
122
|
+
// Disambiguate the two overloads: a ref (has `.value`) or a function is a
|
|
123
|
+
// target; anything else is the options object.
|
|
124
|
+
const targetGiven =
|
|
125
|
+
typeof targetOrOptions === "function" ||
|
|
126
|
+
(targetOrOptions !== null &&
|
|
127
|
+
targetOrOptions !== undefined &&
|
|
128
|
+
typeof targetOrOptions === "object" &&
|
|
129
|
+
"value" in targetOrOptions);
|
|
130
|
+
|
|
131
|
+
const target = targetGiven
|
|
132
|
+
? (targetOrOptions as ContainerWidthTarget)
|
|
133
|
+
: undefined;
|
|
134
|
+
const options =
|
|
135
|
+
(targetGiven
|
|
136
|
+
? maybeOptions
|
|
137
|
+
: (targetOrOptions as UseContainerWidthOptions | undefined)) ?? {};
|
|
138
|
+
|
|
139
|
+
const {
|
|
140
|
+
threshold,
|
|
141
|
+
hysteresis = 0,
|
|
142
|
+
initialWidth = 0,
|
|
143
|
+
box = "content-box",
|
|
144
|
+
debounce = 0,
|
|
145
|
+
} = options;
|
|
146
|
+
|
|
147
|
+
/** Returned for the common `<div ref="containerRef">` case. */
|
|
148
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
149
|
+
|
|
150
|
+
const resolveTarget = (): HTMLElement | null => {
|
|
151
|
+
if (target === undefined) return containerRef.value;
|
|
152
|
+
if (typeof target === "function") return target() ?? null;
|
|
153
|
+
return target.value ?? null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const startWidth = Math.round(initialWidth);
|
|
157
|
+
const width = ref<number>(startWidth);
|
|
158
|
+
const hasMeasured = ref<boolean>(false);
|
|
159
|
+
|
|
160
|
+
let observer: ResizeObserver | null = null;
|
|
161
|
+
let observedElement: HTMLElement | null = null;
|
|
162
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
163
|
+
|
|
164
|
+
function publish(nextWidth: number): void {
|
|
165
|
+
// Sub-pixel churn (a 0.3px reflow) must not wake every dependent
|
|
166
|
+
// computed, so publish at whole-pixel resolution and skip no-ops.
|
|
167
|
+
const rounded = Math.round(nextWidth);
|
|
168
|
+
if (!hasMeasured.value) hasMeasured.value = true;
|
|
169
|
+
if (rounded !== width.value) width.value = rounded;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function schedule(nextWidth: number): void {
|
|
173
|
+
if (debounce <= 0) {
|
|
174
|
+
publish(nextWidth);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
178
|
+
debounceTimer = setTimeout(() => {
|
|
179
|
+
debounceTimer = null;
|
|
180
|
+
publish(nextWidth);
|
|
181
|
+
}, debounce);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function measure(entry: ResizeObserverEntry): number {
|
|
185
|
+
if (box === "border-box") {
|
|
186
|
+
// borderBoxSize is an array in the spec (fragmented inline boxes);
|
|
187
|
+
// some engines shipped a bare object first, hence the shape check.
|
|
188
|
+
const sizes = entry.borderBoxSize as unknown;
|
|
189
|
+
if (Array.isArray(sizes) && sizes.length > 0) {
|
|
190
|
+
return sizes[0].inlineSize;
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
sizes !== null &&
|
|
194
|
+
sizes !== undefined &&
|
|
195
|
+
typeof sizes === "object" &&
|
|
196
|
+
"inlineSize" in (sizes as ResizeObserverSize)
|
|
197
|
+
) {
|
|
198
|
+
return (sizes as ResizeObserverSize).inlineSize;
|
|
199
|
+
}
|
|
200
|
+
// No borderBoxSize: fall back to the element's own box.
|
|
201
|
+
return (entry.target as HTMLElement).getBoundingClientRect().width;
|
|
202
|
+
}
|
|
203
|
+
return entry.contentRect.width;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// `stop()` is final: it also tears down the watcher that re-observes when
|
|
207
|
+
// the target changes, so a later ref change cannot silently restart a
|
|
208
|
+
// stopped observer.
|
|
209
|
+
let stopTargetWatch: (() => void) | null = null;
|
|
210
|
+
let stopped = false;
|
|
211
|
+
|
|
212
|
+
function stop(): void {
|
|
213
|
+
stopped = true;
|
|
214
|
+
if (stopTargetWatch !== null) {
|
|
215
|
+
stopTargetWatch();
|
|
216
|
+
stopTargetWatch = null;
|
|
217
|
+
}
|
|
218
|
+
if (debounceTimer !== null) {
|
|
219
|
+
clearTimeout(debounceTimer);
|
|
220
|
+
debounceTimer = null;
|
|
221
|
+
}
|
|
222
|
+
if (observer !== null) {
|
|
223
|
+
observer.disconnect();
|
|
224
|
+
observer = null;
|
|
225
|
+
}
|
|
226
|
+
observedElement = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* SSR guard: `ResizeObserver` (and `window`) do not exist while the docs
|
|
231
|
+
* site server-renders these components. We stay at `initialWidth`, so the
|
|
232
|
+
* markup is the stacked/narrow one — see `initialWidth` above.
|
|
233
|
+
*/
|
|
234
|
+
const canObserve =
|
|
235
|
+
typeof window !== "undefined" && typeof ResizeObserver !== "undefined";
|
|
236
|
+
|
|
237
|
+
function observe(element: HTMLElement | null): void {
|
|
238
|
+
if (stopped) return;
|
|
239
|
+
if (element === observedElement) return;
|
|
240
|
+
if (!canObserve) return;
|
|
241
|
+
|
|
242
|
+
if (observer === null) {
|
|
243
|
+
observer = new ResizeObserver((entries) => {
|
|
244
|
+
// Only the last entry matters — a single element is observed and
|
|
245
|
+
// the observer coalesces to one delivery per frame.
|
|
246
|
+
const entry = entries[entries.length - 1];
|
|
247
|
+
if (entry === undefined) return;
|
|
248
|
+
schedule(measure(entry));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (observedElement !== null) observer.unobserve(observedElement);
|
|
253
|
+
observedElement = element;
|
|
254
|
+
if (element !== null) observer.observe(element, { box });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// `post` flush so the element exists when a v-if/v-for swaps the target.
|
|
258
|
+
stopTargetWatch = watch(resolveTarget, observe, {
|
|
259
|
+
immediate: true,
|
|
260
|
+
flush: "post",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (getCurrentScope() !== undefined) onScopeDispose(stop);
|
|
264
|
+
|
|
265
|
+
const resolveThreshold = (): number | undefined =>
|
|
266
|
+
typeof threshold === "function" ? threshold() : threshold;
|
|
267
|
+
|
|
268
|
+
// `isBelow` is a latched ref rather than a computed BECAUSE of hysteresis:
|
|
269
|
+
// the next answer depends on the current one, which a pure getter cannot
|
|
270
|
+
// express without mutating during evaluation. With `hysteresis: 0` it is
|
|
271
|
+
// exactly `width < threshold`. The watcher is `sync` so `isBelow` is never
|
|
272
|
+
// observed one tick behind `width`.
|
|
273
|
+
const initialThreshold = resolveThreshold();
|
|
274
|
+
const isBelow = ref<boolean>(
|
|
275
|
+
initialThreshold === undefined ? false : startWidth < initialThreshold,
|
|
276
|
+
);
|
|
277
|
+
watch(
|
|
278
|
+
[width, () => resolveThreshold()],
|
|
279
|
+
([currentWidth, currentThreshold]) => {
|
|
280
|
+
if (currentThreshold === undefined) {
|
|
281
|
+
isBelow.value = false;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
isBelow.value = isBelow.value
|
|
285
|
+
? currentWidth < currentThreshold + hysteresis
|
|
286
|
+
: currentWidth < currentThreshold;
|
|
287
|
+
},
|
|
288
|
+
{ flush: "sync" },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
function isNarrowerThan(px: number): boolean {
|
|
292
|
+
return width.value < px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
containerRef,
|
|
297
|
+
width,
|
|
298
|
+
hasMeasured,
|
|
299
|
+
isBelow,
|
|
300
|
+
isNarrowerThan,
|
|
301
|
+
stop,
|
|
302
|
+
};
|
|
303
|
+
}
|
package/resources/js/index.ts
CHANGED
|
@@ -110,6 +110,7 @@ export { default as DTooltip } from "./components/base/DTooltip.vue";
|
|
|
110
110
|
export { useForm } from "./composables/useForm";
|
|
111
111
|
export { defineForm } from "./composables/defineForm";
|
|
112
112
|
export { useToast } from "./composables/useToast";
|
|
113
|
+
export { useContainerWidth } from "./composables/useContainerWidth";
|
|
113
114
|
|
|
114
115
|
// Utils
|
|
115
116
|
export { api } from "./utils/api";
|
|
@@ -168,6 +169,12 @@ export type {
|
|
|
168
169
|
DefineFormReturn,
|
|
169
170
|
} from "./composables/defineForm";
|
|
170
171
|
|
|
172
|
+
export type {
|
|
173
|
+
ContainerWidthTarget,
|
|
174
|
+
UseContainerWidthOptions,
|
|
175
|
+
UseContainerWidthReturn,
|
|
176
|
+
} from "./composables/useContainerWidth";
|
|
177
|
+
|
|
171
178
|
export type {
|
|
172
179
|
ApiError,
|
|
173
180
|
ApiResponse,
|