diff --git a/src/App.tsx b/src/App.tsx index 153637c..a12aaea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,13 +12,16 @@ export default defineComponent({ setup() { const block = reactive({ - block: 'sb-layout', - orientation: 'vertical', - children: [], + name: 'sb-layout', + blockId: `${+(new Date())}`, + data: { + orientation: 'vertical', + children: [], + }, }); watchEffect(() => { - console.log(block); + console.log('base block update', block); }); return { block }; diff --git a/src/components/Schlechtenburg.tsx b/src/components/Schlechtenburg.tsx index 41a03cf..d9a246d 100644 --- a/src/components/Schlechtenburg.tsx +++ b/src/components/Schlechtenburg.tsx @@ -1,9 +1,21 @@ import { defineComponent, + provide, computed, reactive, + ref, + PropType, } from '@vue/composition-api'; -import { model, useDynamicBlocks } from '@components/TreeElement'; +import { + model, + ActiveBlock, + BlockProps, + BlockDefinition, + BlockLibraryDefinition, + BlockLibrary, +} from '@components/TreeElement'; + +import SbBlock from '@internal/Block'; export default defineComponent({ name: 'schlechtenburg-main', @@ -11,29 +23,55 @@ export default defineComponent({ model, props: { + customBlocks: { type: (null as unknown) as PropType, default: () => [] }, block: { type: Object, required: true }, }, - setup(props, context) { - const { getBlock } = useDynamicBlocks(context); + setup(props: BlockProps) { + const activeBlock = ref(null); + provide(ActiveBlock, activeBlock); - return { - getBlock, - }; + const blockLibrary: BlockLibraryDefinition = reactive({ + 'sb-layout': { + name: 'sb-layout', + edit: () => import('@user/Layout'), + display: () => import('@user/Layout'), + }, + 'sb-image': { + name: 'sb-image', + edit: () => import('@user/Image'), + display: () => import('@user/Image'), + }, + 'sb-paragraph': { + name: 'sb-paragraph', + edit: () => import('@user/Paragraph'), + display: () => import('@user/Paragraph'), + }, + 'sb-heading': { + name: 'sb-heading', + edit: () => import('@user/Heading'), + display: () => import('@user/Heading'), + }, + ...props.customBlocks.reduce( + ( + blocks: BlockLibraryDefinition, + block: BlockLibraryDefinition, + ) => ({ ...blocks, [block.name]: block }), + {}, + ), + }); + provide(BlockLibrary, blockLibrary); }, render() { - const Block = this.getBlock(this.block.name); - console.log(this.name, Block); + console.log('render base'); return ( - this.$emit('block-update', block), + update: (block: BlockDefinition) => this.$emit('update', block), }, }} /> diff --git a/src/components/TreeElement.ts b/src/components/TreeElement.ts index 01295b5..b841466 100644 --- a/src/components/TreeElement.ts +++ b/src/components/TreeElement.ts @@ -1,34 +1,61 @@ import { - Context, + Ref, + ref, + inject, + reactive, + computed, } from '@vue/composition-api'; -type IComponentDefinition = { [name: string]: () => Promise }; +export const ActiveBlock = Symbol('Schlechtenburg active block'); +export const BlockLibrary = Symbol('Schlechtenburg block library'); -type IBlockData = { +export interface BlockDefinition { name: string; - id: string; + edit: () => Promise; + display: () => Promise; +} + +export interface BlockLibraryDefinition { + [name: string]: BlockDefinition; +} + +export interface BlockData { + name: string; + blockId: string|number; data: { [name: string]: any }; } -type ITreeElementProps = { - id: string; +export interface BlockProps { + blockId: string|number; data: { [key: string]: any}; -}; +} export const model = { prop: 'block', - event: 'block-update', + event: 'update', }; export const blockProps = { - id: { type: String, required: true }, + blockId: { type: [String, Number], required: true }, data: { type: Object, default: () => ({}) }, }; -// export function useActivation +export function useDynamicBlocks() { + const customBlocks: BlockLibraryDefinition = inject(BlockLibrary, reactive({})); + const getBlock = (name: string) => customBlocks[name]; -export function useDynamicBlocks(context: Context) { - const getBlock = (name: string) => context.root.$sb.blocks[name]; - - return { getBlock }; + return { customBlocks, getBlock }; +} + +export function useActivation(currentBlockId: string|number) { + const activeBlockId: Ref = inject(ActiveBlock, ref(null)); + const isActive = computed(() => activeBlockId.value === currentBlockId); + const activate = (blockId?: string|number|null) => { + activeBlockId.value = blockId !== undefined ? blockId : currentBlockId; + }; + + return { + isActive, + activate, + }; } diff --git a/src/components/internal/Block.tsx b/src/components/internal/Block.tsx index 6b78acb..586cfff 100644 --- a/src/components/internal/Block.tsx +++ b/src/components/internal/Block.tsx @@ -1,4 +1,14 @@ -import { computed, defineComponent } from '@vue/composition-api'; +import { + computed, + defineComponent, + PropType, +} from '@vue/composition-api'; +import { + BlockData, + useDynamicBlocks, + useActivation, + BlockDefinition, +} from '@components/TreeElement'; import './Block.scss'; @@ -6,43 +16,51 @@ export default defineComponent({ name: 'sb-block', props: { - active: { type: Boolean, default: false }, + block: { type: (null as unknown) as PropType, default: false }, }, setup(props, context) { + const { isActive, activate } = useActivation(props.block.blockId); + const { getBlock } = useDynamicBlocks(); const classes = computed(() => ({ 'sb-block': true, - 'sb-block_active': props.active, + 'sb-block_active': isActive, })); - const activate = () => { - if (props.active) { - return; - } - - context.emit('activate'); + const onChildUpdate = (updated: {[key: string]: any}) => { + console.log('child update', updated); + context.emit('update', { + ...props.block, + data: { + ...props.block.data, + ...updated, + }, + }); }; return { + getBlock, classes, activate, + onChildUpdate, }; }, render() { - return ( -
- {this.$slots.toolbar} - {this.$slots.default ? this.$slots.default :
Your content here
} -
- ); + console.log('render block', this.block); + const Block = this.getBlock(this.block.name).edit; + return this.$emit('insert-block', block), + 'append-block': (block: BlockDefinition) => this.$emit('append-block', block), + }, + }} + />; }, }); diff --git a/src/components/internal/BlockChooser.tsx b/src/components/internal/BlockPicker.tsx similarity index 56% rename from src/components/internal/BlockChooser.tsx rename to src/components/internal/BlockPicker.tsx index 1314301..4815397 100644 --- a/src/components/internal/BlockChooser.tsx +++ b/src/components/internal/BlockPicker.tsx @@ -1,25 +1,20 @@ import { defineComponent } from '@vue/composition-api'; -import { treeElementProps, useDynamicComponents } from './TreeElement'; +import { useDynamicComponents } from './TreeElement'; export default defineComponent({ - props: { - ...treeElementProps, - orientation: String, - }, + props: {}, setup(props) { - const getComponent = useDynamicComponents(props.components || {}); + const { customBlocks } = useDynamicComponents(props.components || {}); return { - getComponent, + customBlocks, }; }, render() { return ( -
- {{ orientation }} - +
this.$emit('add-block', { - component: 'sb-paragraph', - id: +(new Date()), - value: '', + click: () => this.$emit('insert-block', { + name: 'sb-paragraph', + blockId: +(new Date()), + data: { + value: '', + }, }), }, }} diff --git a/src/components/user/Layout.tsx b/src/components/user/Layout.tsx index f06f558..468166d 100644 --- a/src/components/user/Layout.tsx +++ b/src/components/user/Layout.tsx @@ -1,12 +1,16 @@ import { + inject, reactive, + computed, defineComponent, - watchEffect, + watch, } from '@vue/composition-api'; import { model, - treeElementProps, - useDynamicComponents, + blockProps, + useDynamicBlocks, + useActivation, + BlockData, } from '@components/TreeElement'; import SbBlock from '@internal/Block'; @@ -21,51 +25,52 @@ export default defineComponent({ model, props: { - ...treeElementProps, + ...blockProps, }, - setup(props, context) { - const { getComponent } = useDynamicComponents(props.userComponents); + setup(props: BlockProps, context) { + const { getBlock } = useDynamicBlocks(); + const { isActive, activate } = useActivation(props.blockId); const localData = reactive({ - orientation: props.tree.data.orientation, - children: [...props.tree.data.children], + orientation: props.data.orientation, + children: [...props.data.children], }); - watchEffect(() => { - localData.orientation = props.tree.data.orientation; - localData.children = [...props.tree.data.children]; + watch(() => props.data, () => { + localData.orientation = props.data.orientation; + localData.children = [...props.data.children]; }); - const classes = { + const classes = computed(() => ({ 'sb-layout': true, + 'sb-layout_active': isActive, [`sb-layout_${localData.orientation}`]: true, - }; + })); const toggleOrientation = () => { - context.emit('data', { - id: props.blockId, - - ...localData, + context.emit('update', { orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical', }); }; const onChildUpdate = (child, updated) => { const index = localData.children.indexOf(child); - context.emit('data', { - ...localData, + context.emit('update', { children: [ ...localData.children.slice(0, index), - updated, + { + ...child, + ...updated, + }, ...localData.children.slice(index + 1), ], }); }; - const addBlock = (block) => { - context.emit('tree', { - ...localData, + const appendBlock = (block: BlockData) => { + console.log('append block', block); + context.emit('update', { children: [ ...localData.children, block, @@ -73,19 +78,35 @@ export default defineComponent({ }); }; + const insertBlock = (index: number, block: BlockData) => { + console.log('insert block', index, block); + context.emit('update', { + children: [ + ...localData.children.slice(0, index + 1), + block, + ...localData.children.slice(index + 1), + ], + }); + }; + return { + isActive, + activate, + classes, onChildUpdate, toggleOrientation, localData, - getComponent, - addBlock, + getBlock, + appendBlock, + insertBlock, }; }, render() { + console.log('render layout'); return ( - +
+ >{this.localData.orientation} - {...this.localTree.children.map((child) => { - const Component = this.getComponent(child.component); - return ( + this.onChildUpdate(child, updated), + update: (updated) => this.onChildUpdate(child, updated), + 'insert-block': (block: BlockDefinition) => this.insertBlock(index, block), + 'append-block': this.appendBlock, }, }} - />; - })} + /> + ))} + - +
); }, }); diff --git a/src/components/user/Paragraph.tsx b/src/components/user/Paragraph.tsx index b797f68..75eb6e7 100644 --- a/src/components/user/Paragraph.tsx +++ b/src/components/user/Paragraph.tsx @@ -2,12 +2,15 @@ import { defineComponent, reactive, ref, + Ref, onMounted, + watch, } from '@vue/composition-api'; import { model, - treeElementProps, - useDynamicComponents, + blockProps, + BlockProps, + useActivation, } from '@components/TreeElement'; import SbBlock from '@internal/Block'; @@ -21,56 +24,87 @@ export default defineComponent({ model, props: { - ...treeElementProps, + ...blockProps, }, - setup(props, context) { - const { localTree } = useTree(props); + setup(props: BlockProps, context) { + const localData = reactive({ + value: props.data.value, + focused: false, + }); + + const inputEl: Ref = ref(null); + + const { isActive, activate } = useActivation(props.blockId); + + onMounted(() => { + if (inputEl.value) { + inputEl.value.innerHTML = localData.value; + + if (isActive) { + inputEl.value.focus(); + } + } + }); + + watch(() => props.data, () => { + localData.value = props.data.value; + if (inputEl.value) { + inputEl.value.innerHTML = localData.value; + } + }); const onTextUpdate = ($event: InputEvent) => { - localTree.value = $event.target.innerHTML; + localData.value = $event.target.innerHTML; }; - const focused = ref(false); - const classes = reactive({ 'sb-paragraph': true, - 'sb-paragraph_focused': focused, + 'sb-paragraph_focused': localData.focused, }); const onFocus = () => { - console.log('focus'); - focused.value = true; + localData.focused = true; }; + const onBlur = () => { - console.log('blur'); - focused.value = false; - context.emit('tree', { - value: localTree.value.value, + localData.focused = false; + context.emit('update', { + value: localData.value, }); + activate(null); }; - const inputEl = ref(null); + const onKeypress = ($event: KeyboardEvent) => { + if ($event.key === 'Enter') { + const blockId = +(new Date()); + context.emit('insert-block', { + blockId, + name: 'sb-paragraph', + data: { value: '' }, + }); - onMounted(() => { - console.log(inputEl); - inputEl.value.innerHTML = localTree.value; - }); + activate(blockId); + + $event.preventDefault(); + } + }; return { classes, - localTree, + localData, onTextUpdate, - focused, onFocus, onBlur, + onKeypress, inputEl, }; }, render() { + console.log('render paragraph'); return ( - +
Paragraph editing

- +
); }, }); diff --git a/src/lib.ts b/src/lib.ts index 2da4afd..3b24345 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,11 +1,5 @@ /* eslint no-param-reassign: 0 */ -interface UserBlock { - name: string; - edit: () => Promise; - display: () => Promise; -} - function addUserBlock(Vue, block) { if (Vue.prototype.$sb.blocks[block.name]) { console.warn(`Block ${block.name} is already registered`); diff --git a/src/main.ts b/src/main.ts index 426692b..2ebffac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,19 +1,13 @@ import Vue from 'vue'; -import Vuex from 'vuex'; import VueCompositionApi from '@vue/composition-api'; -import VueSchlechtenburg from './lib'; import App from './App'; import './main.scss'; Vue.config.productionTip = false; -Vue.use(Vuex); -const store = new Vuex.Store({}); Vue.use(VueCompositionApi); -Vue.use(VueSchlechtenburg); new Vue({ - store, render: (h) => h(App), }).$mount('#app');