2024-09-09 06:57:36 -05:00
|
|
|
|
<!--
|
|
|
|
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<MkModalWindow
|
|
|
|
|
ref="dialogEl"
|
|
|
|
|
:width="1000"
|
|
|
|
|
:height="600"
|
|
|
|
|
:scroll="false"
|
|
|
|
|
:withOkButton="false"
|
|
|
|
|
@close="cancel()"
|
|
|
|
|
@closed="$emit('closed')"
|
|
|
|
|
>
|
2024-09-10 02:14:02 -05:00
|
|
|
|
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
2024-09-09 06:57:36 -05:00
|
|
|
|
|
|
|
|
|
<div :class="$style.embedCodeGenRoot">
|
|
|
|
|
<Transition
|
|
|
|
|
mode="out-in"
|
|
|
|
|
:enterActiveClass="$style.transition_x_enterActive"
|
|
|
|
|
:leaveActiveClass="$style.transition_x_leaveActive"
|
|
|
|
|
:enterFromClass="$style.transition_x_enterFrom"
|
|
|
|
|
:leaveToClass="$style.transition_x_leaveTo"
|
|
|
|
|
>
|
|
|
|
|
<div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
|
|
|
|
|
<div
|
|
|
|
|
:class="$style.embedCodeGenPreviewRoot"
|
|
|
|
|
>
|
|
|
|
|
<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
|
|
|
|
|
<div :class="$style.embedCodeGenPreviewWrapper">
|
|
|
|
|
<div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
|
|
|
|
|
<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert>
|
|
|
|
|
<div
|
|
|
|
|
:class="$style.embedCodeGenPreviewResizer"
|
|
|
|
|
:style="{ transform: iframeStyle }"
|
|
|
|
|
>
|
|
|
|
|
<iframe
|
|
|
|
|
ref="iframeEl"
|
|
|
|
|
:src="embedPreviewUrl"
|
|
|
|
|
:class="$style.embedCodeGenPreviewIframe"
|
|
|
|
|
:style="{ height: `${iframeHeight}px` }"
|
|
|
|
|
@load="iframeOnLoad"
|
|
|
|
|
></iframe>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div :class="$style.embedCodeGenSettings" class="_gaps">
|
|
|
|
|
<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
|
|
|
|
|
<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
|
|
|
|
|
<template #suffix>px</template>
|
|
|
|
|
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
|
|
|
|
</MkInput>
|
|
|
|
|
<MkSelect v-model="colorMode">
|
|
|
|
|
<template #label>{{ i18n.ts.theme }}</template>
|
|
|
|
|
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
|
|
|
|
|
<option value="light">{{ i18n.ts.light }}</option>
|
|
|
|
|
<option value="dark">{{ i18n.ts.dark }}</option>
|
|
|
|
|
</MkSelect>
|
|
|
|
|
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
|
|
|
|
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
|
|
|
|
<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
|
|
|
|
|
<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
|
|
|
|
|
<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
|
|
|
|
|
<div class="_buttons">
|
|
|
|
|
<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
|
|
|
|
|
<MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
|
|
|
|
|
<div :class="$style.embedCodeGenResultWrapper" class="_gaps">
|
|
|
|
|
<div class="_gaps_s">
|
|
|
|
|
<div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
|
|
|
|
|
<div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
|
|
|
|
|
<div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="_gaps_s">
|
|
|
|
|
<MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
|
|
|
|
|
<MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
|
|
|
|
</div>
|
|
|
|
|
<MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
</MkModalWindow>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
|
2024-09-23 00:51:34 -05:00
|
|
|
|
import { url } from '@@/js/config.js';
|
|
|
|
|
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
|
2024-09-09 06:57:36 -05:00
|
|
|
|
import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
|
|
|
|
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
|
|
|
|
|
|
|
|
|
import MkInput from '@/components/MkInput.vue';
|
|
|
|
|
import MkSelect from '@/components/MkSelect.vue';
|
|
|
|
|
import MkSwitch from '@/components/MkSwitch.vue';
|
|
|
|
|
import MkButton from '@/components/MkButton.vue';
|
|
|
|
|
|
|
|
|
|
import MkCode from '@/components/MkCode.vue';
|
|
|
|
|
import MkInfo from '@/components/MkInfo.vue';
|
|
|
|
|
|
|
|
|
|
import * as os from '@/os.js';
|
|
|
|
|
import { i18n } from '@/i18n.js';
|
|
|
|
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
|
|
|
|
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
(ev: 'ok'): void;
|
|
|
|
|
(ev: 'cancel'): void;
|
|
|
|
|
(ev: 'closed'): void;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
entity: EmbeddableEntity;
|
|
|
|
|
id: string;
|
|
|
|
|
params?: EmbedParams;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
//#region Modalの制御
|
|
|
|
|
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
|
|
|
|
|
|
|
|
|
function cancel() {
|
|
|
|
|
emit('cancel');
|
|
|
|
|
dialogEl.value?.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
|
dialogEl.value?.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const phase = ref<'input' | 'result'>('input');
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region 埋め込みURL生成・カスタマイズ
|
|
|
|
|
|
|
|
|
|
// 本URL生成用params
|
|
|
|
|
const paramsForUrl = computed<EmbedParams>(() => ({
|
|
|
|
|
header: header.value,
|
|
|
|
|
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
|
|
|
|
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
|
|
|
|
rounded: rounded.value,
|
|
|
|
|
border: border.value,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// プレビュー用params(手動で更新を掛けるのでref)
|
|
|
|
|
const paramsForPreview = ref<EmbedParams>(props.params ?? {});
|
|
|
|
|
|
|
|
|
|
const embedPreviewUrl = computed(() => {
|
|
|
|
|
const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
|
|
|
|
|
if (paramClass.has('maxHeight')) {
|
|
|
|
|
const maxHeight = parseInt(paramClass.get('maxHeight')!);
|
|
|
|
|
paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
|
|
|
|
|
}
|
|
|
|
|
return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
|
|
|
|
|
const header = ref(props.params?.header ?? true);
|
|
|
|
|
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
|
|
|
|
|
|
|
|
|
|
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
|
|
|
|
const rounded = ref(props.params?.rounded ?? true);
|
|
|
|
|
const border = ref(props.params?.border ?? true);
|
|
|
|
|
|
|
|
|
|
function applyToPreview() {
|
|
|
|
|
const currentPreviewUrl = embedPreviewUrl.value;
|
|
|
|
|
|
|
|
|
|
paramsForPreview.value = {
|
|
|
|
|
header: header.value,
|
|
|
|
|
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
|
|
|
|
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
|
|
|
|
rounded: rounded.value,
|
|
|
|
|
border: border.value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
if (currentPreviewUrl === embedPreviewUrl.value) {
|
|
|
|
|
// URLが変わらなくてもリロード
|
|
|
|
|
iframeEl.value?.contentWindow?.location.reload();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = ref('');
|
|
|
|
|
|
|
|
|
|
function generate() {
|
|
|
|
|
result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value);
|
|
|
|
|
phase.value = 'result';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doCopy() {
|
|
|
|
|
copyToClipboard(result.value);
|
|
|
|
|
os.success();
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region プレビューのリサイズ
|
|
|
|
|
const resizerRootEl = shallowRef<HTMLDivElement>();
|
|
|
|
|
const iframeLoading = ref(true);
|
|
|
|
|
const iframeEl = shallowRef<HTMLIFrameElement>();
|
|
|
|
|
const iframeHeight = ref(0);
|
|
|
|
|
const iframeScale = ref(1);
|
|
|
|
|
const iframeStyle = computed(() => {
|
|
|
|
|
return `translate(-50%, -50%) scale(${iframeScale.value})`;
|
|
|
|
|
});
|
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
calcScale();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function iframeOnLoad() {
|
|
|
|
|
iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
|
|
|
|
|
iframeLoading.value = true;
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
iframeHeight.value = 0;
|
|
|
|
|
iframeScale.value = 1;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function windowEventHandler(event: MessageEvent) {
|
|
|
|
|
if (event.source !== iframeEl.value?.contentWindow) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (event.data.type === 'misskey:embed:ready') {
|
|
|
|
|
iframeEl.value!.contentWindow?.postMessage({
|
|
|
|
|
type: 'misskey:embedParent:registerIframeId',
|
|
|
|
|
payload: {
|
|
|
|
|
iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (event.data.type === 'misskey:embed:changeHeight') {
|
|
|
|
|
iframeHeight.value = event.data.payload.height;
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
calcScale();
|
|
|
|
|
iframeLoading.value = false; // 初回の高さ変更まで待つ
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calcScale() {
|
|
|
|
|
if (!resizerRootEl.value) return;
|
|
|
|
|
const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
|
|
|
|
|
const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
|
|
|
|
|
const iframeWidth = 500;
|
|
|
|
|
const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に
|
|
|
|
|
iframeScale.value = scale;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
window.addEventListener('message', windowEventHandler);
|
|
|
|
|
if (!resizerRootEl.value) return;
|
|
|
|
|
resizeObserver.observe(resizerRootEl.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
|
window.removeEventListener('message', windowEventHandler);
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
|
|
|
|
|
// プレビューのリセット
|
|
|
|
|
iframeHeight.value = 0;
|
|
|
|
|
iframeScale.value = 1;
|
|
|
|
|
iframeLoading.value = true;
|
|
|
|
|
result.value = '';
|
|
|
|
|
phase.value = 'input';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDeactivated(() => {
|
|
|
|
|
reset();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
reset();
|
|
|
|
|
});
|
|
|
|
|
//#endregion
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style module>
|
|
|
|
|
.transition_x_enterActive,
|
|
|
|
|
.transition_x_leaveActive {
|
|
|
|
|
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
|
|
|
|
}
|
|
|
|
|
.transition_x_enterFrom {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(50px);
|
|
|
|
|
}
|
|
|
|
|
.transition_x_leaveTo {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-50px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenRoot {
|
|
|
|
|
container-type: inline-size;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenInputRoot {
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 400px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewRoot {
|
|
|
|
|
position: relative;
|
2024-10-09 04:08:14 -05:00
|
|
|
|
background-color: var(--MI_THEME-bg);
|
2024-09-23 00:51:34 -05:00
|
|
|
|
background-size: auto auto;
|
2024-10-09 04:08:14 -05:00
|
|
|
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
2024-09-09 06:57:36 -05:00
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewWrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
user-select: none;
|
|
|
|
|
-webkit-user-drag: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewTitle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
top: 8px;
|
|
|
|
|
left: 8px;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 85%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewSpinner {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
user-select: none;
|
|
|
|
|
-webkit-user-drag: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewResizerRoot {
|
|
|
|
|
position: relative;
|
|
|
|
|
flex: 1 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewResizer {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenPreviewIframe {
|
|
|
|
|
display: block;
|
|
|
|
|
border: none;
|
|
|
|
|
width: 500px;
|
|
|
|
|
color-scheme: light dark;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenSettings {
|
|
|
|
|
padding: 24px;
|
|
|
|
|
overflow-y: scroll;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultRoot {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
max-width: 700px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultHeading {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 1.2em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultHeadingIcon {
|
|
|
|
|
margin: 0 auto;
|
2024-10-09 04:08:14 -05:00
|
|
|
|
background-color: var(--MI_THEME-accentedBg);
|
|
|
|
|
color: var(--MI_THEME-accent);
|
2024-09-09 06:57:36 -05:00
|
|
|
|
text-align: center;
|
|
|
|
|
height: 64px;
|
|
|
|
|
width: 64px;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
line-height: 64px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultDescription {
|
|
|
|
|
text-align: center;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultWrapper,
|
|
|
|
|
.embedCodeGenResultCode {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.embedCodeGenResultButtons {
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@container (max-width: 800px) {
|
|
|
|
|
.embedCodeGenInputRoot {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
grid-template-rows: 1fr 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|