misskey/packages/frontend/src/components/MkWidgetSettingsDialog.vue

166 lines
3.7 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="1000"
:height="600"
:scroll="false"
:withOkButton="true"
@close="cancel()"
@ok="save()"
@closed="emit('closed')"
>
<template #header><i class="ti ti-icons"></i> {{ i18n.ts._widgets[widgetName] ?? widgetName }}</template>
<MkPreviewWithControls>
<template #preview>
<div :class="$style.previewWrapper">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div ref="resizerRootEl" :class="$style.previewResizerRoot" inert>
<div
ref="resizerEl"
:class="$style.previewResizer"
:style="{ transform: widgetStyle }"
>
<component
:is="`widget-${widgetName}`"
:widget="{ name: widgetName, id: '__PREVIEW__', data: settings }"
></component>
</div>
</div>
</div>
</template>
<template #controls>
<div class="_spacer">
<MkForm v-model="settings" :form="form"/>
</div>
</template>
</MkPreviewWithControls>
</MkModalWindow>
</template>
<script setup lang="ts">
import { reactive, useTemplateRef, ref, computed, watch, onBeforeUnmount, onMounted } from 'vue';
import MkPreviewWithControls from './MkPreviewWithControls.vue';
import type { Form } from '@/utility/form.js';
import { deepClone } from '@/utility/clone.js';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkForm from '@/components/MkForm.vue';
const props = defineProps<{
widgetName: string;
form: Form;
currentSettings: Record<string, any>;
}>();
const emit = defineEmits<{
(ev: 'saved', settings: Record<string, any>): void;
(ev: 'canceled'): void;
(ev: 'closed'): void;
}>();
const dialog = useTemplateRef('dialog');
const settings = ref<Record<string, any>>(deepClone(props.currentSettings));
function save() {
emit('saved', deepClone(settings.value));
dialog.value?.close();
}
function cancel() {
emit('canceled');
dialog.value?.close();
}
//#region プレビューのリサイズ
const resizerRootEl = useTemplateRef('resizerRootEl');
const resizerEl = useTemplateRef('resizerEl');
const widgetHeight = ref(0);
const widgetScale = ref(1);
const widgetStyle = computed(() => {
return `translate(-50%, -50%) scale(${widgetScale.value})`;
});
const ro1 = new ResizeObserver(() => {
widgetHeight.value = resizerEl.value!.clientHeight;
calcScale();
});
const ro2 = new ResizeObserver(() => {
calcScale();
});
function calcScale() {
if (!resizerRootEl.value) return;
const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
const widgetWidth = 280;
const scale = Math.min(previewWidth / widgetWidth, previewHeight / widgetHeight.value, 1); // 拡大はしないので1を上限に
widgetScale.value = scale;
}
onMounted(() => {
if (resizerEl.value) {
ro1.observe(resizerEl.value);
}
if (resizerRootEl.value) {
ro2.observe(resizerRootEl.value);
}
calcScale();
});
onBeforeUnmount(() => {
ro1.disconnect();
ro2.disconnect();
});
//#endregion
</script>
<style module>
.previewContainer {
display: flex;
flex-direction: column;
height: 100%;
user-select: none;
-webkit-user-drag: none;
}
.previewTitle {
position: absolute;
z-index: 100;
top: 8px;
left: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 85%;
}
.previewWrapper {
display: flex;
flex-direction: column;
height: 100%;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
.previewResizerRoot {
position: relative;
flex: 1 0;
}
.previewResizer {
position: absolute;
container-type: inline-size;
top: 50%;
left: 50%;
width: 280px;
}
</style>