mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-27 08:10:24 +01:00
wip
This commit is contained in:
parent
3c0a878b1a
commit
5c126d0703
10 changed files with 325 additions and 45 deletions
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -9284,6 +9284,10 @@ export interface Locale extends ILocale {
|
|||
* ここに移動
|
||||
*/
|
||||
"moveToHere": string;
|
||||
/**
|
||||
* このブロックを削除しますか?
|
||||
*/
|
||||
"blockDeleteAreYouSure": string;
|
||||
"blocks": {
|
||||
/**
|
||||
* テキスト
|
||||
|
|
|
@ -2448,6 +2448,7 @@ _pages:
|
|||
specialBlocks: "特殊"
|
||||
inputTitleHere: "タイトルを入力"
|
||||
moveToHere: "ここに移動"
|
||||
blockDeleteAreYouSure: "このブロックを削除しますか?"
|
||||
blocks:
|
||||
text: "テキスト"
|
||||
textarea: "テキストエリア"
|
||||
|
|
|
@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')">
|
||||
<XContainer
|
||||
:draggable="true"
|
||||
:blockId="modelValue.id"
|
||||
@remove="() => emit('remove')"
|
||||
@move="(direction) => emit('move', direction)"
|
||||
>
|
||||
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
|
@ -36,6 +41,7 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
|
||||
(ev: 'remove'): void;
|
||||
(ev: 'move', direction: 'up' | 'down'): void;
|
||||
}>();
|
||||
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
|
|
|
@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')">
|
||||
<XContainer
|
||||
:draggable="true"
|
||||
:blockId="modelValue.id"
|
||||
@remove="() => emit('remove')"
|
||||
@move="(direction) => emit('move', direction)"
|
||||
>
|
||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||
|
||||
<section style="padding: 16px;" class="_gaps_s">
|
||||
|
@ -39,6 +44,8 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
|
||||
(ev: 'remove'): void;
|
||||
(ev: 'move', direction: 'up' | 'down'): void;
|
||||
}>();
|
||||
|
||||
const id = ref(props.modelValue.note);
|
||||
|
|
|
@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')">
|
||||
<XContainer
|
||||
:draggable="true"
|
||||
:blockId="modelValue.id"
|
||||
@remove="() => emit('remove')"
|
||||
@move="(direction) => emit('move', direction)"
|
||||
>
|
||||
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
||||
<template #func>
|
||||
<button class="_button" @click="rename()">
|
||||
|
@ -41,6 +46,7 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
|
||||
(ev: 'remove'): void;
|
||||
(ev: 'move', direction: 'up' | 'down'): void;
|
||||
}>();
|
||||
|
||||
const children = ref(deepClone(props.modelValue.children ?? []));
|
||||
|
|
|
@ -5,18 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||
|
||||
<section>
|
||||
<textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea>
|
||||
</section>
|
||||
<XContainer
|
||||
:draggable="true"
|
||||
:blockId="modelValue.id"
|
||||
@remove="() => emit('remove')"
|
||||
@move="(direction) => emit('move', direction)"
|
||||
>
|
||||
<template #header><i class="ti ti-align-left"></i></template>
|
||||
<template #actions>
|
||||
<button class="_button" :class="$style.previewToggleRoot" @click="toggleEnablePreview">
|
||||
<MkSwitchButton :class="$style.previewToggleSwitch" :checked="enablePreview" @toggle="toggleEnablePreview"></MkSwitchButton>{{ i18n.ts.preview }}
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ focus }">
|
||||
<section>
|
||||
<div v-if="enablePreview" ref="previewEl" :class="$style.previewRoot"><Mfm :text="text"></Mfm></div>
|
||||
<textarea v-else ref="inputEl" v-model="text" :class="$style.textarea" @input.passive="calcTextAreaHeight"></textarea>
|
||||
</section>
|
||||
</template>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||
import { watch, ref, computed, useTemplateRef, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||
|
@ -27,12 +40,37 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
|
||||
(ev: 'remove'): void;
|
||||
(ev: 'move', direction: 'up' | 'down'): void;
|
||||
}>();
|
||||
|
||||
let autocomplete: Autocomplete;
|
||||
|
||||
const inputEl = useTemplateRef('inputEl');
|
||||
const inputHeight = ref(150);
|
||||
const previewEl = useTemplateRef('previewEl');
|
||||
const previewHeight = ref(150);
|
||||
const editorHeight = computed(() => Math.max(inputHeight.value, previewHeight.value));
|
||||
|
||||
function calcTextAreaHeight() {
|
||||
if (!inputEl.value) return;
|
||||
inputEl.value.setAttribute('style', 'min-height: auto');
|
||||
inputHeight.value = Math.max(150, inputEl.value.scrollHeight ?? 0);
|
||||
inputEl.value.removeAttribute('style');
|
||||
}
|
||||
|
||||
const enablePreview = ref(false);
|
||||
function toggleEnablePreview() {
|
||||
enablePreview.value = !enablePreview.value;
|
||||
|
||||
if (enablePreview.value === true) {
|
||||
nextTick(() => {
|
||||
previewHeight.value = Math.max(150, previewEl.value?.scrollHeight ?? 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const text = ref(props.modelValue.text ?? '');
|
||||
const inputEl = shallowRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
watch(text, () => {
|
||||
emit('update:modelValue', {
|
||||
|
@ -42,6 +80,7 @@ watch(text, () => {
|
|||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!inputEl.value) return;
|
||||
autocomplete = new Autocomplete(inputEl.value, text);
|
||||
});
|
||||
|
||||
|
@ -58,7 +97,7 @@ onUnmounted(() => {
|
|||
appearance: none;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 150px;
|
||||
min-height: v-bind("editorHeight + 'px'");
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 16px;
|
||||
|
@ -67,4 +106,19 @@ onUnmounted(() => {
|
|||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.previewRoot {
|
||||
padding: 16px;
|
||||
min-height: v-bind("editorHeight + 'px'");
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.previewToggleRoot {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.previewToggleSwitch {
|
||||
--height: 1.35em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="[$style.insertBetweenRoot, {
|
||||
[$style.insertBetweenDraggingOver]: draggingOverAfterId === '__FIRST__' && draggingBlockId !== modelValue[0]?.id,
|
||||
}]"
|
||||
@click="insertNewBlock('__FIRST__')"
|
||||
@dragover="insertBetweenDragOver($event, '__FIRST__')"
|
||||
@dragleave="insertBetweenDragLeave"
|
||||
@drop="insertBetweenDrop($event, '__FIRST__')"
|
||||
>
|
||||
<div :class="$style.insertBetweenBorder"></div>
|
||||
<span :class="$style.insertBetweenText">{{ i18n.ts._pages.moveToHere }}</span>
|
||||
<span :class="$style.insertBetweenText"><i v-if="!isDragging" class="ti ti-plus"></i> {{ isDragging ? i18n.ts._pages.moveToHere : i18n.ts.add }}</span>
|
||||
</div>
|
||||
|
||||
<div v-for="block, index in modelValue" :key="block.id" :class="$style.item">
|
||||
|
@ -28,18 +29,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:modelValue="block"
|
||||
@update:modelValue="updateItem"
|
||||
@remove="() => removeItem(block)"
|
||||
@move="(direction: 'up' | 'down') => moveItem(block.id, direction)"
|
||||
/>
|
||||
<div
|
||||
:data-after-id="block.id"
|
||||
:class="[$style.insertBetweenRoot, {
|
||||
[$style.insertBetweenDraggingOver]: draggingOverAfterId === block.id && draggingBlockId !== block.id && draggingBlockId !== modelValue[index + 1]?.id,
|
||||
}]"
|
||||
@click="insertNewBlock(block.id)"
|
||||
@dragover="insertBetweenDragOver($event, block.id, modelValue[index + 1]?.id)"
|
||||
@dragleave="insertBetweenDragLeave"
|
||||
@drop="insertBetweenDrop($event, block.id, modelValue[index + 1]?.id)"
|
||||
>
|
||||
<div :class="$style.insertBetweenBorder"></div>
|
||||
<span :class="$style.insertBetweenText">{{ i18n.ts._pages.moveToHere }}</span>
|
||||
<span :class="$style.insertBetweenText"><i v-if="!isDragging" class="ti ti-plus"></i> {{ isDragging ? i18n.ts._pages.moveToHere : i18n.ts.add }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,7 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
import XSection from './els/page-editor.el.section.vue';
|
||||
import XText from './els/page-editor.el.text.vue';
|
||||
import XImage from './els/page-editor.el.image.vue';
|
||||
|
@ -65,6 +72,7 @@ function getComponent(type: string) {
|
|||
}
|
||||
|
||||
const props = defineProps<{
|
||||
scrollContainer?: HTMLElement | null;
|
||||
modelValue: Misskey.entities.Page['content'];
|
||||
}>();
|
||||
|
||||
|
@ -81,9 +89,10 @@ function dragStart(ev: DragEvent) {
|
|||
const blockId = ev.target.dataset.blockId;
|
||||
if (blockId != null) {
|
||||
console.log('dragStart', blockId);
|
||||
ev.dataTransfer!.setData('text/plain', blockId);
|
||||
ev.dataTransfer!.setData('application/x-misskey-pageblock-id', blockId);
|
||||
isDragging.value = true;
|
||||
draggingBlockId.value = blockId;
|
||||
document.addEventListener('dragover', watchForMouseMove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,10 +100,39 @@ function dragStart(ev: DragEvent) {
|
|||
function dragEnd() {
|
||||
isDragging.value = false;
|
||||
draggingBlockId.value = null;
|
||||
document.removeEventListener('dragover', watchForMouseMove);
|
||||
}
|
||||
|
||||
function watchForMouseMove(ev: DragEvent) {
|
||||
if (isDragging.value) {
|
||||
// 画面上部・下部1/4のときにスクロールする
|
||||
const scrollContainer = getScrollContainer(props.scrollContainer ?? null) ?? document.scrollingElement;
|
||||
if (scrollContainer != null) {
|
||||
const rect = scrollContainer.getBoundingClientRect();
|
||||
const y = ev.clientY - rect.top;
|
||||
const h = rect.height;
|
||||
const scrollSpeed = 30;
|
||||
if (y < h / 4) {
|
||||
const acceralation = Math.max(0, 1 - (y / (h / 4)));
|
||||
scrollContainer.scrollBy({
|
||||
top: -scrollSpeed * acceralation,
|
||||
});
|
||||
} else if (y > (h / 4 * 3)) {
|
||||
const acceralation = Math.max(0, 1 - ((h - y) / (h / 4)));
|
||||
scrollContainer.scrollBy({
|
||||
top: scrollSpeed * acceralation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertBetweenDragOver(ev: DragEvent, id: string, nextId?: string) {
|
||||
if (draggingBlockId.value === id || draggingBlockId.value === nextId) return;
|
||||
if (
|
||||
draggingBlockId.value === id ||
|
||||
draggingBlockId.value === nextId ||
|
||||
![...(ev.dataTransfer?.types ?? [])].includes('application/x-misskey-pageblock-id')
|
||||
) return;
|
||||
|
||||
ev.preventDefault();
|
||||
if (ev.target instanceof HTMLElement) {
|
||||
|
@ -110,12 +148,16 @@ function insertBetweenDragLeave() {
|
|||
}
|
||||
|
||||
function insertBetweenDrop(ev: DragEvent, id: string, nextId?: string) {
|
||||
if (draggingBlockId.value === id || draggingBlockId.value === nextId) return;
|
||||
if (
|
||||
draggingBlockId.value === id ||
|
||||
draggingBlockId.value === nextId ||
|
||||
![...(ev.dataTransfer?.types ?? [])].includes('application/x-misskey-pageblock-id')
|
||||
) return;
|
||||
|
||||
ev.preventDefault();
|
||||
if (ev.target instanceof HTMLElement) {
|
||||
const afterId = ev.target.dataset.afterId; // insert after this
|
||||
const moveId = ev.dataTransfer?.getData('text/plain');
|
||||
const moveId = ev.dataTransfer?.getData('application/x-misskey-pageblock-id');
|
||||
if (afterId != null && moveId != null) {
|
||||
const oldValue = props.modelValue.filter((x) => x.id !== moveId);
|
||||
const afterIdAt = afterId === '__FIRST__' ? 0 : oldValue.findIndex((x) => x.id === afterId);
|
||||
|
@ -140,6 +182,49 @@ function insertBetweenDrop(ev: DragEvent, id: string, nextId?: string) {
|
|||
draggingOverAfterId.value = null;
|
||||
}
|
||||
|
||||
async function insertNewBlock(id: string) {
|
||||
const { canceled, result: type } = await os.select({
|
||||
title: i18n.ts._pages.chooseBlock,
|
||||
items: getPageBlockList(),
|
||||
});
|
||||
if (canceled || type == null) return;
|
||||
|
||||
const blockId = uuid();
|
||||
|
||||
let newValue: Misskey.entities.Page['content'];
|
||||
|
||||
if (id === '__FIRST__') {
|
||||
newValue = [
|
||||
{ id: blockId, type },
|
||||
...props.modelValue,
|
||||
];
|
||||
} else {
|
||||
const afterIdAt = props.modelValue.findIndex((x) => x.id === id);
|
||||
newValue = [
|
||||
...props.modelValue.slice(0, afterIdAt + 1),
|
||||
{ id: blockId, type },
|
||||
...props.modelValue.slice(afterIdAt + 1),
|
||||
];
|
||||
}
|
||||
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
|
||||
function moveItem(id: string, direction: 'up' | 'down') {
|
||||
const i = props.modelValue.findIndex(x => x.id === id);
|
||||
if (i === -1) return;
|
||||
|
||||
const newValue = [...props.modelValue];
|
||||
const [removed] = newValue.splice(i, 1);
|
||||
if (direction === 'up') {
|
||||
newValue.splice(i - 1, 0, removed);
|
||||
} else {
|
||||
newValue.splice(i + 1, 0, removed);
|
||||
}
|
||||
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
|
||||
function updateItem(v: Misskey.entities.PageBlock) {
|
||||
const i = props.modelValue.findIndex(x => x.id === v.id);
|
||||
const newValue = [
|
||||
|
@ -164,8 +249,20 @@ function removeItem(v: Misskey.entities.PageBlock) {
|
|||
.insertBetweenRoot {
|
||||
height: calc(var(--MI-margin) * 2);
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.insertBetweenBorder {
|
||||
display: block;
|
||||
}
|
||||
.insertBetweenText {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.insertBetweenBorder {
|
||||
|
@ -187,6 +284,7 @@ function removeItem(v: Misskey.entities.PageBlock) {
|
|||
transform: translate(-50%, -50%);
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
border-radius: 999px;
|
||||
display: none;
|
||||
|
|
|
@ -5,41 +5,63 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRootEl"
|
||||
:class="[$style.blockContainerRoot, {
|
||||
[$style.dragging]: isDragging,
|
||||
[$style.draggingOver]: isDraggingOver,
|
||||
}]"
|
||||
@focus.capture="toggleFocus"
|
||||
@blur.capture="toggleFocus"
|
||||
@dragover="dragOver"
|
||||
@dragleave="dragLeave"
|
||||
@drop="drop"
|
||||
>
|
||||
<header :class="$style.blockContainerHeader">
|
||||
<header :class="$style.blockContainerHeader" tabindex="1">
|
||||
<div :class="$style.title"><slot name="header"></slot></div>
|
||||
<div :class="$style.buttons">
|
||||
<div v-if="$slots.actions != null"><slot name="actions"></slot></div>
|
||||
<button v-if="removable" :class="$style.blockContainerActionButton" class="_button" @click="remove()">
|
||||
<i class="ti ti-trash"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="draggable"
|
||||
draggable="true"
|
||||
:class="$style.blockContainerActionButton"
|
||||
class="_button"
|
||||
:data-block-id="blockId"
|
||||
@dragstart="dragStart"
|
||||
@dragend="dragEnd"
|
||||
>
|
||||
<i class="ti ti-menu-2"></i>
|
||||
</button>
|
||||
<template v-if="draggable">
|
||||
<div :class="$style.divider"></div>
|
||||
<button
|
||||
:class="$style.blockContainerActionButton"
|
||||
class="_button"
|
||||
@click="() => emit('move', 'up')"
|
||||
>
|
||||
<i class="ti ti-arrow-up"></i>
|
||||
</button>
|
||||
<button
|
||||
:class="$style.blockContainerActionButton"
|
||||
class="_button"
|
||||
@click="() => emit('move', 'down')"
|
||||
>
|
||||
<i class="ti ti-arrow-down"></i>
|
||||
</button>
|
||||
<button
|
||||
draggable="true"
|
||||
:class="$style.blockContainerActionButton"
|
||||
class="_button"
|
||||
:data-block-id="blockId"
|
||||
@dragstart="dragStart"
|
||||
@dragend="dragEnd"
|
||||
>
|
||||
<i class="ti ti-menu-2"></i>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<div :class="$style.blockContainerBody" tabindex="0">
|
||||
<slot></slot>
|
||||
<slot :focus="focus"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
blockId: string;
|
||||
|
@ -53,15 +75,28 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'remove'): void;
|
||||
(ev: 'move', direction: 'up' | 'down'): void;
|
||||
}>();
|
||||
|
||||
function remove() {
|
||||
async function remove() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._pages.blockDeleteAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
emit('remove');
|
||||
}
|
||||
|
||||
const containerRootEl = useTemplateRef('containerRootEl');
|
||||
const focus = ref(false);
|
||||
function toggleFocus() {
|
||||
focus.value = containerRootEl.value?.contains(document.activeElement) ?? false;
|
||||
}
|
||||
|
||||
const isDragging = ref(false);
|
||||
function dragStart(ev: DragEvent) {
|
||||
ev.dataTransfer?.setData('text/plain', props.blockId);
|
||||
ev.dataTransfer?.setData('application/x-misskey-pageblock-id', props.blockId);
|
||||
isDragging.value = true;
|
||||
}
|
||||
function dragEnd() {
|
||||
|
@ -98,10 +133,10 @@ function drop() {
|
|||
transform: translateY(-100%);
|
||||
z-index: 1;
|
||||
display: none;
|
||||
gap: var(--MI-margin);
|
||||
gap: 8px;
|
||||
|
||||
height: 42px;
|
||||
padding: 6px 14px;
|
||||
padding: 6px 8px;
|
||||
background-color: var(--MI_THEME-panel);
|
||||
border: 2px solid var(--MI_THEME-accent);
|
||||
border-bottom: none;
|
||||
|
@ -109,11 +144,21 @@ function drop() {
|
|||
|
||||
> .title {
|
||||
line-height: 26px;
|
||||
padding-left: 2px;
|
||||
padding-right: 8px;
|
||||
border-right: 0.5px solid var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
> .divider {
|
||||
width: 0.5px;
|
||||
height: 26px;
|
||||
background-color: var(--MI_THEME-divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<MkStickyContainer ref="containerEl">
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="fetchStatus === 'loading'">
|
||||
|
@ -31,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.pageContent">
|
||||
<XBlocks v-model="content"/>
|
||||
<div>
|
||||
<XPage v-if="enableGlobalPreview" key="preview" :page="page" />
|
||||
<XBlocks v-else key="editor" v-model="content" :scrollContainer="containerEl?.rootEl"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="fetchStatus === 'notMe'" class="_fullInfo">
|
||||
|
@ -41,10 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<div class="_buttons" :class="$style.footerInner">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
|
||||
<MkButton v-if="initPageId != null" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
<div :class="$style.footerInner">
|
||||
<div :class="$style.footerActionSwitchWrapper">
|
||||
<MkSwitch v-model="enableGlobalPreview">{{ i18n.ts.preview }}</MkSwitch>
|
||||
</div>
|
||||
<div :class="$style.footerActionButtons" class="_buttons">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -52,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, watch, ref } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XBlocks from './page-editor.blocks.vue';
|
||||
|
@ -61,6 +65,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
import XPage from '@/components/page/page.vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
@ -70,6 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { signinRequired } from '@/account.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
import type { SlimPage } from '@/types/page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
|
@ -78,7 +84,7 @@ const props = defineProps<{
|
|||
const $i = signinRequired();
|
||||
|
||||
const fetchStatus = ref<'loading' | 'done' | 'notMe'>('loading');
|
||||
const page = ref<Partial<Misskey.entities.Page> | null>(null);
|
||||
const page = ref<Partial<SlimPage> | null>(null);
|
||||
const title = computed({
|
||||
get: () => page.value?.title ?? '',
|
||||
set: (value) => {
|
||||
|
@ -104,6 +110,10 @@ const content = computed<Misskey.entities.Page['content']>({
|
|||
},
|
||||
});
|
||||
|
||||
const enableGlobalPreview = ref(false);
|
||||
|
||||
const containerEl = useTemplateRef('containerEl');
|
||||
|
||||
function onTitleUpdated(ev: Event) {
|
||||
title.value = (ev.target as HTMLDivElement).innerText;
|
||||
}
|
||||
|
@ -246,6 +256,24 @@ definePageMetadata(() => ({
|
|||
}
|
||||
}
|
||||
|
||||
.editorMenu {
|
||||
position: sticky;
|
||||
top: var(--MI-stickyTop, 0px);
|
||||
left: 0;
|
||||
width: calc(100% + 4rem);
|
||||
margin: 0 -2rem 0;
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
border-bottom: solid .5px var(--MI_THEME-divider);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.editorMenuInner {
|
||||
padding: 16px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
|
@ -256,5 +284,18 @@ definePageMetadata(() => ({
|
|||
padding: 16px;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footerActionSwitchWrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footerActionButtons {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
18
packages/frontend/src/types/page.ts
Normal file
18
packages/frontend/src/types/page.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export type SlimPage = Pick<Misskey.entities.Page,
|
||||
'alignCenter' |
|
||||
'attachedFiles' |
|
||||
'content' |
|
||||
'eyeCatchingImage' |
|
||||
'eyeCatchingImageId' |
|
||||
'font' |
|
||||
'title' |
|
||||
'user' |
|
||||
'userId'
|
||||
>;
|
Loading…
Reference in a new issue