replaced vue3-promise-dialog for internal component

This commit is contained in:
2023-03-24 11:45:12 +10:00
parent ad5b47f2a5
commit 85cfdfd24f
17 changed files with 279 additions and 203 deletions

View File

@@ -95,3 +95,24 @@ code {
}
}
}
/* SM Dialog */
.sm-dialog-outer {
position: fixed;
display: flex;
top: 0;
left: 0;
bottom: 0;
right: 0;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.sm-dialog-outer:last-of-type {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}

View File

@@ -0,0 +1,114 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
AllowedComponentProps,
Component,
defineComponent,
shallowReactive,
VNodeProps,
} from "vue";
export interface DialogInstance {
comp?: any;
dialog: Component;
wrapper: string;
props: unknown;
resolve: (data: unknown) => void;
}
const dialogRefs = shallowReactive<DialogInstance[]>([]);
export default defineComponent({
name: "SMDialogList",
template: `
<div class="sm-dialog-list">
<div v-for="(dialogRef, index) in dialogRefList" :key="index" class="sm-dialog-outer">
<component
:is="dialogRef.dialog"
v-if="dialogRef && dialogRef.wrapper === name"
v-bind="dialogRef.props"
:ref="(ref) => (dialogRef.comp = ref)"></component>
</div>
</div>
`,
data() {
const dialogRefList = dialogRefs;
return {
name: "default",
transitionAttrs: {},
dialogRefList,
};
},
});
/**
* Closes last opened dialog, resolving the promise with the return value of the dialog, or with the given
* data if any.
*
* @param {unknown} data The dialog return value.
*/
export function closeDialog(data?: unknown) {
if (dialogRefs.length <= 1) {
document.getElementsByTagName("html")[0].style.overflow = "";
document.getElementsByTagName("body")[0].style.overflow = "";
}
const lastDialog = dialogRefs.pop();
if (data === undefined) {
data = lastDialog.comp.returnValue();
}
lastDialog.resolve(data);
}
/**
* Extracts the type of props from a component definition.
*/
type PropsType<C extends Component> = C extends new (...args: any) => any
? Omit<
InstanceType<C>["$props"],
keyof VNodeProps | keyof AllowedComponentProps
>
: never;
/**
* Extracts the return type of the dialog from the setup function.
*/
type BindingReturnType<C extends Component> = C extends new (
...args: any
) => any
? InstanceType<C> extends { returnValue: () => infer Y }
? Y
: never
: never;
/**
* Extracts the return type of the dialog either from the setup method or from the methods.
*/
type ReturnType<C extends Component> = BindingReturnType<C>;
/**
* Opens a dialog.
*
* @param {Component} dialog The dialog you want to open.
* @param {PropsType} props The props to be passed to the dialog.
* @param {string} wrapper The dialog wrapper you want the dialog to open into.
* @returns {Promise} A promise that resolves when the dialog is closed
*/
export function openDialog<C extends Component>(
dialog: C,
props?: PropsType<C>,
wrapper: string = "default"
): Promise<ReturnType<C>> {
if (dialogRefs.length === 0) {
document.getElementsByTagName("html")[0].style.overflow = "hidden";
document.getElementsByTagName("body")[0].style.overflow = "hidden";
}
return new Promise((resolve) => {
dialogRefs.push({
dialog,
props,
wrapper,
resolve,
});
});
}

View File

@@ -88,7 +88,7 @@
<script setup lang="ts">
import { computed, inject, ref, useSlots, watch } from "vue";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "./SMDialog";
import { api } from "../helpers/api";
import { MediaResponse } from "../helpers/api.types";
import { imageMedium } from "../helpers/image";

View File

@@ -37,7 +37,7 @@
<script setup lang="ts">
import { ref, Ref, watch } from "vue";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../components/SMDialog";
import { api } from "../helpers/api";
import { Media, MediaResponse } from "../helpers/api.types";
import { bytesReadable } from "../helpers/types";

View File

@@ -1,36 +0,0 @@
<template>
<div class="sm-modal">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
document.getElementsByTagName("body")[0].style.overflow = "hidden";
});
onUnmounted(() => {
document.getElementsByTagName("body")[0].style.overflow = "auto";
});
</script>
<style lang="scss">
.sm-modal {
position: fixed;
display: flex;
top: 0;
left: 0;
bottom: 0;
right: 0;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
z-index: 1000;
padding: 1rem;
}
</style>

View File

@@ -1,32 +1,27 @@
<template>
<SMModal>
<SMFormCard :loading="dialogLoading">
<h1>Change Password</h1>
<p class="text-center">Enter your new password below</p>
<SMForm :model-value="form" @submit="handleSubmit">
<SMInput
control="password"
type="password"
label="New Password" />
<SMFormFooter>
<template #left>
<SMButton
type="secondary"
label="Cancel"
@click="handleClickCancel" />
</template>
<template #right>
<SMButton type="submit" label="Update" />
</template>
</SMFormFooter>
</SMForm>
</SMFormCard>
</SMModal>
<SMFormCard :loading="dialogLoading">
<h1>Change Password</h1>
<p class="text-center">Enter your new password below</p>
<SMForm :model-value="form" @submit="handleSubmit">
<SMInput control="password" type="password" label="New Password" />
<SMFormFooter>
<template #left>
<SMButton
type="secondary"
label="Cancel"
@click="handleClickCancel" />
</template>
<template #right>
<SMButton type="submit" label="Update" />
</template>
</SMFormFooter>
</SMForm>
</SMFormCard>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from "vue";
import { closeDialog } from "vue3-promise-dialog";
import { closeDialog } from "../SMDialog";
import { api } from "../../helpers/api";
import { Form, FormControl, FormObject } from "../../helpers/form";
import { And, Password, Required } from "../../helpers/validate";
@@ -38,7 +33,6 @@ import SMFormCard from "../SMFormCard.vue";
import SMForm from "../SMForm.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMInput from "../SMInput.vue";
import SMModal from "../SMModal.vue";
const form: FormObject = reactive(
Form({

View File

@@ -1,35 +1,32 @@
<template>
<SMModal>
<SMFormCard>
<h1>{{ props.title }}</h1>
<p v-html="computedSanitizedText"></p>
<SMFormFooter>
<template #left>
<SMButton
:type="props.cancel.type"
:label="props.cancel.label"
@click="handleClickCancel()" />
</template>
<template #right>
<SMButton
:type="props.confirm.type"
:label="props.confirm.label"
@click="handleClickConfirm()" />
</template>
</SMFormFooter>
</SMFormCard>
</SMModal>
<SMFormCard>
<h1>{{ props.title }}</h1>
<p v-html="computedSanitizedText"></p>
<SMFormFooter>
<template #left>
<SMButton
:type="props.cancel.type"
:label="props.cancel.label"
@click="handleClickCancel()" />
</template>
<template #right>
<SMButton
:type="props.confirm.type"
:label="props.confirm.label"
@click="handleClickConfirm()" />
</template>
</SMFormFooter>
</SMFormCard>
</template>
<script setup lang="ts">
import DOMPurify from "dompurify";
import { computed, onMounted, onUnmounted } from "vue";
import { closeDialog } from "vue3-promise-dialog";
import { closeDialog } from "../SMDialog";
import { useApplicationStore } from "../../store/ApplicationStore";
import SMButton from "../SMButton.vue";
import SMFormCard from "../SMFormCard.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMModal from "../SMModal.vue";
const props = defineProps({
title: {

View File

@@ -1,108 +1,106 @@
<template>
<SMModal>
<SMFormCard
:loading="dialogLoading"
full
:loading-message="dialogLoadingMessage"
class="sm-dialog-media">
<h1>Insert Media</h1>
<SMMessage
v-if="formMessage"
icon="alert-circle-outline"
type="error"
:message="formMessage"
class="d-flex" />
<div class="media-browser" :class="mediaBrowserClasses">
<div class="media-browser-content">
<SMLoadingIcon v-if="mediaLoading" />
<div
v-if="!mediaLoading && mediaItems.length == 0"
class="media-none">
<ion-icon name="sad-outline"></ion-icon>
<p>No media found</p>
</div>
<ul v-if="!mediaLoading && mediaItems.length > 0">
<li
v-for="item in mediaItems"
:key="item.id"
:class="[{ selected: item.id == selected }]"
@click="handleClickItem(item.id)"
@dblclick="handleDblClickItem(item.id)">
<div
:style="{
backgroundImage: `url('${getFilePreview(
item.url
)}')`,
}"
class="media-image"></div>
<span class="media-title">{{ item.title }}</span>
<span class="media-size">{{
bytesReadable(item.size)
}}</span>
</li>
</ul>
<SMFormCard
:loading="dialogLoading"
full
:loading-message="dialogLoadingMessage"
class="sm-dialog-media">
<h1>Insert Media</h1>
<SMMessage
v-if="formMessage"
icon="alert-circle-outline"
type="error"
:message="formMessage"
class="d-flex" />
<div class="media-browser" :class="mediaBrowserClasses">
<div class="media-browser-content">
<SMLoadingIcon v-if="mediaLoading" />
<div
v-if="!mediaLoading && mediaItems.length == 0"
class="media-none">
<ion-icon name="sad-outline"></ion-icon>
<p>No media found</p>
</div>
<div class="media-browser-toolbar">
<div class="layout-buttons">
<ion-icon
name="grid-outline"
class="layout-button-grid"
@click="handleClickGridLayout"></ion-icon>
<ion-icon
name="list-outline"
class="layout-button-list"
@click="handleClickListLayout"></ion-icon>
</div>
<div class="pagination-buttons">
<ion-icon
name="chevron-back-outline"
:class="[{ disabled: computedDisablePrevButton }]"
@click="handleClickPrev" />
<span class="pagination-info">{{
computedPaginationInfo
<ul v-if="!mediaLoading && mediaItems.length > 0">
<li
v-for="item in mediaItems"
:key="item.id"
:class="[{ selected: item.id == selected }]"
@click="handleClickItem(item.id)"
@dblclick="handleDblClickItem(item.id)">
<div
:style="{
backgroundImage: `url('${getFilePreview(
item.url
)}')`,
}"
class="media-image"></div>
<span class="media-title">{{ item.title }}</span>
<span class="media-size">{{
bytesReadable(item.size)
}}</span>
<ion-icon
name="chevron-forward-outline"
:class="[{ disabled: computedDisableNextButton }]"
@click="handleClickNext" />
</div>
</li>
</ul>
</div>
<div class="media-browser-toolbar">
<div class="layout-buttons">
<ion-icon
name="grid-outline"
class="layout-button-grid"
@click="handleClickGridLayout"></ion-icon>
<ion-icon
name="list-outline"
class="layout-button-list"
@click="handleClickListLayout"></ion-icon>
</div>
<div class="pagination-buttons">
<ion-icon
name="chevron-back-outline"
:class="[{ disabled: computedDisablePrevButton }]"
@click="handleClickPrev" />
<span class="pagination-info">{{
computedPaginationInfo
}}</span>
<ion-icon
name="chevron-forward-outline"
:class="[{ disabled: computedDisableNextButton }]"
@click="handleClickNext" />
</div>
</div>
<SMFormFooter>
<template #left>
<SMButton
type="button"
label="Cancel"
@click="handleClickCancel" />
</template>
<template #right>
<SMButton
v-if="props.allowUpload"
type="button"
label="Upload"
@click="handleClickUpload" />
<SMButton
type="primary"
label="Insert"
:disabled="selected.length == 0"
@click="handleClickInsert" />
</template>
</SMFormFooter>
<input
v-if="props.allowUpload"
id="file"
ref="refUploadInput"
type="file"
style="display: none"
:accept="computedAccepts"
@change="handleChangeUpload" />
</SMFormCard>
</SMModal>
</div>
<SMFormFooter>
<template #left>
<SMButton
type="button"
label="Cancel"
@click="handleClickCancel" />
</template>
<template #right>
<SMButton
v-if="props.allowUpload"
type="button"
label="Upload"
@click="handleClickUpload" />
<SMButton
type="primary"
label="Insert"
:disabled="selected.length == 0"
@click="handleClickInsert" />
</template>
</SMFormFooter>
<input
v-if="props.allowUpload"
id="file"
ref="refUploadInput"
type="file"
style="display: none"
:accept="computedAccepts"
@change="handleChangeUpload" />
</SMFormCard>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, Ref, watch } from "vue";
import { closeDialog } from "vue3-promise-dialog";
import { closeDialog } from "../SMDialog";
import { api } from "../../helpers/api";
import { Media, MediaCollection, MediaResponse } from "../../helpers/api.types";
import { bytesReadable } from "../../helpers/types";
@@ -113,7 +111,6 @@ import SMFormCard from "../SMFormCard.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMLoadingIcon from "../SMLoadingIcon.vue";
import SMMessage from "../SMMessage.vue";
import SMModal from "../SMModal.vue";
const props = defineProps({
mime: {
@@ -356,7 +353,7 @@ const handleClickNext = ($event: MouseEvent): void => {
/**
* When the user clicks the upload button
*/
const handleClickUpload = () => {
const handleClickUpload = async () => {
if (refUploadInput.value != null) {
refUploadInput.value.click();
}

View File

@@ -4,7 +4,6 @@ import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { createApp } from "vue";
import { VueReCaptcha } from "vue-recaptcha-v3";
import { PromiseDialog } from "vue3-promise-dialog";
import "../css/app.scss";
import "./bootstrap";
import SMColumn from "./components/SMColumn.vue";
@@ -20,7 +19,6 @@ pinia.use(piniaPluginPersistedstate);
createApp(App)
.use(pinia)
.use(Router)
.use(PromiseDialog)
.use(VueReCaptcha, {
siteKey: import.meta.env.GOOGLE_RECAPTCHA_SITE_KEY,
loaderOptions: {

View File

@@ -11,6 +11,7 @@
<SMProgress />
<SMToastList />
<DialogWrapper :transition-attrs="{ name: 'fade' }" />
<SMDialogList />
</template>
<script setup lang="ts">
@@ -18,7 +19,7 @@ import SMNavbar from "../components/SMNavbar.vue";
import SMFooter from "../components/SMFooter.vue";
import SMProgress from "../components/SMProgress.vue";
import SMToastList from "../components/SMToastList.vue";
import { DialogWrapper } from "vue3-promise-dialog";
import SMDialogList from "../components/SMDialog";
</script>
<style lang="scss">

View File

@@ -63,7 +63,7 @@
import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../../components/SMDialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMHeading from "../../components/SMHeading.vue";

View File

@@ -59,7 +59,7 @@
import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../../components/SMDialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMFileLink from "../../components/SMFileLink.vue";

View File

@@ -59,7 +59,7 @@
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../../components/SMDialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMHeading from "../../components/SMHeading.vue";

View File

@@ -32,7 +32,7 @@
<script setup lang="ts">
import { computed, reactive } from "vue";
import { useRoute } from "vue-router";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../../components/SMDialog";
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue";

View File

@@ -29,7 +29,7 @@
import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import EasyDataTable from "vue3-easy-data-table";
import { openDialog } from "vue3-promise-dialog";
import { openDialog } from "../../components/SMDialog";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMHeading from "../../components/SMHeading.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";