diff --git a/client/components/open/forms/OpenCompleteForm.vue b/client/components/open/forms/OpenCompleteForm.vue index 4300d63a..efecd633 100644 --- a/client/components/open/forms/OpenCompleteForm.vue +++ b/client/components/open/forms/OpenCompleteForm.vue @@ -69,6 +69,7 @@ :loading="loading" :fields="form.properties" :theme="theme" + :dark-mode="darkMode" :admin-preview="adminPreview" @submit="submitForm" > @@ -127,7 +128,11 @@ export default { form: { type: Object, required: true }, creating: { type: Boolean, default: false }, // If true, fake form submit adminPreview: { type: Boolean, default: false }, // If used in FormEditorPreview - submitButtonClass: { type: String, default: '' } + submitButtonClass: { type: String, default: '' }, + darkMode: { + type: Boolean, + default: false + } }, setup(props) { diff --git a/client/components/open/forms/OpenForm.vue b/client/components/open/forms/OpenForm.vue index 016ea34c..f96e635a 100644 --- a/client/components/open/forms/OpenForm.vue +++ b/client/components/open/forms/OpenForm.vue @@ -41,6 +41,7 @@ :data-form="dataForm" :data-form-value="dataFormValue" :theme="theme" + :dark-mode="darkMode" :admin-preview="adminPreview" /> @@ -51,7 +52,7 @@ @@ -85,7 +86,6 @@ import VueHcaptcha from "@hcaptcha/vue3-hcaptcha" import OpenFormField from './OpenFormField.vue' import {pendingSubmission} from "~/composables/forms/pendingSubmission.js" import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js" -import {darkModeEnabled} from "~/lib/forms/public-page.js" export default { name: 'OpenForm', @@ -112,7 +112,11 @@ export default { required: true }, defaultDataForm:{}, - adminPreview: { type: Boolean, default: false } // If used in FormEditorPreview + adminPreview: { type: Boolean, default: false }, // If used in FormEditorPreview + darkMode: { + type: Boolean, + default: false + } }, setup (props) { @@ -124,7 +128,6 @@ export default { dataForm, recordsStore, workingFormStore, - darkModeEnabled: darkModeEnabled(), pendingSubmission: pendingSubmission(props.form) } }, diff --git a/client/components/open/forms/OpenFormField.vue b/client/components/open/forms/OpenFormField.vue index bca19e47..9edb6ff2 100644 --- a/client/components/open/forms/OpenFormField.vue +++ b/client/components/open/forms/OpenFormField.vue @@ -56,7 +56,6 @@ + + diff --git a/client/lib/forms/public-page.js b/client/lib/forms/public-page.js index 93932467..9e6af7f1 100644 --- a/client/lib/forms/public-page.js +++ b/client/lib/forms/public-page.js @@ -1,30 +1,121 @@ - let darkModeNodeParent = import.meta.client ? document.body : null /** * Handle form public pages dark mode and transparent mode */ export function handleDarkMode (darkMode, elem = null) { - if (import.meta.server) return + if (import.meta.server) + return darkModeNodeParent = elem ?? document.body // Dark mode - if (['dark', 'light'].includes(darkMode)) { + if (['dark', 'light'].includes(darkMode)) return handleDarkModeToggle(darkMode === 'dark') - } // Case auto - handleDarkModeToggle(window.matchMedia('(prefers-color-scheme: dark)').matches) + handleDarkModeToggle( + window.matchMedia('(prefers-color-scheme: dark)').matches + ) // Create listener - window.matchMedia('(prefers-color-scheme: dark)') + window + .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', handleDarkModeToggle) } -export function darkModeEnabled() { - if (import.meta.server) return false - return computed(() => document.body.classList.contains('dark')) +export function useClassWatcher (elem, className) { + const hasClass = ref(false) + + const updateClassPresence = () => { + hasClass.value = elem.value?.classList.contains(className) ?? false + } + + let observer = null + + const startObserving = () => { + if (elem.value) { + updateClassPresence() + observer = new MutationObserver(updateClassPresence) + observer.observe(elem.value, { attributes: true, attributeFilter: ['class'] }) + } + } + + const stopObserving = () => { + if (observer) { + observer.disconnect() + observer = null + } + } + + onMounted(() => { + watch(elem, (newElem, oldElem, onCleanup) => { + stopObserving() + startObserving() + onCleanup(stopObserving) + }, { immediate: true }) + }) + + onUnmounted(() => { + stopObserving() + }) + + return computed(() => hasClass.value) +} + +export function useDarkMode (elem = ref(null)) { + // Define a computed property to handle the element reference reactively + const effectiveElem = computed(() => { + return elem.value || (process.client ? document.body : null) + }) + + // Pass the computed property to useClassWatcher + return useClassWatcher(effectiveElem, 'dark') +} + +export function darkModeEnabled (elem = ref(null)) { + if (import.meta.server) + return ref(false) + + // TODO: replace this with a function that returns a watcher for the given of dark class element + // Simplify this + const isDark = ref(false) + + // Update isDark based on the current class + const updateIsDark = () => { + console.log(elem.value, 'test') + const finalElement = elem.value ?? document.body + isDark.value = finalElement.classList.contains('dark') + } + + // MutationObserver callback to react to class changes + let observer = new MutationObserver(updateIsDark) + + // Initialize and clean up the observer + const initObserver = (element) => { + if (observer) { + observer.disconnect() + } + if (element) { + observer = new MutationObserver(updateIsDark) + observer.observe(element, { attributes: true, attributeFilter: ['class'] }) + } + } + + onMounted(() => { + if (!import.meta.server) { + initObserver(elem) + } + }) + + onUnmounted(() => { + if (observer) { + observer.disconnect() + } + }) + + // Return a computed ref that depends on isDark + return computed(() => isDark.value) } function handleDarkModeToggle (enabled) { @@ -32,20 +123,27 @@ function handleDarkModeToggle (enabled) { // if we received an event enabled = enabled.matches } - enabled ? darkModeNodeParent.classList.add('dark') : darkModeNodeParent.classList.remove('dark') + enabled + ? darkModeNodeParent.classList.add('dark') + : darkModeNodeParent.classList.remove('dark') } export function disableDarkMode () { - if (import.meta.server) return + if (import.meta.server) + return const body = document.body body.classList.remove('dark') // Remove event listener - window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handleDarkModeToggle) + window + .matchMedia('(prefers-color-scheme: dark)') + .removeEventListener('change', handleDarkModeToggle) } export function handleTransparentMode (transparentModeEnabled) { - if (import.meta.server) return - if (!useIsIframe() || !transparentModeEnabled) return + if (import.meta.server) + return + if (!useIsIframe() || !transparentModeEnabled) + return const app = document.getElementById('app') app.classList.remove('bg-white') @@ -53,9 +151,12 @@ export function handleTransparentMode (transparentModeEnabled) { app.classList.add('bg-transparent') } -export function focusOnFirstFormElement() { - if (import.meta.server) return - for (const ele of document.querySelectorAll('input,button,textarea,[role="button"]')) { +export function focusOnFirstFormElement () { + if (import.meta.server) + return + for (const ele of document.querySelectorAll( + 'input,button,textarea,[role="button"]' + )) { if (ele.offsetWidth !== 0 || ele.offsetHeight !== 0) { ele.focus() break diff --git a/client/pages/forms/[slug]/index.vue b/client/pages/forms/[slug]/index.vue index 0abff8ad..907b9d4c 100644 --- a/client/pages/forms/[slug]/index.vue +++ b/client/pages/forms/[slug]/index.vue @@ -40,7 +40,7 @@

- @@ -52,13 +52,19 @@ import {computed} from 'vue' import OpenCompleteForm from "~/components/open/forms/OpenCompleteForm.vue" import sha256 from 'js-sha256' -import {onBeforeRouteLeave} from 'vue-router' -import {disableDarkMode, handleDarkMode, handleTransparentMode, focusOnFirstFormElement} from '~/lib/forms/public-page' +import { onBeforeRouteLeave } from 'vue-router' +import { + disableDarkMode, + handleDarkMode, + handleTransparentMode, + focusOnFirstFormElement, + useDarkMode +} from '~/lib/forms/public-page' const crisp = useCrisp() const formsStore = useFormsStore() const recordsStore = useRecordsStore() - +const darkMode = useDarkMode() const isIframe = useIsIframe() const formLoading = computed(() => formsStore.loading) const recordLoading = computed(() => recordsStore.loading)