Automate block tree generation
This commit is contained in:
parent
7d6a3730c6
commit
16e0ffdd99
|
@ -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())}`,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
margin: 0;
|
||||
z-index: var(--z-context-menu);
|
||||
|
||||
max-height: 70vh;
|
||||
max-width: 100vw;
|
||||
overflow: auto;
|
||||
|
||||
&[open] {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
class="sb-tree-block-select"
|
||||
v-slots={{
|
||||
context: ({ toggle }) => <SbButton onClick={toggle}>Tree</SbButton>,
|
||||
default: () => <ul>{treeToHtml(tree.value)}</ul>,
|
||||
}}
|
||||
/>
|
||||
blockTree.value
|
||||
? <SbContextMenu
|
||||
class="sb-tree-block-select"
|
||||
v-slots={{
|
||||
context: ({ toggle }) => <SbButton onClick={toggle}>Tree</SbButton>,
|
||||
default: ({ close }) => <ul
|
||||
class="sb-tree-block-select__list sb-tree-block-select__list_base"
|
||||
>{treeToHtml(blockTree.value, close)}</ul>,
|
||||
}}
|
||||
/>
|
||||
: ''
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
68
packages/core/lib/use-block-tree.ts
Normal file
68
packages/core/lib/use-block-tree.ts
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue