Work on standalone editor

This commit is contained in:
Benjamin Bädorf 2022-03-20 14:49:44 +01:00
parent 7b9c598756
commit 4f870c63a6
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
24 changed files with 309 additions and 63 deletions

View file

@ -129,7 +129,7 @@ export const SbBlock = defineComponent({
/>; />;
} }
if (mode.value === SbMode.Display) { if (mode.value === SbMode.View) {
return <BlockComponent return <BlockComponent
data={props.block.data} data={props.block.data}
blockId={props.block.id} blockId={props.block.id}

View file

@ -3,6 +3,7 @@ import {
provide, provide,
shallowReactive, shallowReactive,
ref, ref,
watch,
PropType, PropType,
Ref, Ref,
} from 'vue'; } from 'vue';
@ -28,6 +29,13 @@ import { SbMainMenu } from './MainMenu';
import { SbBlockToolbar } from './BlockToolbar'; import { SbBlockToolbar } from './BlockToolbar';
import { SbBlock } from './Block'; import { SbBlock } from './Block';
export interface ISbMainProps {
availableBlocks: IBlockDefinition<any>[];
block: IBlockData<any>;
onUpdate: OnUpdateBlockCb;
mode: SbMode;
}
import './Main.scss'; import './Main.scss';
export const SbMain = defineComponent({ export const SbMain = defineComponent({
@ -60,13 +68,18 @@ export const SbMain = defineComponent({
}, },
}, },
setup(props: any) { // TODO: why does the typing of props not work here? setup(props: ISbMainProps) {
const el: Ref<null|HTMLElement> = ref(null); const el: Ref<null|HTMLElement> = ref(null);
useResizeObserver(el, SymEditorDimensions); useResizeObserver(el, SymEditorDimensions);
const mode = ref(props.mode); const mode = ref(props.mode);
provide(SymMode, mode); provide(SymMode, mode);
watch(() => props.mode, (newMode) => {
console.log('Mode update', newMode);
mode.value = newMode;
});
const activeBlock = ref(null); const activeBlock = ref(null);
provide(SymActiveBlock, activeBlock); provide(SymActiveBlock, activeBlock);

View file

@ -2,6 +2,6 @@ import { defineAsyncComponent } from 'vue';
export default { export default {
name: 'sb-missing-block', name: 'sb-missing-block',
edit: defineAsyncComponent(() => import('./display')), edit: defineAsyncComponent(() => import('./view')),
display: defineAsyncComponent(() => import('./display')), view: defineAsyncComponent(() => import('./view')),
}; };

View file

@ -3,6 +3,6 @@
*/ */
export enum SbMode { export enum SbMode {
Edit = 'edit', Edit = 'edit',
Display = 'display', View = 'view',
} }
export const SymMode = Symbol('Schlechtenburg mode'); export const SymMode = Symbol('Schlechtenburg mode');

View file

@ -4,8 +4,6 @@ export default defineConfig({
title: 'Schlechtenburg', title: 'Schlechtenburg',
description: 'Experimental WYSIWYG block editor', description: 'Experimental WYSIWYG block editor',
base: '/schlechtenburg/',
themeConfig: { themeConfig: {
nav: [ nav: [
{ {
@ -27,6 +25,7 @@ export default defineConfig({
text: 'Getting Started', text: 'Getting Started',
children: [ children: [
{ text: 'Why Schlechtenburg?', link: '/guide/why' }, { text: 'Why Schlechtenburg?', link: '/guide/why' },
{ text: 'Examples', link: '/guide/examples' },
{ text: 'Installation', link: '/guide/installation' }, { text: 'Installation', link: '/guide/installation' },
], ],
} }

View file

@ -25,37 +25,20 @@ export default defineComponent({
const displayedElement = computed(() => { const displayedElement = computed(() => {
switch (activeTab.value) { switch (activeTab.value) {
case SbMode.Edit:
return <SbMain
class="example-editor--sb"
block={block}
onUpdate={(newBlock: IBlockData<any>) => {
block.data = newBlock.data;
}}
availableBlocks={[
SbLayout,
SbHeading,
SbImage,
SbParagraph,
]}
key="edit"
mode={SbMode.Edit}
/>;
case SbMode.Display:
return <SbMain
class="example-editor--sb"
block={block}
availableBlocks={[
SbLayout,
SbHeading,
SbImage,
SbParagraph,
]}
key="display"
mode={SbMode.Display}
/>;
case 'data': case 'data':
return <pre><code>{ JSON.stringify(block, null, 2) }</code></pre>; return <pre><code>{ JSON.stringify(block, null, 2) }</code></pre>;
default:
return <SbMain
class="example-editor--sb"
block={block}
availableBlocks={[
SbLayout,
SbHeading,
SbImage,
SbParagraph,
]}
mode={activeTab.value as SbMode}
/>;
} }
}); });
@ -70,8 +53,8 @@ export default defineComponent({
activeTab.value = ($event.target as HTMLSelectElement).value; activeTab.value = ($event.target as HTMLSelectElement).value;
}} }}
> >
<option value="edit">Editor mode</option> <option value={SbMode.Edit}>Editor mode</option>
<option value="display">Display mode</option> <option value={SbMode.View}>Viewer mode</option>
<option value="data">JSON Data structure</option> <option value="data">JSON Data structure</option>
</select> </select>
</h2> </h2>

View file

@ -0,0 +1,51 @@
import {
defineComponent,
onMounted,
} from 'vue';
import { startSchlechtenburg } from '@schlechtenburg/standalone';
import { SbMode } from '@schlechtenburg/core';
import SbLayout from '@schlechtenburg/layout';
import SbHeading from '@schlechtenburg/heading';
import SbParagraph from '@schlechtenburg/paragraph';
import SbImage from '@schlechtenburg/image';
import exampleData from './example-data';
import './ExampleEditor.scss';
export default defineComponent({
name: 'ExampleStandaloneEditor',
setup() {
const block = exampleData;
onMounted(async () => {
const { getBlock } = await startSchlechtenburg(
'#example-editor',
{
// The input block data
block,
mode: SbMode.Edit,
// The list of available blocks in this editor instance
availableBlocks: [
SbLayout,
SbHeading,
SbParagraph,
SbImage,
],
// This callback will be alled any time the block data gets updated
onUpdate: (blockData) => {
console.log('Got onUpdate', blockData);
console.log('getBlock', getBlock());
}
},
)
});
return () => <div id="example-editor"></div>;
},
});

View file

@ -0,0 +1,20 @@
<script setup>
import ExampleEditor from '../ExampleEditor'
import ExampleStandaloneEditor from '../ExampleStandaloneEditor'
</script>
# Examples
## Vue Component without wrapper
This documentation website already uses Vue under the hood, so Schlechtenburg can just imported as
any other component:
<ExampleEditor></ExampleEditor>
## Wrapped with Vue
`@schlechtenburg/standalone` gives you a wrapped version of the editor in case you don't have Vue
already installed in your application
<ExampleStandaloneEditor></ExampleStandaloneEditor>

View file

@ -1,6 +1,83 @@
# Installation # Installation
First, install the editor core and any blocks you want to use: Schlechtenburg is very modular; consisting of one core package and multiple blocks. All packages are versioned together,
meaning that v2.0.3 of one package is guaranteed to work with v2.0.3 of another schlechtenburg package.
Schlechtenburg is basically one Vue component, so if you're already using Vue you can import and use it directly.
Otherwise, there's the standalone version that comes prepackaged with Vue.
## You're not yet using Vue
### Install npm packages
Install the standalone editor and any blocks you want to use:
```ts
npm i --save @schlechtenburg/standalone \
@schlechtenburg/layout \
@schlechtenburg/heading \
@schlechtenburg/paragraph
```
### Initializing the editor
```ts
// Import the initialization function
import { startSchlechtenburg } from '@schlechtenburg/standalone';
import { SbMode } from '@schlechtenburg/core';
// The following are some Schlechtenburg blocks that
// will be available when editing or viewing
import {
SbLayout,
getDefaultData as getEmptyLayoutBlock,
} from '@schlechtenburg/layout';
import { SbHeading } from '@schlechtenburg/heading';
import { SbParagraph } from '@schlechtenburg/paragraph';
import { SbImage } from '@schlechtenburg/image';
// This will be our input state
const emptyLayout = getEmptyLayoutBlock();
// This call initializes the Schlechtenburg editor and viewer.
useSchlechtenburg(
// Selector of the element the editor should bind to.
// Can also the an `HTMLElement` reference.
'#editor',
{
// The input block data
block: emptyLayout,
// Whether Schlechtenburg is in what-you-see (editing)
// or in what-you-get (viewing)
mode: SbMode.Edit,
// The list of available blocks in this editor instance
availableBlocks: [
SbLayout,
SbHeading,
SbParagraph,
SbImage,
],
// This callback will be alled any time the block data gets updated
onUpdate: (blockData) => {
console.log('Got new block data', blockData);
}
}, //
)
```
**Note:** You need to provide both a root node
## You're already using Vue
### Install npm packages
Install the editor core and any blocks you want to use:
``` ```
npm i --save @schlechtenburg/core \ npm i --save @schlechtenburg/core \
@ -10,3 +87,33 @@ npm i --save @schlechtenburg/core \
``` ```
### Using the editor component
The following example uses TSX, but `SbMain` is just a Vue component here and can be imported and used just like any other vue component.
You need to provide a root
```tsx
// This is the main Schlechtenburg component
import { SbMain } from '@schlechtenburg/core';
// The following are some Schlechtenburg blocks that will be available when editing or viewing
import { SbLayout } from '@schlechtenburg/layout';
import { SbHeading } from '@schlechtenburg/heading';
import { SbParagraph } from '@schlechtenburg/paragraph';
import { SbImage } from '@schlechtenburg/image';
// In your component
setup () {
// ..
return () => <SbMain
availableBlocks={[
SbLayout,
SbHeading,
SbParagraph,
SbImage,
]}
/>;
}
```

View file

@ -48,11 +48,6 @@ experience is great. Schlechtenburg aims to offer a vast library of reusable com
variables, and rules for the editing UI. We call this **SBUI**. Complex blocks require complex editing forms and UIs so variables, and rules for the editing UI. We call this **SBUI**. Complex blocks require complex editing forms and UIs so
most of the work goes into creating this UI. A good Design System should help ease the pain. most of the work goes into creating this UI. A good Design System should help ease the pain.
## SSR Compatible
Does as it says; drop Schlechtenburg into Nuxt.js, and not just the display mode but also the editor
itself will render on the server.
## Accessible ## Accessible
Toolbars and editing elements are in the correct tab order, **SBUI** elements are all fully Toolbars and editing elements are in the correct tab order, **SBUI** elements are all fully
@ -74,6 +69,12 @@ looks like this:
}, },
``` ```
The main advantage here is that it enables you to write your own tooling around the format, since
you don't have to deal with HTML or the DOM directly. This also enables really easy subtree rendering,
by just taking that subtree of the JSON and feeding it to a Schlechtenburg instance. if instead of
rendering a full page you'd only want to render the images, you could find all of the `sb-image` nodes
in the tree and rendering them all inside an `sb-layout` block.
## So why not Gutenberg? ## So why not Gutenberg?
Gutenberg is tied heavily into the Wordpress ecosystem, making its inclusion in other sites harder Gutenberg is tied heavily into the Wordpress ecosystem, making its inclusion in other sites harder

View file

@ -26,6 +26,7 @@
"serve": "vitepress serve lib" "serve": "vitepress serve lib"
}, },
"dependencies": { "dependencies": {
"@schlechtenburg/standalone": "^0.0.0",
"@schlechtenburg/core": "^0.0.0", "@schlechtenburg/core": "^0.0.0",
"@schlechtenburg/heading": "^0.0.0", "@schlechtenburg/heading": "^0.0.0",
"@schlechtenburg/image": "^0.0.0", "@schlechtenburg/image": "^0.0.0",

View file

@ -8,5 +8,5 @@ export default {
name, name,
getDefaultData, getDefaultData,
edit: defineAsyncComponent(() => import('./edit')), edit: defineAsyncComponent(() => import('./edit')),
display: defineAsyncComponent(() => import('./display')), view: defineAsyncComponent(() => import('./view')),
}; };

View file

@ -8,5 +8,5 @@ export default {
name, name,
getDefaultData, getDefaultData,
edit: defineAsyncComponent(() => import('./edit')), edit: defineAsyncComponent(() => import('./edit')),
display: defineAsyncComponent(() => import('./display')), view: defineAsyncComponent(() => import('./view')),
}; };

View file

@ -8,5 +8,5 @@ export default {
name, name,
getDefaultData, getDefaultData,
edit: defineAsyncComponent(() => import('./edit')), edit: defineAsyncComponent(() => import('./edit')),
display: defineAsyncComponent(() => import('./display')), view: defineAsyncComponent(() => import('./view')),
}; };

View file

@ -8,5 +8,5 @@ export default {
name, name,
getDefaultData, getDefaultData,
edit: defineAsyncComponent(() => import('./edit')), edit: defineAsyncComponent(() => import('./edit')),
display: defineAsyncComponent(() => import('./display')), view: defineAsyncComponent(() => import('./view')),
}; };

View file

@ -47,8 +47,8 @@ export const Schlechtenburg = defineComponent({
setup(props: ISchlechtenburgProps) { setup(props: ISchlechtenburgProps) {
return () => <SbMain return () => <SbMain
availableBlocks={props.availableBlocks}
block={props.block} block={props.block}
availableBlocks={props.availableBlocks}
onUpdate={props.onUpdate} onUpdate={props.onUpdate}
mode={props.mode} mode={props.mode}
/>; />;

View file

@ -0,0 +1,61 @@
import {
defineComponent,
ref,
PropType,
} from 'vue'
import {
IBlockDefinition,
IBlockData,
SbMain,
SbMode,
OnUpdateBlockCb,
} from '@schlechtenburg/core';
/**
*
*/
export default function getWrapper({
block,
mode,
availableBlocks,
}) {
return defineComponent({
name: 'SchlechtenburgWrapper',
props: {
availableBlocks: {
type: Array as PropType<IBlockDefinition<any>[]>,
default: () => [],
},
block: {
type: Object as PropType<IBlockData<any>>,
required: true,
},
/**
* Called when the block should be updated.
*/
onUpdate: {
type: (null as unknown) as PropType<OnUpdateBlockCb>,
default: () => {},
},
mode: {
type: String as PropType<SbMode>,
validator(value: any) {
return Object.values(SbMode).includes(value);
},
default: SbMode.Edit,
},
},
setup(props) {
const refBlock = ref({ ...block });
const refMode = ref({ ...block });
return () => <SbMain
block={refBlock}
availableBlocks={props.availableBlocks}
mode={mode}
/>
}
});
}

View file

@ -1,8 +1,11 @@
import { createApp } from 'vue'
import { import {
ISchlechtenburgProps, createApp,
Schlechtenburg, } from 'vue'
} from './Schlechtenburg'; import {
ISbMainProps,
IBlockData,
SbMain,
} from '@schlechtenburg/core';
/** /**
* *
@ -16,10 +19,21 @@ export const startSchlechtenburg = async (
/** /**
* The schlechtenburg props * The schlechtenburg props
*/ */
props:ISchlechtenburgProps, props:ISbMainProps,
) => { ) => {
const app = createApp(Schlechtenburg, props as unknown as Record<string, unknown>); let block = { ...props.block };
const app = createApp(SbMain, {
...props,
onUpdate: (update: IBlockData<any>) => {
block = update;
props.onUpdate(update);
},
}as unknown as Record<string, unknown>);
app.mount(el); app.mount(el);
return app; return {
getBlock() {
return block;
},
};
} }

View file

@ -1,5 +1,5 @@
{ {
"name": "@schlechtenburg/core", "name": "@schlechtenburg/standalone",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
@ -982,11 +982,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lru-cache": { "lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -1213,7 +1208,8 @@
"uuid": { "uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
}, },
"vscode-languageserver-textdocument": { "vscode-languageserver-textdocument": {
"version": "1.0.4", "version": "1.0.4",