mirror of https://github.com/requarks/wiki
parent
a857f96bb0
commit
7cf47610d1
@ -0,0 +1,257 @@
|
||||
import { LitElement, html, css } from 'lit'
|
||||
import { deflateRaw } from 'pako'
|
||||
import { DarkMode } from '../shared/theme.js'
|
||||
|
||||
/**
|
||||
* The draw.io that answers when the block names no server.
|
||||
*
|
||||
* Two hosts, because draw.io publishes two deployments of the same application: `viewer` is the one
|
||||
* that opens a diagram read-only from a link, `embed` the one the editor talks to over postMessage
|
||||
* (see `BlockContentDrawio.vue`). A self-hosted draw.io is one deployment doing both, which is why the
|
||||
* `server` prop replaces both and defaults to neither.
|
||||
*/
|
||||
const PUBLIC_VIEWER = 'https://viewer.diagrams.net'
|
||||
|
||||
/** How many bytes are turned into characters at a time, below. */
|
||||
const CHUNK_SIZE = 0x8000
|
||||
|
||||
/**
|
||||
* A drawing as draw.io writes it into a link fragment.
|
||||
*
|
||||
* This is `Graph.compress` from draw.io itself, and every step of it matters to the other end:
|
||||
* percent-encode, raw deflate (no zlib header, unlike Kroki's), then base64. The percent-encoding
|
||||
* comes FIRST and is not a transport detail — draw.io decompresses and then `decodeURIComponent`s, so
|
||||
* a diagram deflated without it comes back mangled at every non-ASCII character.
|
||||
*
|
||||
* `btoa` takes a string, and spreading a whole drawing into `String.fromCharCode` at once overflows
|
||||
* the stack somewhere in the tens of thousands of bytes — hence a chunk at a time, as `block-kroki`
|
||||
* does for the same reason.
|
||||
*/
|
||||
function compressForUrl(xml) {
|
||||
const bytes = deflateRaw(encodeURIComponent(xml))
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE))
|
||||
}
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Draw.io
|
||||
*/
|
||||
export class BlockDrawioElement extends LitElement {
|
||||
/**
|
||||
* Metadata for the admin area and the editor's block picker. Collected at build time into
|
||||
* `compiled/blocks.manifest.json`, which the server reads to register the block. Values must be
|
||||
* plain literals. See `props` in `block-index` for what the picker does with that list.
|
||||
*/
|
||||
static definition = {
|
||||
block: 'drawio',
|
||||
name: 'Draw.io',
|
||||
description: 'A diagram drawn in draw.io, stored as its own XML and edited on a canvas.',
|
||||
icon: 'web-design',
|
||||
/*
|
||||
Fenced, and `xml` because that is what a draw.io document is — so an author reading the page
|
||||
source gets it highlighted, and markdown keeps its hands off it. Without the fence a drawing is
|
||||
a document full of `<` and `_` and lines beginning with spaces, every one of which means
|
||||
something to markdown.
|
||||
*/
|
||||
template: `\`\`\`xml
|
||||
<mxfile>
|
||||
<diagram name="Page-1">
|
||||
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" page="1" pageWidth="850" pageHeight="1100">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
\`\`\``,
|
||||
/*
|
||||
The body is a drawing, so it is edited on a canvas rather than typed: this names the editor the
|
||||
markdown editor offers above the block, as a second lens beside "Edit Block Parameters". The key
|
||||
is resolved to a component by `BlockContentEditorOverlay`; nothing about draw.io reaches the
|
||||
block system itself.
|
||||
*/
|
||||
contentEditor: 'drawio',
|
||||
props: [
|
||||
{
|
||||
name: 'server',
|
||||
type: 'string',
|
||||
label: 'Server',
|
||||
hint: 'A self-hosted draw.io to draw and display with. The public diagrams.net services when left empty.'
|
||||
},
|
||||
{
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
label: 'Height',
|
||||
hint: 'How tall the diagram is, in pixels.',
|
||||
default: 420
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'string',
|
||||
label: 'Caption',
|
||||
hint: 'Shown under the diagram.'
|
||||
},
|
||||
{
|
||||
name: 'align',
|
||||
type: 'select',
|
||||
label: 'Alignment',
|
||||
options: ['left', 'center'],
|
||||
default: 'left'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* -> The gap below the block. On this element rather than :host: see block-index. */
|
||||
.diagram,
|
||||
.error {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.diagram.is-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*
|
||||
The frame carries the border rather than the iframe: draw.io's lightbox paints its own
|
||||
background over the whole box, so a border on the iframe itself is drawn under it at the
|
||||
corners.
|
||||
*/
|
||||
.frame {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
}
|
||||
:host([dark]) .frame {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background-color: #18191a;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #c10015;
|
||||
color: #c10015;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
/** A self-hosted draw.io, standing in for both public services. */
|
||||
server: { type: String },
|
||||
/** How tall the frame is, in pixels. */
|
||||
height: { type: Number },
|
||||
/** Shown under the diagram. */
|
||||
caption: { type: String },
|
||||
/** `left` or `center`. */
|
||||
align: { type: String },
|
||||
|
||||
_source: { state: true }
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.server = ''
|
||||
this.height = 420
|
||||
this.caption = ''
|
||||
this.align = 'left'
|
||||
this._source = ''
|
||||
/*
|
||||
The lightbox reads its theme from the URL at load, so the frame is rebuilt when the reader
|
||||
switches — which for a read-only diagram costs nothing, unlike doing the same to the editor.
|
||||
*/
|
||||
this._darkMode = new DarkMode(this)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback()
|
||||
this._readSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* The drawing, out of the body markdown left behind.
|
||||
*
|
||||
* `textContent` on the `<pre>` is what undoes the escaping the fence went through — the same three
|
||||
* lines every source block in this wiki uses.
|
||||
*/
|
||||
_readSource() {
|
||||
const fence = this.querySelector('pre')
|
||||
this._source = ((fence ?? this).textContent ?? '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* The link the frame opens.
|
||||
*
|
||||
* `lightbox=1` is draw.io's read-only viewer: no editing, no menus, just the drawing with zoom and
|
||||
* a layers control. `edit=_blank` is deliberately absent — the pencil it adds opens the diagram in
|
||||
* a copy of draw.io that has nowhere to save to, and the way to change a drawing here is the page.
|
||||
*/
|
||||
_url() {
|
||||
const server = (this.server || '').trim().replace(/\/+$/, '') || PUBLIC_VIEWER
|
||||
const params = new URLSearchParams({
|
||||
lightbox: '1',
|
||||
nav: '1',
|
||||
ui: this._darkMode.isDark ? 'dark' : 'kennedy'
|
||||
})
|
||||
return `${server}/?${params.toString()}#R${encodeURIComponent(compressForUrl(this._source))}`
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._source) {
|
||||
return html`
|
||||
<div class="error">
|
||||
This diagram is empty. Draw one with the Edit Content link above the block in the editor.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
const height = Number(this.height) > 0 ? Number(this.height) : 420
|
||||
return html`
|
||||
<div class="diagram ${this.align === 'center' ? 'is-center' : ''}">
|
||||
<div class="frame" style="height: ${height}px">
|
||||
<iframe
|
||||
src=${this._url()}
|
||||
title=${this.caption || 'Diagram'}
|
||||
loading="lazy"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
${this.caption ? html`<div class="caption">${this.caption}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('block-drawio', BlockDrawioElement)
|
||||
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="block-content-drawio">
|
||||
<iframe
|
||||
ref="frameEl"
|
||||
class="block-content-drawio-frame"
|
||||
:src="editorUrl"
|
||||
:title="t('editor.blockContent.drawioTitle')" />
|
||||
<!--
|
||||
Covers the frame until draw.io says hello, and stays covering it if it never does. The editor is
|
||||
loaded from another host, so "nothing appeared" is a real outcome — an offline wiki, a blocked
|
||||
origin, a self-hosted deployment that has moved — and a blank white rectangle explains none of
|
||||
that.
|
||||
-->
|
||||
<div v-if="!state.ready" class="block-content-drawio-veil">
|
||||
<template v-if="state.timedOut">
|
||||
<w-icon name="la:plug" size="42px" />
|
||||
<div class="mt-3 text-body1">{{ t('editor.blockContent.drawioUnreachable') }}</div>
|
||||
<div class="text-caption mt-1 opacity-70">{{ origin }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<w-spinner size="42px" color="primary" />
|
||||
<div class="mt-3 text-caption">{{ t('editor.blockContent.drawioLoading') }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDark } from '@/composables/dark'
|
||||
|
||||
/**
|
||||
* The draw.io drawing surface, as a body editor for `block-drawio`.
|
||||
*
|
||||
* Everything in this file is draw.io's own: nothing else in the app knows this protocol, and the
|
||||
* block system it plugs into knows only that it takes a string of text and gives one back. See
|
||||
* `BlockContentEditorOverlay` for what an editor component has to be.
|
||||
*
|
||||
* draw.io is embedded rather than reimplemented, and it is embedded rather than bundled because it is
|
||||
* a whole application. The wiki holds an iframe and talks to it over `postMessage`, which is what the
|
||||
* `embed=1&proto=json` mode of any draw.io deployment offers:
|
||||
*
|
||||
* in `{ event: 'init' }` it is ready to be given a drawing
|
||||
* out `{ action: 'load', xml, … }` here is the drawing
|
||||
* in `{ event: 'autosave', xml }` the drawing changed
|
||||
* in `{ event: 'save', xml }` the author pressed its Save button
|
||||
*
|
||||
* `autosave` is what keeps this component's model current, so the overlay's own Apply always has the
|
||||
* drawing as it stands; `save` is the same thing plus "and close", which is what the author means by
|
||||
* pressing it. Nothing is ever uploaded: an embedded draw.io does its work in the browser, and the XML
|
||||
* arrives here rather than at whoever is hosting the editor.
|
||||
*/
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
/** The block's body: a draw.io XML document, or empty for a drawing that does not exist yet. */
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** The block's own parameters. Only `server` means anything here. */
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// EMITS
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
/**
|
||||
* The draw.io that answers when the block names none.
|
||||
*
|
||||
* `embed.diagrams.net` is the deployment draw.io publishes for exactly this, and it is a different
|
||||
* host from the `viewer.diagrams.net` the block reads a finished drawing from — which is why the
|
||||
* block's `server` prop has a default on neither side and means "the public services" when empty.
|
||||
*/
|
||||
const PUBLIC_EDITOR = 'https://embed.diagrams.net'
|
||||
|
||||
/** How long draw.io has to say `init` before the veil stops being a spinner and starts explaining. */
|
||||
const READY_TIMEOUT = 20000
|
||||
|
||||
// DARK MODE
|
||||
|
||||
// -> The reader's effective theme, not the site's default: a user who has chosen light on a dark wiki
|
||||
// should be drawing on a light canvas
|
||||
const dark = useDark()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const frameEl = ref(null)
|
||||
|
||||
const state = reactive({
|
||||
ready: false,
|
||||
timedOut: false
|
||||
})
|
||||
|
||||
let readyTimer = null
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const server = computed(() => String(props.params.server || '').trim() || PUBLIC_EDITOR)
|
||||
|
||||
/** Where messages are sent, and the only place they are accepted from. */
|
||||
const origin = computed(() => {
|
||||
try {
|
||||
return new URL(server.value).origin
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const editorUrl = computed(() => {
|
||||
/*
|
||||
`ui` is settled once, when the frame is built, and deliberately does not follow the reader
|
||||
switching theme underneath it: the parameter is read at load, so keeping the two in step would
|
||||
mean reloading draw.io — and reloading it mid-drawing is the one thing that would lose work.
|
||||
*/
|
||||
const params = new URLSearchParams({
|
||||
embed: '1',
|
||||
proto: 'json',
|
||||
spin: '1',
|
||||
libraries: '1',
|
||||
// -> The overlay's own Cancel is the way out, and two of them beside each other say different
|
||||
// things about the unsaved drawing
|
||||
noExitBtn: '1',
|
||||
saveAndExit: '0',
|
||||
ui: dark.isActive ? 'dark' : 'kennedy'
|
||||
})
|
||||
return `${server.value.replace(/\/+$/, '')}/?${params.toString()}`
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
function post(message) {
|
||||
frameEl.value?.contentWindow?.postMessage(JSON.stringify(message), origin.value || '*')
|
||||
}
|
||||
|
||||
/**
|
||||
* One message from the frame.
|
||||
*
|
||||
* Both the window it came from and the origin it came from are checked. The window because any page
|
||||
* may post to any other, and this listener is on `window`; the origin because the frame's own
|
||||
* `contentWindow` is still whatever has been navigated into it.
|
||||
*/
|
||||
function onMessage(event) {
|
||||
if (event.source !== frameEl.value?.contentWindow) {
|
||||
return
|
||||
}
|
||||
if (origin.value && event.origin !== origin.value) {
|
||||
return
|
||||
}
|
||||
let message = null
|
||||
try {
|
||||
message = JSON.parse(event.data)
|
||||
} catch {
|
||||
// -> Not ours: draw.io speaks JSON here, and its other protocols do not
|
||||
return
|
||||
}
|
||||
switch (message.event) {
|
||||
case 'init':
|
||||
state.ready = true
|
||||
clearTimeout(readyTimer)
|
||||
// -> `autosave` is what makes the frame report every change, which is what Apply relies on
|
||||
post({ action: 'load', autosave: 1, xml: props.modelValue })
|
||||
break
|
||||
case 'autosave':
|
||||
emit('update:modelValue', message.xml ?? '')
|
||||
break
|
||||
case 'save':
|
||||
emit('update:modelValue', message.xml ?? '')
|
||||
emit('save')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// LIFECYCLE
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage)
|
||||
readyTimer = setTimeout(() => {
|
||||
state.timedOut = true
|
||||
}, READY_TIMEOUT)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
clearTimeout(readyTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.block-content-drawio {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&-frame {
|
||||
flex: 1 1 auto;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* -> Over the frame rather than instead of it: draw.io is loading behind this the whole time */
|
||||
&-veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
|
||||
@at-root .body--light & {
|
||||
background-color: #fff;
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
background-color: $dark-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<w-layout class="block-content-editor" view="hHh lpR fFf" container>
|
||||
<w-header class="card-header px-4 py-2">
|
||||
<w-icon
|
||||
:name="`img:/_assets/icons/ultraviolet-${block.isCustom ? 'plugin' : block.icon}.svg`"
|
||||
left
|
||||
size="md" />
|
||||
<div>
|
||||
<span>{{ t('editor.blockContent.title') }}</span>
|
||||
<div class="text-caption">{{ block.name }}</div>
|
||||
</div>
|
||||
<w-space />
|
||||
<w-btn-group push>
|
||||
<w-btn
|
||||
push
|
||||
color="white"
|
||||
text-color="grey-7"
|
||||
:label="t(`common.actions.cancel`)"
|
||||
:aria-label="t(`common.actions.cancel`)"
|
||||
icon="la:times"
|
||||
@click="close" />
|
||||
<w-btn
|
||||
push
|
||||
color="positive"
|
||||
text-color="white"
|
||||
:label="t(`common.actions.apply`)"
|
||||
:aria-label="t(`common.actions.apply`)"
|
||||
icon="la:check"
|
||||
@click="apply" />
|
||||
</w-btn-group>
|
||||
</w-header>
|
||||
<w-page-container>
|
||||
<!--
|
||||
No padding around the editor: every one of these is a whole working surface -- a canvas, a
|
||||
source pane -- and wants the screen it is given rather than a card's worth of it.
|
||||
-->
|
||||
<w-page class="block-content-editor-body">
|
||||
<component
|
||||
:is="editorComponent"
|
||||
v-if="editorComponent"
|
||||
v-model="state.source"
|
||||
:params="params"
|
||||
@save="apply" />
|
||||
<div v-else class="p-6">
|
||||
<w-card class="bg-negative rounded text-white" flat>
|
||||
<w-card-section class="items-center" horizontal>
|
||||
<w-card-section class="shrink-0 pr-0">
|
||||
<w-icon name="la:ban" size="lg" />
|
||||
</w-card-section>
|
||||
<w-card-section>
|
||||
<span>{{ t('editor.blockContent.unknownEditor') }}</span>
|
||||
<div class="text-caption text-red-1">{{ editorKey }}</div>
|
||||
</w-card-section>
|
||||
</w-card-section>
|
||||
</w-card>
|
||||
</div>
|
||||
</w-page>
|
||||
</w-page-container>
|
||||
</w-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent, onBeforeUnmount, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
|
||||
import LoadingGeneric from './LoadingGeneric.vue'
|
||||
|
||||
/**
|
||||
* The body of a block, in whatever editor the block named for it.
|
||||
*
|
||||
* The chrome and the plumbing are the block system's; what goes in the middle is not. A block
|
||||
* declares `contentEditor: '<key>'` in its `static definition`, the markdown editor offers an "Edit
|
||||
* Content" lens above it, and this resolves that key against the registry below — so adding an editor
|
||||
* is adding one component and one line to that map, and nothing anywhere else has to know what it
|
||||
* edits.
|
||||
*
|
||||
* What an editor component has to be, and the whole of it:
|
||||
*
|
||||
* - a `modelValue` of the block's body as text, and `update:modelValue` when it changes;
|
||||
* - a `params` object of the block's own parameters, for an editor that is configured by one — the
|
||||
* server it talks to, say. Which of them mean anything is the editor's business alone;
|
||||
* - optionally `@save`, for an editor with a save gesture of its own, which applies and closes.
|
||||
* An editor without one is applied by the button up here, which is always there either way.
|
||||
*
|
||||
* Text in and text out: the fence it lives in, the line it goes back on and the undo it lands in are
|
||||
* the markdown editor's, and it receives the result over the event bus the way the table editor and
|
||||
* the file manager hand theirs back.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The editors a block may name, by the key it names them with.
|
||||
*
|
||||
* Async, so a block nobody on this page uses costs nothing to have — an embedded drawing editor is a
|
||||
* whole application, and it should not be in the bundle of a wiki that has never drawn one.
|
||||
*/
|
||||
const EDITORS = {
|
||||
drawio: defineAsyncComponent({
|
||||
loader: () => import('./BlockContentDrawio.vue'),
|
||||
loadingComponent: LoadingGeneric
|
||||
})
|
||||
}
|
||||
|
||||
// STORES
|
||||
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
/*
|
||||
Read once, on the way in. The overlay is opened by a store patch and closes by clearing it, so
|
||||
holding the block and its parameters here keeps them from going out from under the editor as it
|
||||
closes -- and the source is a copy, so leaving without applying leaves the page as it was.
|
||||
*/
|
||||
const editorKey = siteStore.overlayOpts?.editor ?? ''
|
||||
const block = siteStore.overlayOpts?.block ?? {}
|
||||
const params = siteStore.overlayOpts?.params ?? {}
|
||||
const replace = siteStore.overlayOpts?.replace ?? null
|
||||
|
||||
const state = reactive({
|
||||
source: siteStore.overlayOpts?.source ?? ''
|
||||
})
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const editorComponent = computed(() => EDITORS[editorKey] ?? null)
|
||||
|
||||
// METHODS
|
||||
|
||||
function apply() {
|
||||
if (replace) {
|
||||
EVENT_BUS.emit('replaceBlockContent', { source: state.source, replace })
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
function close() {
|
||||
siteStore.$patch({ overlay: '' })
|
||||
}
|
||||
|
||||
// -> Cleared whichever way the overlay was left, so a body left behind in the options is not the one
|
||||
// the next block opens on
|
||||
onBeforeUnmount(() => {
|
||||
siteStore.overlayOpts = {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.block-content-editor {
|
||||
/*
|
||||
A foreground to go with the surface, as the table editor needs for the same reason: nothing in
|
||||
here sits on a `w-card`, and that is what declares the app's text colour -- so without this
|
||||
everything that merely inherits it stays black on the dark overlay.
|
||||
*/
|
||||
@at-root .body--light & {
|
||||
color: var(--color-black);
|
||||
}
|
||||
@at-root .body--dark & {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
/*
|
||||
The editor fills the overlay. `min-height: 0` because this is a flex item of the page container:
|
||||
without it the box is sized by its content, and an editor asking for 100% of a box that is as tall
|
||||
as itself resolves to nothing at all.
|
||||
*/
|
||||
&-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
|
||||
> * {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue