Big improvements

This commit is contained in:
Benjamin Bädorf 2022-09-05 21:12:20 +02:00
parent a6b162eddb
commit 3ee4418ef3
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
61 changed files with 27724 additions and 3340 deletions

30
docs/.vitepress/config.ts Normal file
View file

@ -0,0 +1,30 @@
import { defineConfig } from 'vitepress';
export default defineConfig({
lang: 'en-US',
title: 'Schlechtenburg',
description: 'Experimental WYSIWYG editor framework made with Vue 3 and TypeScript',
lastUpdated: true,
themeConfig: {
sidebar: [
{
text: 'User Guide',
items: [
{ text: 'Introduction', link: '/' },
{ text: 'Try it out', link: '/try' },
{ text: 'Why Schlechtenburg?', link: '/why' },
{ text: 'Installation and usage', link: '/installation' },
]
},
{
text: 'Development',
items: [
{ text: 'Introduction', link: '/' },
{ text: 'Installation and usage', link: '/installation' },
{ text: 'Getting Started', link: '/getting-started' },
]
}
]
}
});

View file

@ -2,16 +2,40 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&--title { &--mode {
display: flex; display: flex;
justify-content: space-between; justify-content: stretch;
align-items: center; align-items: center;
} }
&--mode { } &--mode-tab {
display: flex;
margin-top: 1px;
padding: 0.25rem 1rem;
border-top: 1px solid var(--vp-c-divider-light);
border-right: 1px solid var(--vp-c-divider-light);
input {
display: none;
}
&_checked {
color: var(--vp-c-brand);
}
}
&--sb { &--sb {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--vp-c-divider-light);
}
&--json {
margin: 0;
border: 1px solid var(--vp-c-divider-light);
background-color: var(--vp-c-text-1);
color: var(--vp-c-bg);
overflow: scroll;
} }
} }

View file

@ -22,11 +22,12 @@ export default defineComponent({
setup() { setup() {
const activeTab = ref('edit'); const activeTab = ref('edit');
const block: IBlockData<any> = reactive({ ...exampleData }); const block: IBlockData<any> = reactive({ ...exampleData });
const dateID = +(new Date());
const displayedElement = computed(() => { const displayedElement = computed(() => {
switch (activeTab.value) { switch (activeTab.value) {
case 'data': case 'data':
return <pre><code>{ JSON.stringify(block, null, 2) }</code></pre>; return <pre class="example-editor--json"><code>{ JSON.stringify(block, null, 2) }</code></pre>;
default: default:
return <SbMain return <SbMain
class="example-editor--sb" class="example-editor--sb"
@ -38,26 +39,62 @@ export default defineComponent({
SbParagraph, SbParagraph,
]} ]}
mode={activeTab.value as SbMode} mode={activeTab.value as SbMode}
eventUpdate={(data:IBlockData<any>) => {
block.id = data.id;
block.name = data.name;
block.data = data.data;
}}
/>; />;
} }
}); });
const onModeChange = ($event: Event) => {
activeTab.value = ($event.target as HTMLSelectElement).value;
};
return () => { return () => {
return <div class="example-editor"> return <div class="example-editor">
<h2 class="example-editor--title"> <div class="example-editor--mode">
<span>Try it yourself</span> <label class={{
<select 'example-editor--mode-tab': true,
class="example-editor--mode" 'example-editor--mode-tab_checked': activeTab.value === SbMode.Edit,
value={activeTab.value} }}>
onChange={($event: Event) => { <input
activeTab.value = ($event.target as HTMLSelectElement).value; type="radio"
}} name={`example-editor-${dateID}`}
> value={SbMode.Edit}
<option value={SbMode.Edit}>Editor mode</option> checked={activeTab.value === SbMode.Edit}
<option value={SbMode.View}>Viewer mode</option> onChange={onModeChange}
<option value="data">JSON Data structure</option> />
</select> Editor mode
</h2> </label>
<label class={{
'example-editor--mode-tab': true,
'example-editor--mode-tab_checked': activeTab.value === SbMode.View,
}}>
<input
type="radio"
name={`example-editor-${dateID}`}
value={SbMode.View}
checked={activeTab.value === SbMode.View}
onChange={onModeChange}
/>
View mode
</label>
<label class={{
'example-editor--mode-tab': true,
'example-editor--mode-tab_checked': activeTab.value === "data",
}}>
<input
type="radio"
name={`example-editor-${dateID}`}
value="data"
checked={activeTab.value === "data"}
onChange={onModeChange}
/>
JSON Data structure
</label>
</div>
{displayedElement.value} {displayedElement.value}
</div>; </div>;
}; };

13896
docs/api.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.

View file

@ -1,64 +0,0 @@
:root {
--light-hl-0: #000000;
--dark-hl-0: #C8C8C8;
--light-hl-1: #000000;
--dark-hl-1: #D4D4D4;
--light-hl-2: #0000FF;
--dark-hl-2: #569CD6;
--light-hl-3: #AF00DB;
--dark-hl-3: #C586C0;
--light-hl-4: #267F99;
--dark-hl-4: #4EC9B0;
--light-hl-5: #001080;
--dark-hl-5: #9CDCFE;
--light-code-background: #F5F5F5;
--dark-code-background: #1E1E1E;
}
@media (prefers-color-scheme: light) { :root {
--hl-0: var(--light-hl-0);
--hl-1: var(--light-hl-1);
--hl-2: var(--light-hl-2);
--hl-3: var(--light-hl-3);
--hl-4: var(--light-hl-4);
--hl-5: var(--light-hl-5);
--code-background: var(--light-code-background);
} }
@media (prefers-color-scheme: dark) { :root {
--hl-0: var(--dark-hl-0);
--hl-1: var(--dark-hl-1);
--hl-2: var(--dark-hl-2);
--hl-3: var(--dark-hl-3);
--hl-4: var(--dark-hl-4);
--hl-5: var(--dark-hl-5);
--code-background: var(--dark-code-background);
} }
body.light {
--hl-0: var(--light-hl-0);
--hl-1: var(--light-hl-1);
--hl-2: var(--light-hl-2);
--hl-3: var(--light-hl-3);
--hl-4: var(--light-hl-4);
--hl-5: var(--light-hl-5);
--code-background: var(--light-code-background);
}
body.dark {
--hl-0: var(--dark-hl-0);
--hl-1: var(--dark-hl-1);
--hl-2: var(--dark-hl-2);
--hl-3: var(--dark-hl-3);
--hl-4: var(--dark-hl-4);
--hl-5: var(--dark-hl-5);
--code-background: var(--dark-code-background);
}
.hl-0 { color: var(--hl-0); }
.hl-1 { color: var(--hl-1); }
.hl-2 { color: var(--hl-2); }
.hl-3 { color: var(--hl-3); }
.hl-4 { color: var(--hl-4); }
.hl-5 { color: var(--hl-5); }
pre, code { background: var(--code-background); }

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,3 +0,0 @@
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>IBlockLibrary | schlechtenburg</title><meta name="description" content="Documentation for schlechtenburg"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">schlechtenburg</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label><input type="checkbox" id="tsd-filter-externals" checked/><label class="tsd-widget" for="tsd-filter-externals">Externals</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../index.html">schlechtenburg</a></li><li><a href="../modules/_schlechtenburg_core.html">@schlechtenburg/core</a></li><li><a href="_schlechtenburg_core.IBlockLibrary.html">IBlockLibrary</a></li></ul><h1>Interface IBlockLibrary</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel tsd-comment"><div class="tsd-comment tsd-typography"><div class="lead">
<p>Schlechtenburg maintains a library of blocks that are available</p>
</div><dl class="tsd-comment-tags"><dt>internal</dt><dd></dd></dl></div></section><section class="tsd-panel tsd-hierarchy"><h3>Hierarchy</h3><ul class="tsd-hierarchy"><li><span class="target">IBlockLibrary</span></li></ul></section><section class="tsd-panel tsd-kind-interface tsd-parent-kind-module"><h3 class="tsd-before-signature">Indexable</h3><div class="tsd-signature tsd-kind-icon"><span class="tsd-signature-symbol">[</span>name: <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">]: </span><a href="_schlechtenburg_core.IBlockDefinition.html" class="tsd-signature-type" data-tsd-kind="Interface">IBlockDefinition</a><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">any</span><span class="tsd-signature-symbol">&gt;</span></div></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../index.html">Modules</a></li><li class="current tsd-kind-module"><a href="../modules/_schlechtenburg_core.html">@schlechtenburg/core</a></li><li class=" tsd-kind-module"><a href="../modules/_schlechtenburg_heading.html">@schlechtenburg/heading</a></li><li class=" tsd-kind-module"><a href="../modules/_schlechtenburg_image.html">@schlechtenburg/image</a></li><li class=" tsd-kind-module"><a href="../modules/_schlechtenburg_layout.html">@schlechtenburg/layout</a></li><li class=" tsd-kind-module"><a href="../modules/_schlechtenburg_paragraph.html">@schlechtenburg/paragraph</a></li><li class=" tsd-kind-module"><a href="../modules/_schlechtenburg_standalone.html">@schlechtenburg/standalone</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="current tsd-kind-interface tsd-parent-kind-module"><a href="_schlechtenburg_core.IBlockLibrary.html" class="tsd-kind-icon">IBlock<wbr/>Library</a><ul></ul></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-variable"><span class="tsd-kind-icon">Variable</span></li><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li><li class="tsd-kind-function tsd-has-type-parameter"><span class="tsd-kind-icon">Function with type parameter</span></li><li class="tsd-kind-type-alias"><span class="tsd-kind-icon">Type alias</span></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><span class="tsd-kind-icon">Type alias with type parameter</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li><li class="tsd-kind-interface tsd-has-type-parameter"><span class="tsd-kind-icon">Interface with type parameter</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
# Creating blocks

View file

@ -1,20 +1,19 @@
<script setup> ---
import { withBase } from 'vitepress'; layout: home
import ExampleEditor from './ExampleEditor';
</script>
# Yet another WYSIWYG editor hero:
name: Schlechtenburg
text: WYSIWYG that's fun.
tagline: What-you-see-is-what-you-get editors are unfortunately still really hard to deal with on the web. The default functionality is usually quickly deployed, but the honeymoon does not last long.
Schlechtenburg is an experimental WYSIWYG editor framework made with Vue 3 and TypeScript. It takes cues from both Wordpress' Gutenberg editor and CKEditor, though it tries to become a best of both worlds; a very lightweight, easily extensible core, written with modern components and the accompanying state management.
It inputs and outputs a tree of JSON-serializable data. Changes to editor behavior are always needed in the real-world, a fact that Schlechtenburg embraces.
This is still in the Proof-of-concept phase. actions:
- theme: brand
<div class="cta-row"> text: Try it out
<a :href="withBase('/guide/why')" class="button button_cta">Why Schlechtenburg?</a> link: /try
<a :href="withBase('guide/introduction')" class="button">Get Started</a> - theme: alt
<a :href="withBase('api')" class="button">See the API docs</a> text: View Code
</div> link: https://git.b12f.io/b12f/schlechtenburg
---
<ExampleEditor></ExampleEditor>

View file

@ -1,4 +1,4 @@
# Installation # Installation and usage
Schlechtenburg is very modular; consisting of one core package and multiple blocks. All packages are versioned together, 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. meaning that v2.0.3 of one package is guaranteed to work with v2.0.3 of another schlechtenburg package.
@ -6,7 +6,7 @@ meaning that v2.0.3 of one package is guaranteed to work with v2.0.3 of another
Schlechtenburg is basically one Vue component, so if you're already using Vue you can import and use it directly. 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. Otherwise, there's the standalone version that comes prepackaged with Vue.
## You're not yet using Vue ## Your project does not use Vue 3
### Install npm packages ### Install npm packages
@ -63,7 +63,6 @@ useSchlechtenburg(
// This callback will be alled any time the block data gets updated // This callback will be alled any time the block data gets updated
onUpdate: (blockData) => { onUpdate: (blockData) => {
console.log('Got new block data', blockData); console.log('Got new block data', blockData);
} }
}, // }, //
) )
@ -72,7 +71,7 @@ useSchlechtenburg(
**Note:** You need to provide both a root node **Note:** You need to provide both a root node
## You're already using Vue ## Your project uses Vue 3
### Install npm packages ### Install npm packages

View file

@ -23,11 +23,50 @@ html {
--interact: #3f9cff; --interact: #3f9cff;
--interact-lite: #3f9cff; --interact-lite: #3f9cff;
--z-context-menu: 3000;
} }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
} }
.sb-doc h1,
.sb-doc h2,
.sb-doc h3,
.sb-doc h4,
.sb-doc h5,
.sb-doc h6 {
position: relative;
font-weight: 600;
outline: none;
}
.sb-doc h1 {
letter-spacing: -0.02em;
line-height: 40px;
font-size: 28px;
}
.sb-doc h2 {
margin: 48px 0 16px;
border-top: 1px solid var(--vp-c-divider-light);
padding-top: 24px;
letter-spacing: -0.02em;
line-height: 32px;
font-size: 24px;
}
.sb-doc h3 {
margin: 32px 0 0;
letter-spacing: -0.01em;
line-height: 28px;
font-size: 20px;
}
@media (min-width: 768px) {
.sb-doc h1 {
letter-spacing: -0.02em;
line-height: 40px;
font-size: 32px;
}
}

16
docs/try.md Normal file
View file

@ -0,0 +1,16 @@
---
layout: page
---
<script setup>
import { withBase } from 'vitepress';
import ExampleEditor from './ExampleEditor';
</script>
<div class="sb-doc">
# Example Schlechtenburg Editor
</div>
<ExampleEditor></ExampleEditor>

1
docs/usage.md Normal file
View file

@ -0,0 +1 @@
# Usage

18
docs/vite.config.ts Normal file
View file

@ -0,0 +1,18 @@
import { defineConfig } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx'
import { join } from 'path';
export default defineConfig({
plugins: [
vueJsx(),
],
resolve: {
alias: {
'@schlechtenburg/core': join(__dirname, '../packages/core/lib/index.ts'),
'@schlechtenburg/paragraph': join(__dirname, '../packages/paragraph/lib/index.ts'),
'@schlechtenburg/heading': join(__dirname, '../packages/heading/lib/index.ts'),
'@schlechtenburg/image': join(__dirname, '../packages/image/lib/index.ts'),
'@schlechtenburg/layout': join(__dirname, '../packages/layout/lib/index.ts'),
},
},
});

View file

@ -17,10 +17,10 @@ in their architecture:
* They input and output a string * They input and output a string
* They have one global toolbar * They have one global toolbar
Gutenberg is a bit more involved, literally using building "blocks" to create it's editor. Instead Gutenberg is a bit more involved, literally using building "blocks" to create its editor. Instead
of seeing the content as a long string it takes a more component-esque approach. For example, the of seeing the content as a long string it takes a more component-esque approach. For example, the
following things are all their own blocks in the gutenberg editor, which a specific react component following things are all their own blocks in the gutenberg editor, which a specific react component
that handles the editing mode, and one that handles the display mode. that handles the editing mode, and one that handles the display mode:
* Paragraph * Paragraph
* Heading * Heading

14010
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,20 @@
"version": "0.0.0", "version": "0.0.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"scripts": { "scripts": {
"docs:build": "npx typedoc --out ./docs/api --entryPointStrategy packages --readme none packages/core packages/heading packages/standalone packages/paragraph packages/layout packages/image", "docs:type:build": "npx typedoc --json ./docs/api.json --entryPointStrategy packages --readme none packages/core packages/heading packages/standalone packages/paragraph packages/layout packages/image",
"typecheck": "lerna run --stream typecheck" "typecheck": "lerna run --stream typecheck",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue-jsx": "^2.0.0",
"lerna": "^3.22.1", "lerna": "^3.22.1",
"sass": "^1.54.4",
"typedoc": "^0.22.13", "typedoc": "^0.22.13",
"typescript": "^4.6.2" "typescript": "^4.6.2",
"vitepress": "^1.0.0-alpha.13",
"vue": "^3.2.37"
}, },
"dependencies": { "dependencies": {
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"

View file

@ -45,47 +45,54 @@ export const SbBlock = defineComponent({
type: (null as unknown) as PropType<IBlockData<any>>, type: (null as unknown) as PropType<IBlockData<any>>,
required: true, required: true,
}, },
/**
* The state for the block.
*/
indexAsChild: {
type: Number,
default: -1,
},
/** /**
* Called when the block should be updated. * Called when the block should be updated.
*/ */
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<OnUpdateBlockCb>, type: (null as unknown) as PropType<OnUpdateBlockCb>,
default: () => {}, default: () => () => {},
}, },
/** /**
* Called when a sibling block should be inserted before the block * Called when a sibling block should be inserted before the block
*/ */
onPrependBlock: { eventPrependBlock: {
type: (null as unknown) as PropType<OnPrependBlockCb>, type: (null as unknown) as PropType<OnPrependBlockCb>,
default: () => {}, default: () => () => {},
}, },
/** /**
* Called when a sibling block should be inserted after the block * Called when a sibling block should be inserted after the block
*/ */
onAppendBlock: { eventAppendBlock: {
type: (null as unknown) as PropType<OnAppendBlockCb>, type: (null as unknown) as PropType<OnAppendBlockCb>,
default: () => {}, default: () => () => {},
}, },
/** /**
* Called when the block should be removed * Called when the block should be removed
*/ */
onRemoveSelf: { eventRemoveSelf: {
type: (null as unknown) as PropType<OnRemoveSelfCb>, type: (null as unknown) as PropType<OnRemoveSelfCb>,
default: () => {}, default: () => () => {},
}, },
/** /**
* Called when the previous sibling block should be activated * Called when the previous sibling block should be activated
*/ */
onActivatePrevious: { eventActivatePrevious: {
type: (null as unknown) as PropType<OnActivatePreviousCb>, type: (null as unknown) as PropType<OnActivatePreviousCb>,
default: () => {}, default: () => () => {},
}, },
/** /**
* Called when the next sibling block should be activated * Called when the next sibling block should be activated
*/ */
onActivateNext: { eventActivateNext: {
type: (null as unknown) as PropType<OnActivateNextCb>, type: (null as unknown) as PropType<OnActivateNextCb>,
default: () => {}, default: () => () => {},
}, },
}, },
@ -105,11 +112,11 @@ export const SbBlock = defineComponent({
watch(() => props.block.data, triggerSizeCalculation); watch(() => props.block.data, triggerSizeCalculation);
const { register } = useBlockTree(); const { register } = useBlockTree();
register(props.block); register(props.block, props.indexAsChild);
watch(props.block, () => { register(props.block); }); watch(props.block, () => { register(props.block, props.indexAsChild); });
const onChildUpdate = (updated: {[key: string]: any}) => { const eventChildUpdate = (updated: {[key: string]: any}) => {
props.onUpdate({ props.eventUpdate({
...props.block, ...props.block,
data: { data: {
...props.block.data, ...props.block.data,
@ -151,12 +158,12 @@ export const SbBlock = defineComponent({
<BlockComponent <BlockComponent
data={props.block.data} data={props.block.data}
blockId={props.block.id} blockId={props.block.id}
onUpdate={onChildUpdate} eventUpdate={eventChildUpdate}
onPrependBlock={props.onPrependBlock} eventPrependBlock={props.eventPrependBlock}
onAppendBlock={props.onAppendBlock} eventAppendBlock={props.eventAppendBlock}
onRemoveSelf={props.onRemoveSelf} eventRemoveSelf={props.eventRemoveSelf}
onActivatePrevious={props.onActivatePrevious} eventActivatePrevious={props.eventActivatePrevious}
onActivateNext={props.onActivateNext} eventActivateNext={props.eventActivateNext}
{...{ {...{
onClick: ($event: MouseEvent) => { onClick: ($event: MouseEvent) => {

View file

@ -19,9 +19,9 @@ export const SbBlockOrdering = defineComponent({
type: String, type: String,
default: null, default: null,
}, },
onRemove: { type: Function, default: () => {} }, eventRemove: { type: Function, default: () => {} },
onMoveBackward: { type: Function, default: () => {} }, eventMoveBackward: { type: Function, default: () => {} },
onMoveForward: { type: Function, default: () => {} }, eventMoveForward: { type: Function, default: () => {} },
}, },
setup(props) { setup(props) {
@ -55,9 +55,9 @@ export const SbBlockOrdering = defineComponent({
style={styles} style={styles}
onClick={($event: MouseEvent) => $event.stopPropagation()} onClick={($event: MouseEvent) => $event.stopPropagation()}
> >
<SbButton {...{onClick: props.onMoveBackward}}>{props.orientation === 'vertical' ? '↑' : '←'}</SbButton> <SbButton {...{onClick: props.eventMoveBackward}}>{props.orientation === 'vertical' ? '↑' : '←'}</SbButton>
<SbButton {...{onClick: props.onRemove}}>x</SbButton> <SbButton {...{onClick: props.eventRemove}}>x</SbButton>
<SbButton {...{onClick: props.onMoveForward}}>{props.orientation === 'vertical' ? '↓' : '→'}</SbButton> <SbButton {...{onClick: props.eventMoveForward}}>{props.orientation === 'vertical' ? '↓' : '→'}</SbButton>
</div> </div>
); );
}, },

View file

@ -16,7 +16,7 @@ export const SbBlockPicker = defineComponent({
name: 'sb-block-picker', name: 'sb-block-picker',
props: { props: {
onPickedBlock: { type: Function, default: () => {} }, eventPickedBlock: { type: Function, default: () => {} },
}, },
setup(props, context) { setup(props, context) {
@ -28,7 +28,7 @@ export const SbBlockPicker = defineComponent({
const selectBlock = (block: IBlockDefinition<any>) => { const selectBlock = (block: IBlockDefinition<any>) => {
open.value = false; open.value = false;
props.onPickedBlock({ props.eventPickedBlock({
name: block.name, name: block.name,
id: generateBlockId(), id: generateBlockId(),
data: block.getDefaultData(), data: block.getDefaultData(),

View file

@ -17,14 +17,14 @@ export const SbBlockPlaceholder = defineComponent({
/** /**
* Called when the user picked a block that should be inserted here. * Called when the user picked a block that should be inserted here.
*/ */
onInsertBlock: { type: Function, default: () => {} }, eventInsertBlock: { type: Function, default: () => {} },
}, },
setup(props) { setup(props) {
return () => ( return () => (
<div class="sb-block-placeholder"> <div class="sb-block-placeholder">
<SbBlockPicker <SbBlockPicker
onPickedBlock={(block: IBlockData<any>) => props.onInsertBlock(block)} eventPickedBlock={(block: IBlockData<any>) => props.eventInsertBlock(block)}
/> />
</div> </div>
); );

View file

@ -10,6 +10,7 @@
top: 100%; top: 100%;
left: 0; left: 0;
margin: 0; margin: 0;
padding: 0.5rem 0.25rem;
z-index: var(--z-context-menu); z-index: var(--z-context-menu);
max-height: 70vh; max-height: 70vh;

View file

@ -1,5 +1,6 @@
.sb-main { .sb-main {
position: relative; position: relative;
color: var(--fg);
background-color: var(--bg); background-color: var(--bg);
--grey-0: white; --grey-0: white;
@ -21,7 +22,10 @@
--interact: #3f9cff; --interact: #3f9cff;
--interact-lite: #3f9cff; --interact-lite: #3f9cff;
--z-toolbar: 2000;
--z-context-menu: 3000; --z-context-menu: 3000;
--z-tree-block-select: 4000;
--z-modal: 10000;
*, *,
*::before, *::before,

View file

@ -32,7 +32,7 @@ import { SbBlock } from './Block';
export interface ISbMainProps { export interface ISbMainProps {
availableBlocks: IBlockDefinition<any>[]; availableBlocks: IBlockDefinition<any>[];
block: IBlockData<any>; block: IBlockData<any>;
onUpdate: OnUpdateBlockCb; eventUpdate: OnUpdateBlockCb;
mode: SbMode; mode: SbMode;
} }
@ -55,7 +55,7 @@ export const SbMain = defineComponent({
/** /**
* Called when the block should be updated. * Called when the block should be updated.
*/ */
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<OnUpdateBlockCb>, type: (null as unknown) as PropType<OnUpdateBlockCb>,
default: () => {}, default: () => {},
}, },
@ -76,7 +76,6 @@ export const SbMain = defineComponent({
provide(SymMode, mode); provide(SymMode, mode);
watch(() => props.mode, (newMode) => { watch(() => props.mode, (newMode) => {
console.log('Mode update', newMode);
mode.value = newMode; mode.value = newMode;
}); });
@ -112,7 +111,7 @@ export const SbMain = defineComponent({
} }
<SbBlock <SbBlock
block={props.block} block={props.block}
onUpdate={props.onUpdate} eventUpdate={props.eventUpdate}
/> />
</div> </div>
); );

View file

@ -1,3 +1,5 @@
.sb-main-menu { .sb-main-menu {
display: flex; display: flex;
padding-bottom: 4rem;
background-color: var(--grey-0);
} }

View file

@ -2,7 +2,7 @@
&__overlay { &__overlay {
background-color: var(--grey-3-t); background-color: var(--grey-3-t);
position: fixed; position: fixed;
z-index: 10; z-index: var(--z-modal);
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;

View file

@ -4,4 +4,5 @@
height: auto; height: auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
z-index: var(--z-toolbar);
} }

View file

@ -1,6 +1,7 @@
.sb-tree-block-select { .sb-tree-block-select {
&__list { &__list {
list-style: none; list-style: none;
color: var(--fg);
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -9,9 +10,6 @@
} }
} }
&__node {
}
&__block { &__block {
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -23,7 +21,7 @@
border: 0; border: 0;
font: inherit; font: inherit;
color: inherit; color: inherit;
padding: 0.5rem 1rem; padding: 0.25rem 0.5rem;
width: 100%; width: 100%;
text-align: left; text-align: left;
} }

View file

@ -40,8 +40,9 @@ export const SbTreeBlockSelect = defineComponent({
} }
</li>; </li>;
return () => ( return () => (
blockTree.value console.log(blockTree.value) || blockTree.value
? <SbContextMenu ? <SbContextMenu
class="sb-tree-block-select" class="sb-tree-block-select"
v-slots={{ v-slots={{

View file

@ -10,6 +10,8 @@ import {
ITreeNode, ITreeNode,
IBlockData, IBlockData,
} from './types'; } from './types';
import { useDynamicBlocks } from './use-dynamic-blocks';
import { SbMode } from './mode';
export const SymBlockTree= Symbol('Schlechtenburg block tree'); export const SymBlockTree= Symbol('Schlechtenburg block tree');
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register'); export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
@ -17,8 +19,8 @@ export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregist
export function useBlockTree() { export function useBlockTree() {
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null)); const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
const registerWithParent = inject(SymBlockTreeRegister, (_: ITreeNode) => {}); const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
const unregisterWithParent = inject(SymBlockTreeUnregister, (_: ITreeNode) => {}); const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
const self: ITreeNode = reactive({ const self: ITreeNode = reactive({
id: '', id: '',
@ -28,14 +30,20 @@ export function useBlockTree() {
}); });
// Provide a registration function to child blocks // Provide a registration function to child blocks
provide(SymBlockTreeRegister, (block: ITreeNode) => { provide(SymBlockTreeRegister, (block: ITreeNode, index:number = -1) => {
if (self.children.find((child: ITreeNode) => child.id === block.id)) { if (self.children.find((child: ITreeNode) => child.id === block.id)) {
return; return;
} }
if (index < 0) {
}
const normalizedIndex = index < 0 ? 0 : index;
self.children =[ self.children =[
...self.children, ...self.children.slice(0, normalizedIndex),
block, block,
...self.children.slice(normalizedIndex),
]; ];
}); });
@ -44,16 +52,23 @@ export function useBlockTree() {
self.children = self.children.filter((child: ITreeNode) => child.id !== id); self.children = self.children.filter((child: ITreeNode) => child.id !== id);
}); });
const register = (block: IBlockData<any>) => { const { mode } = useDynamicBlocks();
const register = (block: IBlockData<any>, index: number = 0) => {
if (!block.id) { if (!block.id) {
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`); throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
} }
if (mode.value !== SbMode.Edit) {
console.warn('Ignoring block tree registration requests outside of edit mode.');
return;
}
self.id = block.id; self.id = block.id;
self.name = block.name; self.name = block.name;
// Register ourselves at the parent block // Register ourselves at the parent block
registerWithParent(self); registerWithParent(self, index);
} }
// Unregister from parent when we get destroyed // Unregister from parent when we get destroyed

View file

@ -39,23 +39,23 @@ export default defineComponent({
type: (null as unknown) as PropType<IHeadingData>, type: (null as unknown) as PropType<IHeadingData>,
default: getDefaultData, default: getDefaultData,
}, },
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<OnUpdateSelfCb<IHeadingData>>, type: (null as unknown) as PropType<OnUpdateSelfCb<IHeadingData>>,
default: () => {}, default: () => {},
}, },
onAppendBlock: { eventAppendBlock: {
type: (null as unknown) as PropType<OnAppendBlockCb>, type: (null as unknown) as PropType<OnAppendBlockCb>,
default: () => {}, default: () => {},
}, },
onRemoveSelf: { eventRemoveSelf: {
type: (null as unknown) as PropType<OnRemoveSelfCb>, type: (null as unknown) as PropType<OnRemoveSelfCb>,
default: () => {}, default: () => {},
}, },
onActivateNext: { eventActivateNext: {
type: (null as unknown) as PropType<OnActivateNextCb>, type: (null as unknown) as PropType<OnActivateNextCb>,
default: () => {}, default: () => {},
}, },
onActivatePrevious: { eventActivatePrevious: {
type: (null as unknown) as PropType<OnActivatePreviousCb>, type: (null as unknown) as PropType<OnActivatePreviousCb>,
default: () => {}, default: () => {},
}, },
@ -114,14 +114,14 @@ export default defineComponent({
})); }));
const setLevel = ($event: Event) => { const setLevel = ($event: Event) => {
props.onUpdate({ props.eventUpdate({
...localData, ...localData,
level: parseInt(($event.target as HTMLSelectElement).value, 10), level: parseInt(($event.target as HTMLSelectElement).value, 10),
}); });
}; };
const setAlignment = ($event: Event) => { const setAlignment = ($event: Event) => {
props.onUpdate({ props.eventUpdate({
...localData, ...localData,
align: ($event.target as HTMLSelectElement).value, align: ($event.target as HTMLSelectElement).value,
}); });
@ -134,7 +134,7 @@ export default defineComponent({
const onBlur = () => { const onBlur = () => {
localData.focused = false; localData.focused = false;
props.onUpdate({ props.eventUpdate({
value: localData.value, value: localData.value,
align: localData.align, align: localData.align,
level: localData.level, level: localData.level,
@ -144,7 +144,7 @@ export default defineComponent({
const onKeydown = ($event: KeyboardEvent) => { const onKeydown = ($event: KeyboardEvent) => {
if ($event.key === 'Enter' && !$event.shiftKey) { if ($event.key === 'Enter' && !$event.shiftKey) {
const id = generateBlockId(); const id = generateBlockId();
props.onAppendBlock({ props.eventAppendBlock({
id, id,
name: 'sb-paragraph', name: 'sb-paragraph',
data: getDefaultParagraphData(), data: getDefaultParagraphData(),
@ -158,7 +158,7 @@ export default defineComponent({
const onKeyup = ($event: KeyboardEvent) => { const onKeyup = ($event: KeyboardEvent) => {
if ($event.key === 'Backspace' && localData.value === '') { if ($event.key === 'Backspace' && localData.value === '') {
props.onRemoveSelf(); props.eventRemoveSelf();
} }
const selection = window.getSelection(); const selection = window.getSelection();
@ -168,10 +168,10 @@ export default defineComponent({
if (node === inputEl.value || index === 0 || index === childNodes.length -1) { if (node === inputEl.value || index === 0 || index === childNodes.length -1) {
switch ($event.key) { switch ($event.key) {
case 'ArrowDown': case 'ArrowDown':
props.onActivateNext(); props.eventActivateNext();
break; break;
case 'ArrowUp': case 'ArrowUp':
props.onActivatePrevious(); props.eventActivatePrevious();
break; break;
} }
} }

View file

@ -1,6 +1,7 @@
.sb-heading { .sb-heading {
flex-basis: 100%; flex-basis: 100%;
font-weight: bold; font-weight: bold;
line-height: 1.2;
&_1 { &_1 {
font-size: 4rem; font-size: 4rem;

View file

@ -28,7 +28,7 @@ export default defineComponent({
model, model,
props: { props: {
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<OnUpdateSelfCb<IImageData>>, type: (null as unknown) as PropType<OnUpdateSelfCb<IImageData>>,
default: () => {}, default: () => {},
}, },
@ -68,7 +68,7 @@ export default defineComponent({
throw new Error('Couldn\'t load image src'); throw new Error('Couldn\'t load image src');
} }
props.onUpdate({ props.eventUpdate({
src, src,
alt: props.data.alt, alt: props.data.alt,
description: props.data.description, description: props.data.description,
@ -80,7 +80,7 @@ export default defineComponent({
}; };
const onDescriptionUpdate = (description: IBlockData<IParagraphData>) => { const onDescriptionUpdate = (description: IBlockData<IParagraphData>) => {
props.onUpdate({ props.eventUpdate({
...props.data, ...props.data,
description, description,
}); });

View file

@ -31,7 +31,7 @@ export default defineComponent({
model, model,
props: { props: {
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<OnUpdateSelfCb<ILayoutData>>, type: (null as unknown) as PropType<OnUpdateSelfCb<ILayoutData>>,
default: () => {}, default: () => {},
}, },
@ -60,7 +60,7 @@ export default defineComponent({
})); }));
const toggleOrientation = () => { const toggleOrientation = () => {
props.onUpdate({ props.eventUpdate({
orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical', orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical',
}); });
}; };
@ -70,7 +70,7 @@ export default defineComponent({
if (index === -1) { if (index === -1) {
return; return;
} }
props.onUpdate({ props.eventUpdate({
children: [ children: [
...localData.children.slice(0, index), ...localData.children.slice(0, index),
{ {
@ -87,7 +87,7 @@ export default defineComponent({
...localData.children, ...localData.children,
block, block,
]; ];
props.onUpdate({ children: [...localData.children] }); props.eventUpdate({ children: [...localData.children] });
activate(block.id); activate(block.id);
}; };
@ -97,7 +97,7 @@ export default defineComponent({
block, block,
...localData.children.slice(index + 1), ...localData.children.slice(index + 1),
]; ];
props.onUpdate({ children: [...localData.children] }); props.eventUpdate({ children: [...localData.children] });
activate(block.id); activate(block.id);
}; };
@ -106,7 +106,7 @@ export default defineComponent({
...localData.children.slice(0, index), ...localData.children.slice(0, index),
...localData.children.slice(index + 1), ...localData.children.slice(index + 1),
]; ];
props.onUpdate({ children: [...localData.children] }); props.eventUpdate({ children: [...localData.children] });
const newActiveIndex = Math.max(index - 1, 0); const newActiveIndex = Math.max(index - 1, 0);
activate(localData.children[newActiveIndex].id); activate(localData.children[newActiveIndex].id);
@ -138,7 +138,7 @@ export default defineComponent({
...localData.children.slice(index + 1), ...localData.children.slice(index + 1),
]; ];
props.onUpdate({ children: [...localData.children] }); props.eventUpdate({ children: [...localData.children] });
}; };
const moveForward = (index: number) => { const moveForward = (index: number) => {
@ -155,7 +155,7 @@ export default defineComponent({
...localData.children.slice(index + 2), ...localData.children.slice(index + 2),
]; ];
props.onUpdate({ children: [...localData.children] }); props.eventUpdate({ children: [...localData.children] });
}; };
return () => ( return () => (
@ -174,20 +174,21 @@ export default defineComponent({
<SbBlock <SbBlock
{...{ key: child.id }} {...{ key: child.id }}
data-order={index} data-order={index}
indexAsChild={index}
block={child} block={child}
onUpdate={(updated: IBlockData<any>) => onChildUpdate(child, updated)} eventUpdate={(updated: IBlockData<any>) => onChildUpdate(child, updated)}
onRemoveSelf={() => removeBlock(index)} eventRemoveSelf={() => removeBlock(index)}
onPrependBlock={(block: IBlockData<any>) => insertBlock(index - 1, block)} eventPrependBlock={(block: IBlockData<any>) => insertBlock(index - 1, block)}
onAppendBlock={(block: IBlockData<any>) => insertBlock(index, block)} eventAppendBlock={(block: IBlockData<any>) => insertBlock(index, block)}
onActivatePrevious={() => activateBlock(index - 1,)} eventActivatePrevious={() => activateBlock(index - 1,)}
onActivateNext={() => activateBlock(index + 1,)} eventActivateNext={() => activateBlock(index + 1,)}
> >
{{ {{
'context-toolbar': () => 'context-toolbar': () =>
<SbBlockOrdering <SbBlockOrdering
onMoveBackward={() => moveBackward(index)} eventMoveBackward={() => moveBackward(index)}
onMoveForward={() => moveForward(index)} eventMoveForward={() => moveForward(index)}
onRemove={() => removeBlock(index)} eventRemove={() => removeBlock(index)}
orientation={localData.orientation} orientation={localData.orientation}
/>, />,
}} }}
@ -195,7 +196,7 @@ export default defineComponent({
))} ))}
</> </>
<SbBlockPlaceholder onInsertBlock={appendBlock}></SbBlockPlaceholder> <SbBlockPlaceholder eventInsertBlock={appendBlock}></SbBlockPlaceholder>
</div> </div>
); );
}, },

View file

@ -0,0 +1,11 @@
export const isEmptyContentEditable = (value:string) => {
if (!value) {
return true;
}
if (value === '<br>') {
return true;
}
return false;
}

View file

@ -15,6 +15,7 @@ import {
SbSelect, SbSelect,
generateBlockId, generateBlockId,
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { isEmptyContentEditable } from './contenteditable';
import { import {
getDefaultData, getDefaultData,
IParagraphData, IParagraphData,
@ -33,23 +34,23 @@ export default defineComponent({
type: (null as unknown) as PropType<IParagraphData>, type: (null as unknown) as PropType<IParagraphData>,
default: getDefaultData, default: getDefaultData,
}, },
onUpdate: { eventUpdate: {
type: (null as unknown) as PropType<((block?: Partial<IParagraphData>) => void)>, type: (null as unknown) as PropType<((block?: Partial<IParagraphData>) => void)>,
default: () => {}, default: () => {},
}, },
onAppendBlock: { eventAppendBlock: {
type: (null as unknown) as PropType<((block?: any) => void)>, type: (null as unknown) as PropType<((block?: any) => void)>,
default: () => {}, default: () => {},
}, },
onRemoveSelf: { eventRemoveSelf: {
type: (null as unknown) as PropType<() => void>, type: (null as unknown) as PropType<() => void>,
default: () => {}, default: () => {},
}, },
onActivateNext: { eventActivateNext: {
type: (null as unknown) as PropType<() => void>, type: (null as unknown) as PropType<() => void>,
default: () => {}, default: () => {},
}, },
onActivatePrevious: { eventActivatePrevious: {
type: (null as unknown) as PropType<() => void>, type: (null as unknown) as PropType<() => void>,
default: () => {}, default: () => {},
}, },
@ -104,7 +105,7 @@ export default defineComponent({
})); }));
const setAlignment = ($event: Event) => { const setAlignment = ($event: Event) => {
props.onUpdate({ props.eventUpdate({
value: localData.value, value: localData.value,
align: ($event.target as HTMLSelectElement).value, align: ($event.target as HTMLSelectElement).value,
}); });
@ -117,7 +118,7 @@ export default defineComponent({
const onBlur = () => { const onBlur = () => {
localData.focused = false; localData.focused = false;
props.onUpdate({ props.eventUpdate({
value: localData.value, value: localData.value,
align: localData.align, align: localData.align,
}); });
@ -126,7 +127,7 @@ export default defineComponent({
const onKeydown = ($event: KeyboardEvent) => { const onKeydown = ($event: KeyboardEvent) => {
if ($event.key === 'Enter' && !$event.shiftKey) { if ($event.key === 'Enter' && !$event.shiftKey) {
const id = generateBlockId(); const id = generateBlockId();
props.onAppendBlock({ props.eventAppendBlock({
id, id,
name: 'sb-paragraph', name: 'sb-paragraph',
data: getDefaultData(), data: getDefaultData(),
@ -139,8 +140,8 @@ export default defineComponent({
}; };
const onKeyup = ($event: KeyboardEvent) => { const onKeyup = ($event: KeyboardEvent) => {
if ($event.key === 'Backspace' && localData.value === '') { if ($event.key === 'Backspace' && isEmptyContentEditable(localData.value)) {
props.onRemoveSelf(); props.eventRemoveSelf();
} }
const selection = window.getSelection(); const selection = window.getSelection();
@ -150,10 +151,10 @@ export default defineComponent({
if (node === inputEl.value || index === 0 || index === childNodes.length -1) { if (node === inputEl.value || index === 0 || index === childNodes.length -1) {
switch ($event.key) { switch ($event.key) {
case 'ArrowDown': case 'ArrowDown':
props.onActivateNext(); props.eventActivateNext();
break; break;
case 'ArrowUp': case 'ArrowUp':
props.onActivatePrevious(); props.eventActivatePrevious();
break; break;
} }
} }