Automate block tree generation

This commit is contained in:
Benjamin Bädorf 2021-03-07 18:47:28 +01:00
parent 7d6a3730c6
commit 16e0ffdd99
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
15 changed files with 201 additions and 67 deletions

View file

@ -1,6 +1,7 @@
import { Component } from 'vue';
export interface BlockTree {
id: string;
name: string;
icon?: string;
children?: BlockTree[];
@ -20,7 +21,7 @@ export interface BlockLibraryDefinition {
}
export interface BlockProps {
blockId: string;
id: string;
data: any;
}
@ -34,7 +35,7 @@ export const model = {
};
export const blockProps = {
blockId: {
id: {
type: String,
default: () => `${+(new Date())}`,
},

View file

@ -20,7 +20,7 @@
}
&_active {
outline: 1px solid var(--grey-2);
outline: 4px solid var(--interact);
> * > .sb-toolbar {
opacity: 1;
@ -33,4 +33,8 @@
pointer-events: all;
}
}
&_highlighted {
outline: 2px solid var(--interact);
}
}

View file

@ -10,6 +10,7 @@ import { Block } from '../blocks';
import { SbMode } from '../mode';
import { useResizeObserver, BlockDimensions } from '../use-resize-observer';
import { useActivation } from '../use-activation';
import { useBlockTree } from '../use-block-tree';
import { useDynamicBlocks } from '../use-dynamic-blocks';
import { SbBlockOrdering } from './BlockOrdering';
@ -53,7 +54,11 @@ export const SbBlock = defineComponent({
setup(props: BlockProps, context) {
const el: Ref<null|HTMLElement> = ref(null);
const { mode, getBlock } = useDynamicBlocks();
const { isActive, activate } = useActivation(props.block.blockId);
const {
isActive,
activate,
isHighlighted,
} = useActivation(props.block.id);
const classes = computed(() => ({
'sb-block': true,
'sb-block_active': isActive.value,
@ -62,6 +67,10 @@ export const SbBlock = defineComponent({
const { triggerSizeCalculation } = useResizeObserver(el, BlockDimensions);
watch(() => props.block.data, triggerSizeCalculation);
const { register } = useBlockTree();
register(props.block);
watch(props.block, () => { register(props.block); });
const onChildUpdate = (updated: {[key: string]: any}) => {
props.onUpdate({
...props.block,
@ -79,14 +88,14 @@ export const SbBlock = defineComponent({
const MissingBlock = SbMissingBlock[mode.value];
return <MissingBlock
name={props.block.name}
blockId={props.block.blockId}
blockId={props.block.id}
/>;
}
if (mode.value === SbMode.Display) {
return <BlockComponent
data={props.block.data}
blockId={props.block.blockId}
blockId={props.block.id}
/>;
}
@ -98,7 +107,7 @@ export const SbBlock = defineComponent({
{context.slots['context-toolbar'] ? context.slots['context-toolbar']() : null}
<BlockComponent
data={props.block.data}
blockId={props.block.blockId}
blockId={props.block.id}
onUpdate={onChildUpdate}
onPrependBlock={props.onPrependBlock}
onAppendBlock={props.onAppendBlock}

View file

@ -28,7 +28,7 @@ export const SbBlockPicker = defineComponent({
open.value = false;
context.emit('picked-block', {
name: block.name,
blockId: `${+(new Date())}`,
id: `${+(new Date())}`,
data: block.getDefaultData(),
});
};

View file

@ -12,6 +12,10 @@
margin: 0;
z-index: var(--z-context-menu);
max-height: 70vh;
max-width: 100vw;
overflow: auto;
&[open] {
display: flex;
}

View file

@ -3,18 +3,19 @@ import {
provide,
shallowReactive,
ref,
watch,
PropType,
Ref,
} from 'vue';
import {
model,
Block,
BlockTree,
BlockDefinition,
BlockLibraryDefinition,
} from '../blocks';
import { Mode, SbMode } from '../mode';
import { BlockLibrary } from '../use-dynamic-blocks';
import { BlockTreeSym, BlockTreeRegister, BlockTreeUnregister } from '../use-block-tree';
import { EditorDimensions, useResizeObserver } from '../use-resize-observer';
import { ActiveBlock } from '../use-activation';
@ -58,6 +59,11 @@ export const Schlechtenburg = defineComponent({
const activeBlock = ref(null);
provide(ActiveBlock, activeBlock);
const blockTree = ref(null);
provide(BlockTreeSym, blockTree);
provide(BlockTreeRegister, (block: BlockTree) => { blockTree.value = block; });
provide(BlockTreeUnregister, () => { blockTree.value = null; });
const blockLibrary: BlockLibraryDefinition = shallowReactive({
...props.customBlocks.reduce(
(blocks: {[name: string]: Block<any>}, block: Block<any>) => ({ ...blocks, [block.name]: block }),

View file

@ -1,2 +1,38 @@
.sb-tree-block-select {
&__list {
list-style: none;
margin: 0;
padding: 0;
&_base {
padding-right: 1rem;
}
}
&__node {
}
&__block {
padding: 0;
margin: 0;
padding-left: 1rem;
&-name {
display: block;
background: transparent;
border: 0;
font: inherit;
color: inherit;
padding: 0.5rem 1rem;
width: 100%;
text-align: left;
}
&_active {
& > .sb-tree-block-select__block-name {
outline: 1px solid var(--interact);
}
}
}
}

View file

@ -1,5 +1,4 @@
import {
computed,
defineComponent,
PropType,
} from 'vue';
@ -7,7 +6,8 @@ import {
Block,
BlockTree,
} from '../blocks';
import { useDynamicBlocks } from '../use-dynamic-blocks';
import { useBlockTree } from '../use-block-tree';
import { useActivation } from '../use-activation';
import { SbContextMenu } from './ContextMenu';
import { SbButton } from './Button';
@ -21,42 +21,47 @@ interface TreeBlockSelectProps {
export const SbTreeBlockSelect = defineComponent({
name: 'sb-main-menu',
props: {
block: {
type: (null as unknown) as PropType<Block>,
required: true,
},
},
setup() {
const { blockTree } = useBlockTree();
const {
activate,
activeBlockId,
} = useActivation();
setup(props: TreeBlockSelectProps, context) {
const { getBlock } = useDynamicBlocks();
const getTreeForBlock = (block: Block): BlockTree => {
const getBlockChildren = getBlock(block.name)?.getChildren;
// TODO: vue-jxs apparently cannot parse arrow functions here
const getChildren = getBlockChildren || function ({ data }) { return data?.children; };
const children = getChildren(block) || [];
return {
name: block.name,
children: children.map(getTreeForBlock),
};
};
const tree = computed(() => getTreeForBlock(props.block));
const treeToHtml = (tree: BlockTree) => <li>
{tree.name}
{tree.children.length ? <ul>{tree.children.map(treeToHtml)}</ul> : null}
const treeToHtml = (tree: BlockTree, close: Function) => <li
class={{
'sb-tree-block-select__block': true,
'sb-tree-block-select__block_active': activeBlockId.value === tree.id,
}}
>
<button
class="sb-tree-block-select__block-name"
onClick={() => {
activate(tree.id);
close();
}}
onMouseEnter={() => activate(tree.id)}
>{tree.name}</button>
{tree.children.length
? <ul class="sb-tree-block-select__list">
{tree.children.map((child: BlockTree) => treeToHtml(child, close))}
</ul>
: null
}
</li>;
return () => (
<SbContextMenu
blockTree.value
? <SbContextMenu
class="sb-tree-block-select"
v-slots={{
context: ({ toggle }) => <SbButton onClick={toggle}>Tree</SbButton>,
default: () => <ul>{treeToHtml(tree.value)}</ul>,
default: ({ close }) => <ul
class="sb-tree-block-select__list sb-tree-block-select__list_base"
>{treeToHtml(blockTree.value, close)}</ul>,
}}
/>
: ''
);
},
});

View file

@ -6,11 +6,11 @@ import {
} from 'vue';
export const ActiveBlock = Symbol('Schlechtenburg active block');
export function useActivation(currentBlockId: string) {
export function useActivation(currentBlockId?: string) {
const activeBlockId: Ref<string|null> = inject(ActiveBlock, ref(null));
const isActive = computed(() => activeBlockId.value === currentBlockId);
const activate = (blockId?: string|null) => {
activeBlockId.value = blockId !== undefined ? blockId : currentBlockId;
const activate = (id?: string|null) => {
activeBlockId.value = id !== undefined ? id : currentBlockId;
};
const requestActivation = () => {
if (activeBlockId.value) {
@ -21,6 +21,7 @@ export function useActivation(currentBlockId: string) {
};
return {
activeBlockId,
isActive,
activate,
requestActivation,

View file

@ -0,0 +1,68 @@
import {
Ref,
reactive,
inject,
provide,
onUnmounted,
} from 'vue';
import {
BlockTree,
Block,
} from './blocks';
export const BlockTreeSym = Symbol('Schlechtenburg block tree');
export const BlockTreeRegister = Symbol('Schlechtenburg block tree');
export const BlockTreeUnregister = Symbol('Schlechtenburg block tree');
export function useBlockTree() {
const blockTree: Ref<BlockTree|null> = inject(BlockTreeSym, null);
const registerWithParent = inject(BlockTreeRegister, (_: BlockTree) => {});
const unregisterWithParent = inject(BlockTreeUnregister, (_: BlockTree) => {});
const self: BlockTree= reactive({
id: '',
name: '',
icon: '',
children: [],
});
// Provide a registration function to child blocks
provide(BlockTreeRegister, (block: BlockTree) => {
if (self.children.find((child: BlockTree) => child.id === block.id)) {
return;
}
self.children = [
...self.children,
block,
];
});
// Provide an unregistration function to child blocks
provide(BlockTreeUnregister, ({ id }: BlockTree) => {
self.children = self.children.filter((child: BlockTree) => child.id !== id);
});
const register = (block: Block) => {
if (!block.id) {
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
}
self.id = block.id;
self.name = block.name;
// Register ourselves at the parent block
registerWithParent(self);
}
// Unregister from parent when we get destroyed
onUnmounted(() => {
if (self.id) {
unregisterWithParent(self);
}
});
return {
blockTree,
register,
};
}

View file

@ -40,7 +40,7 @@ export default defineComponent({
<div class={classes.value}>
{...props.data.children.map((child) => (
<SbBlock
key={child.blockId}
key={child.id}
block={child}
/>
))}

View file

@ -41,7 +41,7 @@ export default defineComponent({
},
setup(props: LayoutProps) {
const { activate } = useActivation(props.blockId);
const { activate } = useActivation(props.id);
const localData: LayoutData = reactive({
orientation: props.data.orientation,
@ -88,7 +88,7 @@ export default defineComponent({
block,
];
props.onUpdate({ children: [...localData.children] });
activate(block.blockId);
activate(block.id);
};
const insertBlock = (index: number, block: Block) => {
@ -98,7 +98,7 @@ export default defineComponent({
...localData.children.slice(index + 1),
];
props.onUpdate({ children: [...localData.children] });
activate(block.blockId);
activate(block.id);
};
const removeBlock = (index: number) => {
@ -109,7 +109,7 @@ export default defineComponent({
props.onUpdate({ children: [...localData.children] });
const newActiveIndex = Math.max(index - 1, 0);
activate(localData.children[newActiveIndex].blockId);
activate(localData.children[newActiveIndex].id);
};
const activateBlock = (index: number) => {
@ -121,7 +121,7 @@ export default defineComponent({
),
0,
);
activate(localData.children[safeIndex].blockId);
activate(localData.children[safeIndex].id);
};
const moveBackward = (index: number) => {
@ -169,7 +169,7 @@ export default defineComponent({
{...localData.children.map((child, index) => (
<SbBlock
{...{ key: child.blockId }}
{...{ key: child.id }}
data-order={index}
block={child}
onUpdate={(updated: Block) => onChildUpdate(child, updated)}

View file

@ -123,14 +123,14 @@ export default defineComponent({
const onKeydown = ($event: KeyboardEvent) => {
if ($event.key === 'Enter' && !$event.shiftKey) {
const blockId = `${+(new Date())}`;
const id = `${+(new Date())}`;
props.onAppendBlock({
blockId,
id,
name: 'sb-paragraph',
data: getDefaultData(),
});
activate(blockId);
activate(id);
$event.preventDefault();
}

File diff suppressed because one or more lines are too long

View file

@ -22,7 +22,7 @@ export default defineComponent({
const activeTab = ref('edit');
const block: Block<any> = reactive({
name: 'none',
blockId: '0',
id: '0',
data: null,
});
@ -30,7 +30,7 @@ export default defineComponent({
const res = await fetch('/initial-data.json');
const data = await res.json();
block.name = data.name;
block.blockId = data.blockId;
block.id = data.id;
block.data = data.data;
});