UNPKG

@hybridly/vue

Version:
1,505 lines (1,476 loc) 46.5 kB
'use strict'; const vue = require('vue'); const core = require('@hybridly/core'); const utils = require('@hybridly/utils'); const nprogress = require('nprogress'); const devtoolsApi = require('@vue/devtools-api'); const qs = require('qs'); const dotDiver = require('@clickbar/dot-diver'); const isEqual = require('lodash.isequal'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const nprogress__default = /*#__PURE__*/_interopDefaultCompat(nprogress); const qs__default = /*#__PURE__*/_interopDefaultCompat(qs); const isEqual__default = /*#__PURE__*/_interopDefaultCompat(isEqual); function progress(options) { const resolved = { delay: 250, color: "#29d", includeCSS: true, spinner: false, ...options }; let timeout; function startProgress() { nprogress__default.done(); nprogress__default.remove(); nprogress__default.start(); } function finishProgress() { if (nprogress__default.isStarted()) { nprogress__default.done(true); setTimeout(() => nprogress__default.remove(), 1e3); } clearTimeout(timeout); } return core.definePlugin({ name: "hybridly:progress", initialized() { nprogress__default.configure({ showSpinner: resolved.spinner }); if (resolved.includeCSS) { injectCSS(resolved.color); } }, start: (context) => { if (context.pendingNavigation?.options.progress === false) { return; } clearTimeout(timeout); timeout = setTimeout(() => startProgress(), resolved.delay); }, progress: (progress2) => { if (nprogress__default.isStarted() && progress2.percentage) { nprogress__default.set(Math.max(nprogress__default.status, progress2.percentage / 100 * 0.9)); } }, after: () => finishProgress() }); } function injectCSS(color) { const element = document.createElement("style"); element.textContent = ` #nprogress { pointer-events: none; --progress-color: ${color}; } #nprogress .bar { background: var(--progress-color); position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } #nprogress .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px var(--progress-color), 0 0 5px var(--progress-color); opacity: 1.0; -webkit-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px); } #nprogress .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #nprogress .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: var(--progress-color); border-left-color: var(--progress-color); border-radius: 50%; -webkit-animation: nprogress-spinner 400ms linear infinite; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { overflow: hidden; position: relative; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { position: absolute; } @-webkit-keyframes nprogress-spinner { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(element); } const DEBUG_KEY = "vue:state:dialog"; const dialogStore = { state: { component: vue.shallowRef(), properties: vue.ref(), key: vue.ref(), show: vue.ref() }, removeComponent() { if (dialogStore.state.component.value) { utils.debug.adapter(DEBUG_KEY, "Removing dialog."); dialogStore.state.component.value = void 0; } }, setComponent(component) { utils.debug.adapter(DEBUG_KEY, "Setting dialog component:", component); dialogStore.state.component.value = component; }, setProperties(properties) { utils.debug.adapter(DEBUG_KEY, "Setting dialog properties:", properties); dialogStore.state.properties.value = vue.unref(properties); }, setKey(key) { utils.debug.adapter(DEBUG_KEY, "Setting dialog key:", { new: key, previous: dialogStore.state.key.value }); dialogStore.state.key.value = vue.unref(key); }, show() { if (!dialogStore.state.show.value) { utils.debug.adapter(DEBUG_KEY, "Showing the dialog."); dialogStore.state.show.value = true; } }, hide() { if (dialogStore.state.show.value) { utils.debug.adapter(DEBUG_KEY, "Hiding the dialog."); dialogStore.state.show.value = false; } } }; const onMountedCallbacks = []; const state = { context: vue.shallowRef(), view: vue.shallowRef(), properties: vue.ref(), viewKey: vue.ref(), setView(view) { utils.debug.adapter("vue:state:view", "Storing view:", view); state.view.value = view; }, setProperties(properties) { utils.debug.adapter("vue:state:view", "Storing properties:", properties); state.properties.value = properties; }, setContext(context) { utils.debug.adapter("vue:state:context", "Storing context:", context); if (vue.unref(context) === state.context.value) { vue.triggerRef(state.context); } else { state.context.value = vue.unref(context); } }, setViewKey(key) { utils.debug.adapter("vue:state:key", "Storing view key:", key); state.viewKey.value = vue.unref(key); } }; const wrapper = vue.defineComponent({ name: "Hybridly", setup() { function renderLayout(view) { utils.debug.adapter("vue:render:layout", "Rendering layout."); if (typeof state.view.value?.layout === "function") { return state.view.value.layout(vue.h, view, renderDialog(), { ...state.view.value?.properties ?? {}, ...state.properties.value }); } if (Array.isArray(state.view.value?.layout)) { const layoutsAndView = state.view.value.layout.concat(view).reverse().reduce((child, layout) => { layout.inheritAttrs = !!layout.inheritAttrs; return vue.h(layout, { ...state.view.value?.properties ?? {}, ...state.properties.value }, () => child); }); return [layoutsAndView, renderDialog()]; } return [ vue.h(state.view.value?.layout, { ...state.view.value?.properties ?? {}, ...state.properties.value }, () => view), renderDialog() ]; } function hijackOnMounted(component, type) { if (!component) { return; } const actual = component?.mounted; component.mounted = () => { actual?.(); vue.nextTick(() => { utils.debug.adapter(`vue:render:${type}`, "Calling mounted callbacks."); while (onMountedCallbacks.length) { onMountedCallbacks.shift()?.(); } }); }; } function renderView() { utils.debug.adapter("vue:render:view", "Rendering view."); state.view.value.inheritAttrs = !!state.view.value.inheritAttrs; hijackOnMounted(state.view.value, "view"); return vue.h(state.view.value, { ...state.properties.value, key: state.viewKey.value }); } function renderDialog() { if (dialogStore.state.component.value && dialogStore.state.properties.value) { utils.debug.adapter("vue:render:dialog", "Rendering dialog."); hijackOnMounted(dialogStore.state.component.value, "dialog"); return vue.h(dialogStore.state.component.value, { ...dialogStore.state.properties.value, key: dialogStore.state.key.value }); } } return (...a) => { if (!state.view.value) { return; } utils.debug.adapter("vue:render:wrapper", "Rendering wrapper component.", a.map(vue.toRaw)); const view = renderView(); if (state.view.value.layout) { return renderLayout(view); } return [view, renderDialog()]; }; } }); const hybridlyStateType = "hybridly"; const hybridlyEventsTimelineLayerId = "Hybridly"; function setupDevtools(app) { devtoolsApi.setupDevtoolsPlugin({ id: "hybridly", label: "Hybridly", packageName: "@hybridly/vue", homepage: "https://github.com/hybridly", app, enableEarlyProxy: true, componentStateTypes: [ hybridlyStateType ] }, (api) => { api.on.inspectComponent((payload) => { payload.instanceData.state.push({ type: hybridlyStateType, key: "properties", value: state.context.value?.view.properties, editable: true }); payload.instanceData.state.push({ type: hybridlyStateType, key: "component", value: state.context.value?.view.component }); payload.instanceData.state.push({ type: hybridlyStateType, key: "deferred", value: state.context.value?.view.deferred }); payload.instanceData.state.push({ type: hybridlyStateType, key: "dialog", value: state.context.value?.dialog }); payload.instanceData.state.push({ type: hybridlyStateType, key: "version", value: state.context.value?.version }); payload.instanceData.state.push({ type: hybridlyStateType, key: "url", value: state.context.value?.url }); payload.instanceData.state.push({ type: hybridlyStateType, key: "routing", value: state.context.value?.routing }); }); api.on.editComponentState((payload) => { if (payload.type === hybridlyStateType) { payload.set(state.context.value?.view); } }); api.addTimelineLayer({ id: hybridlyEventsTimelineLayerId, color: 16501221, label: "Hybridly" }); const listen = [ "start", "ready", "data", "navigating", "navigated", "progress", "error", "abort", "success", "invalid", "exception", "fail", "after", "backForward", "success" ]; core.registerHook("before", (options) => { const groupId = (Math.random() + 1).toString(36).substring(7); api.addTimelineEvent({ layerId: hybridlyEventsTimelineLayerId, event: { groupId, title: "before", time: api.now(), data: options } }); listen.forEach((event) => core.registerHook(event, (data) => { api.addTimelineEvent({ layerId: hybridlyEventsTimelineLayerId, event: { groupId, title: event, time: api.now(), data } }); if (event === "after") { setTimeout(() => { api.notifyComponentUpdate(); }, 100); } }, { once: true })); }); }); } const devtools = { install(app) { if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) { setupDevtools(app); } } }; function viewTransition() { if (!document.startViewTransition) { return { name: "view-transition" }; } let domUpdated; return { name: "view-transition", navigating: async ({ type, hasDialog }) => { if (type === "initial" || hasDialog) { return; } return new Promise((confirmTransitionStarted) => document.startViewTransition(() => { confirmTransitionStarted(true); return new Promise((resolve) => domUpdated = resolve); })); }, mounted: () => { domUpdated?.(); domUpdated = void 0; }, navigated: () => { domUpdated?.(); domUpdated = void 0; } }; } const formStore = { defaultConfig: {}, setDefaultConfig: (config) => { formStore.defaultConfig = config; }, getDefaultConfig: () => { return utils.clone(formStore.defaultConfig); } }; async function initializeHybridly(options = {}) { const resolved = options; const { element, payload, resolve } = prepare(resolved); if (!element) { throw new Error("Could not find an HTML element to initialize Vue on."); } state.setContext(await core.createRouter({ axios: resolved.axios, plugins: resolved.plugins, serializer: resolved.serializer, responseErrorModals: resolved.responseErrorModals ?? process.env.NODE_ENV === "development", routing: resolved.routing, adapter: { resolveComponent: resolve, executeOnMounted: (callback) => { onMountedCallbacks.push(callback); }, onDialogClose: async () => { dialogStore.hide(); }, onContextUpdate: (context) => { state.setContext(context); }, onViewSwap: async (options2) => { if (options2.component) { onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: false })); state.setView(options2.component); } state.setProperties(options2.properties); if (!options2.preserveState && !options2.dialog) { state.setViewKey(utils.random()); } if (options2.dialog) { onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: true })); dialogStore.setComponent(await resolve(options2.dialog.component)); dialogStore.setProperties(options2.dialog.properties); dialogStore.setKey(options2.dialog.key); dialogStore.show(); } else { dialogStore.hide(); } } }, payload })); const render = () => vue.h(wrapper); if (options.setup) { return await options.setup({ element, wrapper, render, hybridly: devtools, props: { context: state.context.value }, payload }); } const app = vue.createApp({ render }); if (resolved.devtools !== false) { app.use(devtools); } await options.enhanceVue?.(app, payload); return app.mount(element); } function prepare(options) { utils.debug.adapter("vue", "Preparing Hybridly with options:", options); const isServer = typeof window === "undefined"; const id = options.id ?? "root"; const element = document?.getElementById(id) ?? void 0; utils.debug.adapter("vue", `Element "${id}" is:`, element); const payload = element?.dataset.payload ? JSON.parse(element.dataset.payload) : void 0; if (!payload) { throw new Error("No payload found. Are you using the `@hybridly` directive?"); } if (options.cleanup !== false) { delete element.dataset.payload; } utils.debug.adapter("vue", "Resolved:", { isServer, element, payload }); const resolve = async (name) => { utils.debug.adapter("vue", "Resolving component", name); if (!options.imported) { throw new Error("No component loaded. Did you initialize Hybridly? Does `php artisan hybridly:config` return an error?"); } return await resolveViewComponent(name, options); }; options.plugins ??= []; if (options.progress !== false) { options.plugins.push(progress(typeof options.progress === "object" ? options.progress : {})); } if (options.viewTransition !== false) { options.plugins.push(viewTransition()); } if (options.defaultFormOptions) { formStore.setDefaultConfig(options.defaultFormOptions); } return { isServer, element, payload, resolve }; } async function resolveViewComponent(name, options) { const components = options.imported; const result = options.components.views.find((view) => name === view.identifier); const path = Object.keys(components).sort((a, b) => a.length - b.length).find((path2) => result ? path2.endsWith(result?.path) : false); if (!result || !path) { console.warn(`View component [${name}] not found. Available components: `, options.components.views.map(({ identifier }) => identifier)); utils.showViewComponentErrorModal(name); return; } let component = typeof components[path] === "function" ? await components[path]() : components[path]; component = component.default ?? component; return component; } const RouterLink = vue.defineComponent({ name: "RouterLink", setup(_, { slots, attrs }) { return (props) => { let data = props.data ?? {}; const preloads = props.preload ?? false; const preserveScroll = props.preserveScroll; const preserveState = props.preserveState; const url = core.makeUrl(props.href ?? ""); const method = props.method?.toUpperCase() ?? "GET"; const as = typeof props.as === "object" ? props.as : props.as?.toLowerCase() ?? "a"; if (method === "GET") { utils.debug.adapter("vue", "Moving data object to URL parameters."); url.search = qs__default.stringify(utils.merge(data, qs__default.parse(url.search, { ignoreQueryPrefix: true })), { encodeValuesOnly: true, arrayFormat: "indices" }); data = {}; } if (as === "a" && method !== "GET") { utils.debug.adapter("vue", `Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues. Please specify a more appropriate element using the "as" attribute. For example: <RouterLink href="${url}" method="${method}" as="button">...</RouterLink>`); } function performPreload(type) { if (!preloads) { return; } if (props.external) { return; } if (method !== "GET") { return; } if (type !== "mount" && props.disabled) { return; } if (type === "hover" && preloads === "mount") { return; } if (type === "mount" && preloads !== "mount") { return; } core.router.preload(url, { data, preserveScroll, preserveState, ...props.options }); } performPreload("mount"); return vue.h(props.as, { ...attrs, ...as === "a" ? { href: url } : {}, ...props.disabled ? { disabled: props.disabled } : {}, onMouseenter: () => performPreload("hover"), onAuxclick: (event) => { if (props.disabled) { event.preventDefault(); } }, onClick: (event) => { if (props.disabled) { event.preventDefault(); return; } if (props.external) { return; } if (!shouldIntercept(event)) { return; } event.preventDefault(); core.router.navigate({ url, data, method, preserveState: method !== "GET", ...props.options }); } }, slots.default ? slots : props.text); }; }, props: { href: { type: String, required: false, default: void 0 }, as: { type: [String, Object], default: "a" }, method: { type: String, default: "GET" }, data: { type: Object, default: () => ({}) }, external: { type: Boolean, default: false }, disabled: { type: Boolean, default: false }, options: { type: Object, default: () => ({}) }, text: { type: String, required: false, default: void 0 }, preload: { type: [Boolean, String], default: false }, preserveScroll: { type: Boolean, default: void 0 }, preserveState: { type: Boolean, default: void 0 } } }); function shouldIntercept(event) { const isLink = event.currentTarget.tagName.toLowerCase() === "a"; return !(event.target && (event?.target).isContentEditable || event.defaultPrevented || isLink && event.which > 1 || isLink && event.altKey || isLink && event.ctrlKey || isLink && event.metaKey || isLink && event.shiftKey); } function toReactive(objectRef) { if (!vue.isRef(objectRef)) { return vue.reactive(objectRef); } const proxy = new Proxy({}, { get(_, p, receiver) { return vue.unref(Reflect.get(objectRef.value, p, receiver)); }, set(_, p, value) { if (vue.isRef(objectRef.value[p]) && !vue.isRef(value)) { objectRef.value[p].value = value; } else { objectRef.value[p] = value; } return true; }, deleteProperty(_, p) { return Reflect.deleteProperty(objectRef.value, p); }, has(_, p) { return Reflect.has(objectRef.value, p); }, ownKeys() { return Object.keys(objectRef.value); }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true }; } }); return vue.reactive(proxy); } function useProperties() { return vue.readonly(toReactive(vue.computed(() => state.properties.value))); } function useProperty(path) { return vue.computed(() => dotDiver.getByPath(state.properties.value, path)); } function setProperty(path, value) { if (!state.properties.value) { return; } dotDiver.setByPath(state.properties.value, path, vue.toValue(value)); if (state.context.value?.view.properties) { dotDiver.setByPath(state.context.value.view.properties, path, vue.toValue(value)); } } function safeClone(obj) { return utils.clone(vue.toRaw(obj)); } function useForm(options) { const shouldRemember = !!options.key; const historyKey = options.key ?? "form:default"; const historyData = shouldRemember ? core.router.history.get(historyKey) : void 0; const timeoutIds = { recentlyFailed: void 0, recentlySuccessful: void 0 }; const initial = safeClone(options.fields); const loaded = safeClone(historyData?.fields ?? options.fields); const fields = vue.reactive(safeClone(loaded)); const errors = vue.ref(historyData?.errors ?? {}); const isDirty = vue.ref(false); const recentlySuccessful = vue.ref(false); const successful = vue.ref(false); const recentlyFailed = vue.ref(false); const failed = vue.ref(false); const processing = vue.ref(false); const progress = vue.ref(); function setInitial(newInitial) { Object.entries(newInitial).forEach(([key, value]) => { Reflect.set(initial, key, safeClone(value)); }); } function resetSubmissionState() { successful.value = false; failed.value = false; recentlyFailed.value = false; recentlySuccessful.value = false; clearTimeout(timeoutIds.recentlySuccessful); clearTimeout(timeoutIds.recentlyFailed); progress.value = void 0; } function reset() { resetSubmissionState(); clearErrors(); resetFields(); } function resetFields(...keys) { if (keys.length === 0) { keys = Object.keys(fields); } keys.forEach((key) => { Reflect.set(fields, key, safeClone(Reflect.get(initial, key))); }); } function clear(...keys) { if (keys.length === 0) { keys = Object.keys(fields); } keys.forEach((key) => { delete fields[key]; }); } function submit(optionsOverrides) { const { fields: _f, key: _k, ...optionsWithoutFields } = options; const resolvedOptions = optionsOverrides ? utils.merge(optionsWithoutFields, optionsOverrides, { mergePlainObjects: true }) : optionsWithoutFields; const optionsWithOverrides = utils.merge(formStore.getDefaultConfig(), resolvedOptions, { mergePlainObjects: true }); const url = typeof optionsWithOverrides.url === "function" ? optionsWithOverrides.url() : optionsWithOverrides.url; const data = typeof optionsWithOverrides.transform === "function" ? optionsWithOverrides.transform(fields) : fields; const preserveState = optionsWithOverrides.preserveState ?? optionsWithOverrides.method !== "GET"; const hooks = optionsWithOverrides.hooks ?? {}; return core.router.navigate({ ...optionsWithOverrides, url: url ?? state.context.value?.url, method: optionsWithOverrides.method ?? "POST", data: safeClone(data), preserveState, hooks: { before: (navigation, context) => { resetSubmissionState(); return hooks.before?.(navigation, context); }, start: (context) => { processing.value = true; return hooks.start?.(context); }, progress: (incoming, context) => { progress.value = incoming; return hooks.progress?.(incoming, context); }, error: (incoming, context) => { setErrors(incoming); failed.value = true; recentlyFailed.value = true; timeoutIds.recentlyFailed = setTimeout(() => recentlyFailed.value = false, optionsWithOverrides.timeout ?? 5e3); return hooks.error?.(incoming, context); }, success: (payload, context) => { clearErrors(); if (optionsWithOverrides.updateInitials) { setInitial(fields); } if (optionsWithOverrides.reset !== false) { resetFields(); } successful.value = true; recentlySuccessful.value = true; timeoutIds.recentlySuccessful = setTimeout(() => recentlySuccessful.value = false, optionsWithOverrides.timeout ?? 5e3); return hooks.success?.(payload, context); }, after: (context) => { progress.value = void 0; processing.value = false; return hooks.after?.(context); } } }); } function clearErrors(...keys) { if (keys.length === 0) { keys = Object.keys(fields); } keys.forEach((key) => { clearError(key); }); } function hasDirty(...keys) { if (keys.length === 0) { return isDirty.value; } return keys.some((key) => !isEqual__default(vue.toRaw(dotDiver.getByPath(fields, key)), vue.toRaw(dotDiver.getByPath(initial, key)))); } function clearError(key) { utils.unsetPropertyAtPath(errors.value, key); } function setErrors(incoming) { clearErrors(); Object.entries(incoming).forEach(([path, value]) => { utils.setValueAtPath(errors.value, path, value); }); } function abort() { core.router.abort(); } vue.watch([fields, processing, errors], () => { isDirty.value = !isEqual__default(vue.toRaw(initial), vue.toRaw(fields)); if (shouldRemember) { core.router.history.remember(historyKey, { fields: vue.toRaw(fields), errors: vue.toRaw(errors.value) }); } }, { deep: true, immediate: true }); return vue.reactive({ resetFields, reset, resetSubmissionState, clear, fields, abort, setErrors, clearErrors, clearError, setInitial, hasDirty, submitWith: submit, /** @deprecated Use `submitWith` instead */ submitWithOptions: submit, submit: () => submit(), hasErrors: vue.computed(() => Object.values(errors.value ?? {}).length > 0), initial, loaded, progress, isDirty, errors, processing, successful, failed, recentlySuccessful, recentlyFailed }); } function useHistoryState(key, initial) { const value = vue.ref(core.router.history.get(key) ?? initial); vue.watch(value, (value2) => { core.router.history.remember(key, vue.toRaw(value2)); }, { immediate: true, deep: true }); return value; } function useBackForward(options) { const callbacks = []; core.registerHook("navigated", (options2) => { if (options2.type === "back-forward") { callbacks.forEach((fn) => fn(state.context.value)); callbacks.splice(0, callbacks.length); } }); function onBackForward(fn) { callbacks.push(fn); } function reloadOnBackForward(options2) { onBackForward(() => core.router.reload(options2)); } if (options?.reload) { reloadOnBackForward(options.reload === true ? void 0 : options.reload); } return { onBackForward, reloadOnBackForward }; } const registerHook = (hook, fn, options) => { const unregister = core.registerHook(hook, fn, options); if (vue.getCurrentInstance()) { vue.onUnmounted(() => unregister()); } return unregister; }; function useDialog() { return { /** Closes the dialog. */ close: () => core.router.dialog.close(), /** Closes the dialog without a server round-trip. */ closeLocally: () => core.router.dialog.close({ local: true }), /** Unmounts the dialog. Should be called after its closing animations. */ unmount: () => dialogStore.removeComponent(), /** Whether the dialog is shown. */ show: vue.computed({ get: () => dialogStore.state.show.value, set: (v) => !v ? core.router.dialog.close({ local: true }) : null }), /** Properties of the dialog. */ properties: vue.computed(() => state.context.value?.dialog?.properties) }; } function useRefinements(properties, refinementsKeys, defaultOptions = {}) { const refinements = vue.computed(() => properties[refinementsKeys]); const sortsKey = vue.computed(() => refinements.value.keys.sorts); const filtersKey = vue.computed(() => refinements.value.keys.filters); defaultOptions = { replace: false, ...defaultOptions }; function getSort(name) { return refinements.value.sorts.find((sort) => sort.name === name); } function getFilter(name) { return refinements.value.filters.find((sort) => sort.name === name); } async function reset(options = {}) { return await core.router.reload({ ...defaultOptions, ...options, data: { [filtersKey.value]: void 0, [sortsKey.value]: void 0 } }); } async function clearFilters(options = {}) { return await core.router.reload({ ...defaultOptions, ...options, data: { [filtersKey.value]: void 0 } }); } async function clearFilter(filter, options = {}) { return await core.router.reload({ ...defaultOptions, ...options, data: { [filtersKey.value]: { [filter]: void 0 } } }); } async function applyFilter(name, value, options = {}) { const filter = getFilter(name); if (!filter) { console.warn(`[Refinement] Filter "${name}" does not exist.`); return; } if (["", null].includes(value) || value === filter.default) { value = void 0; } return await core.router.reload({ ...defaultOptions, ...options, data: { [filtersKey.value]: { [name]: value } } }); } async function clearSorts(options = {}) { return await core.router.reload({ ...defaultOptions, ...options, data: { [sortsKey.value]: void 0 } }); } function currentSorts() { return refinements.value.sorts.filter(({ is_active }) => is_active); } function currentFilters() { return refinements.value.filters.filter(({ is_active }) => is_active); } function isSorting(name, direction) { if (name) { return currentSorts().some((sort) => sort.name === name && (direction ? sort.direction === direction : true)); } return currentSorts().length !== 0; } function isFiltering(name) { if (name) { return currentFilters().some((filter) => filter.name === name); } return currentFilters().length !== 0; } async function toggleSort(name, options) { const sort = getSort(name); if (!sort) { console.warn(`[Refinement] Sort "${name}" does not exist.`); return; } const next = options?.direction ? sort[options?.direction] : sort.next; const sortData = next ? options?.sortData ?? {} : Object.fromEntries(Object.entries(options?.sortData ?? {}).map(([key, _]) => [key, void 0])); return await core.router.reload({ ...defaultOptions, ...options, data: { [sortsKey.value]: next || void 0, ...sortData } }); } function bindFilter(name, options = {}) { const transform = options?.transformValue ?? ((value) => value); const watchFn = options?.watch ?? vue.watch; const getFilterValue = () => transform(refinements.value.filters.find((f) => f.name === name)?.value); const _proxy = vue.ref(getFilterValue()); let filterIsBeingApplied = false; let proxyIsBeingUpdated = false; const debouncedApplyFilter = utils.debounce(options.debounce ?? 250, async (value) => { await applyFilter(name, transform(value), options); vue.nextTick(() => filterIsBeingApplied = false); }); const debounceUpdateProxyValue = utils.debounce(options.syncDebounce ?? 250, () => { const filter = refinements.value.filters.find((f) => f.name === name); if (filter) { _proxy.value = transform(filter?.value); } vue.nextTick(() => proxyIsBeingUpdated = false); }, { atBegin: true }); vue.watch(() => refinements.value.filters.find((f) => f.name === name)?.value, () => { if (filterIsBeingApplied === true) { return; } proxyIsBeingUpdated = true; debounceUpdateProxyValue(); }, { deep: true }); watchFn(_proxy, async (value) => { if (proxyIsBeingUpdated === true) { return; } filterIsBeingApplied = true; debouncedApplyFilter(value); }); return _proxy; } return { /** * Binds a named filter to a ref, applying filters when it changes and updating the ref accordingly. */ bindFilter, /** * Available filters. */ filters: toReactive(refinements.value.filters.map((filter) => ({ ...filter, /** * Applies this filter. */ apply: (value, options) => applyFilter(filter.name, value, options), /** * Clears this filter. */ clear: (options) => clearFilter(filter.name, options) }))), /** * Available sorts. */ sorts: toReactive(refinements.value.sorts.map((sort) => ({ ...sort, /** * Toggles this sort. */ toggle: (options) => toggleSort(sort.name, options), /** * Checks if this sort is active. */ isSorting: (direction) => isSorting(sort.name, direction), /** * Clears this sort. */ clear: (options) => clearSorts(options) }))), /** * The key for the filters. */ filtersKey, /** * Gets a filter by name. */ getFilter, /** * Gets a sort by name. */ getSort, /** * Resets all filters and sorts. */ reset, /** * Toggles the specified sort. */ toggleSort, /** * Whether a sort is active. */ isSorting, /** * Whether a filter is active. */ isFiltering, /** * The current sorts. */ currentSorts, /** * The current filters. */ currentFilters, /** * Clears the given filter. */ clearFilter, /** * Resets all sorts. */ clearSorts, /** * Resets all filters. */ clearFilters, /** * Applies the given filter. */ applyFilter }; } const isNavigating = vue.ref(false); function useRoute() { const current = vue.ref(core.router.current()); function matches(name, parameters) { return core.router.matches(vue.toValue(name), parameters); } registerHook("before", () => isNavigating.value = true); registerHook("after", () => isNavigating.value = false); registerHook("navigated", () => { current.value = core.router.current(); }); return { isNavigating: vue.readonly(isNavigating), current: vue.readonly(current), matches }; } function useBulkSelect() { const selection = vue.ref({ all: false, only: /* @__PURE__ */ new Set(), except: /* @__PURE__ */ new Set() }); function selectAll() { selection.value.all = true; selection.value.only.clear(); selection.value.except.clear(); } function deselectAll() { selection.value.all = false; selection.value.only.clear(); selection.value.except.clear(); } function select(...records) { records.forEach((record) => selection.value.except.delete(record)); records.forEach((record) => selection.value.only.add(record)); } function deselect(...records) { records.forEach((record) => selection.value.except.add(record)); records.forEach((record) => selection.value.only.delete(record)); } function toggle(record, force) { if (selected(record) || force === false) { return deselect(record); } if (!selected(record) || force === true) { return select(record); } } function selected(record) { if (selection.value.all) { return !selection.value.except.has(record); } return selection.value.only.has(record); } const allSelected = vue.computed(() => { return selection.value.all && selection.value.except.size === 0; }); function bindCheckbox(key) { return { onChange: (event) => { const target = event.target; if (target.checked) { select(target.value); } else { deselect(target.value); } }, checked: selected(key), value: key }; } return { allSelected, selectAll, deselectAll, select, deselect, toggle, selected, selection, bindCheckbox }; } function useQueryParameters() { const state = vue.reactive({}); function updateState() { const params = new URLSearchParams(window.location.search); const unusedKeys = new Set(Object.keys(state)); for (const key of params.keys()) { const paramsForKey = params.getAll(key); state[key] = paramsForKey.length > 1 ? paramsForKey : params.get(key) || ""; unusedKeys.delete(key); } Array.from(unusedKeys).forEach((key) => delete state[key]); } updateState(); core.registerHook("navigated", updateState); return state; } function useQueryParameter(name, options = {}) { const query = useQueryParameters(); const transform = (value2) => { if (options.transform === "bool") { return value2 === true || value2 === "true" || value2 === "1" || value2 === "yes"; } else if (options.transform === "number") { return Number(value2); } else if (options.transform === "string") { return String(value2); } else if (options.transform === "date") { return new Date(value2); } else if (typeof options.transform === "function") { return options.transform(value2); } return value2; }; const value = vue.ref(); vue.watch(query, () => { value.value = transform(query[name] ?? vue.toValue(options.defaultValue)); }, { deep: true, immediate: true }); return value; } function useTable(props, key, defaultOptions = {}) { const table = vue.computed(() => props[key]); const bulk = useBulkSelect(); const refinements = useRefinements(toReactive(table), "refinements", defaultOptions); function getAdditionnalData() { const data = {}; if (defaultOptions?.includeQueryParameters !== false) { Object.assign(data, structuredClone(vue.toRaw(useQueryParameters()))); } if (defaultOptions?.data) { Object.assign(data, defaultOptions.data); } return data; } function getRecordKey(record) { if (typeof record !== "object") { return record; } if (Reflect.has(record, "__hybridId")) { return Reflect.get(record, "__hybridId"); } return Reflect.get(record, table.value.keyName).value; } function getActionName(action) { return typeof action === "string" ? action : action.name; } async function executeInlineAction(action, record) { return await core.router.navigate({ method: "post", url: core.route(table.value.endpoint), preserveState: true, data: { ...getAdditionnalData(), type: "action:inline", action: getActionName(action), tableId: table.value.id, recordId: getRecordKey(record) } }); } async function executeBulkAction(action, options) { const actionName = getActionName(action); const filterParameters = refinements.currentFilters().reduce((carry, filter) => { return { ...carry, [filter.name]: filter.value }; }, {}); return await core.router.navigate({ method: "post", url: core.route(table.value.endpoint), preserveState: true, data: { ...getAdditionnalData(), type: "action:bulk", action: actionName, tableId: table.value.id, all: bulk.selection.value.all, only: [...bulk.selection.value.only], except: [...bulk.selection.value.except], [refinements.filtersKey.value]: filterParameters }, hooks: { after: () => { if (options?.deselect === true || table.value.bulkActions.find(({ name }) => name === actionName)?.deselect !== false) { bulk.deselectAll(); } } } }); } return vue.reactive({ /** Selects all records. */ selectAll: bulk.selectAll, /** Deselects all records. */ deselectAll: bulk.deselectAll, /** Selects records on the current page. */ selectPage: () => bulk.select(...table.value.records.map((record) => getRecordKey(record))), /** Deselects records on the current page. */ deselectPage: () => bulk.deselect(...table.value.records.map((record) => getRecordKey(record))), /** Whether all records on the current page are selected. */ isPageSelected: vue.computed(() => table.value.records.length > 0 && table.value.records.every((record) => bulk.selected(getRecordKey(record)))), /** Checks if the given record is selected. */ isSelected: (record) => bulk.selected(getRecordKey(record)), /** Whether all records are selected. */ allSelected: bulk.allSelected, /** The current record selection. */ selection: bulk.selection, /** Binds a checkbox to its selection state. */ bindCheckbox: (key2) => bulk.bindCheckbox(key2), /** Toggles selection for the given record. */ toggle: (record) => bulk.toggle(getRecordKey(record)), /** Selects selection for the given record. */ select: (record) => bulk.select(getRecordKey(record)), /** Deselects selection for the given record. */ deselect: (record) => bulk.deselect(getRecordKey(record)), /** List of inline actions for this table. */ inlineActions: vue.computed(() => table.value.inlineActions.map((action) => ({ /** Executes the action. */ execute: (record) => executeInlineAction(action.name, record), ...action }))), /** List of bulk actions for this table. */ bulkActions: vue.computed(() => table.value.bulkActions.map((action) => ({ /** Executes the action. */ execute: (options) => executeBulkAction(action.name, options), ...action }))), /** Executes the given inline action for the given record. */ executeInlineAction, /** Executes the given bulk action. */ executeBulkAction, /** List of columns for this table. */ columns: vue.computed(() => table.value.columns.map((column) => ({ ...column, /** Toggles sorting for this column. */ toggleSort: (options) => refinements.toggleSort(column.name, options), /** Checks whether the column is being sorted. */ isSorting: (direction) => refinements.isSorting(column.name, direction), /** Applies the filer for this column. */ applyFilter: (value, options) => refinements.applyFilter(column.name, value, options), /** Clears the filter for this column. */ clearFilter: (options) => refinements.clearFilter(column.name, options), /** Checks whether the column is sortable. */ isSortable: !!refinements.sorts.find((sort) => sort.name === column.name), /** Checks whether the column is filterable. */ isFilterable: !!refinements.filters.find((filters) => filters.name === column.name) }))), /** List of records for this table. */ records: vue.computed(() => table.value.records.map((record) => ({ /** The actual record. */ record: Object.values(record).map((record2) => record2.value), /** The key of the record. Use this instead of `id`. */ key: getRecordKey(record), /** Executes the given inline action. */ execute: (action) => executeInlineAction(getActionName(action), getRecordKey(record)), /** Gets the available inline actions. */ actions: table.value.inlineActions.map((action) => ({ ...action, /** Executes the action. */ execute: () => executeInlineAction(action.name, getRecordKey(record)) })), /** Selects this record. */ select: () => bulk.select(getRecordKey(record)), /** Deselects this record. */ deselect: () => bulk.deselect(getRecordKey(record)), /** Toggles the selection for this record. */ toggle: (force) => bulk.toggle(getRecordKey(record), force), /** Checks whether this record is selected. */ selected: bulk.selected(getRecordKey(record)), /** Gets the value of the record for the specified column. */ value: (column) => record[typeof column === "string" ? column : column.name].value, /** Gets the extra object of the record for the specified column. */ extra: (column, path) => dotDiver.getByPath(record[typeof column === "string" ? column : column.name].extra, path) }))), /** * Paginated meta and links. */ paginator: vue.computed(() => table.value.paginator), ...refinements }); } exports.can = core.can; exports.route = core.route; exports.router = core.router; exports.RouterLink = RouterLink; exports.initializeHybridly = initializeHybridly; exports.registerHook = registerHook; exports.setProperty = setProperty; exports.useBackForward = useBackForward; exports.useBulkSelect = useBulkSelect; exports.useDialog = useDialog; exports.useForm = useForm; exports.useHistoryState = useHistoryState; exports.useProperties = useProperties; exports.useProperty = useProperty; exports.useQueryParameter = useQueryParameter; exports.useQueryParameters = useQueryParameters; exports.useRefinements = useRefinements; exports.useRoute = useRoute; exports.useTable = useTable;