Providing/injecting, adding blocks

This commit is contained in:
Benjamin Bädorf 2020-05-24 17:33:25 +02:00
parent 2153b06b2d
commit d49ea3e99f
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
11 changed files with 269 additions and 151 deletions

View file

@ -12,13 +12,16 @@ export default defineComponent({
setup() { setup() {
const block = reactive({ const block = reactive({
block: 'sb-layout', name: 'sb-layout',
blockId: `${+(new Date())}`,
data: {
orientation: 'vertical', orientation: 'vertical',
children: [], children: [],
},
}); });
watchEffect(() => { watchEffect(() => {
console.log(block); console.log('base block update', block);
}); });
return { block }; return { block };

View file

@ -1,9 +1,21 @@
import { import {
defineComponent, defineComponent,
provide,
computed, computed,
reactive, reactive,
ref,
PropType,
} from '@vue/composition-api'; } 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({ export default defineComponent({
name: 'schlechtenburg-main', name: 'schlechtenburg-main',
@ -11,29 +23,55 @@ export default defineComponent({
model, model,
props: { props: {
customBlocks: { type: (null as unknown) as PropType<BlockDefinition[]>, default: () => [] },
block: { type: Object, required: true }, block: { type: Object, required: true },
}, },
setup(props, context) { setup(props: BlockProps) {
const { getBlock } = useDynamicBlocks(context); const activeBlock = ref(null);
provide(ActiveBlock, activeBlock);
return { const blockLibrary: BlockLibraryDefinition = reactive({
getBlock, '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() { render() {
const Block = this.getBlock(this.block.name); console.log('render base');
console.log(this.name, Block);
return ( return (
<Component <SbBlock
class="sb-main" class="sb-main"
user-components={this.components} block={this.block}
data={this.block.data}
id={this.block.id}
{...{ {...{
on: { on: {
blockUpdate: (block) => this.$emit('block-update', block), update: (block: BlockDefinition) => this.$emit('update', block),
}, },
}} }}
/> />

View file

@ -1,34 +1,61 @@
import { import {
Context, Ref,
ref,
inject,
reactive,
computed,
} from '@vue/composition-api'; } from '@vue/composition-api';
type IComponentDefinition = { [name: string]: () => Promise<any> }; export const ActiveBlock = Symbol('Schlechtenburg active block');
export const BlockLibrary = Symbol('Schlechtenburg block library');
type IBlockData = { export interface BlockDefinition {
name: string; name: string;
id: string; edit: () => Promise<any>;
display: () => Promise<any>;
}
export interface BlockLibraryDefinition {
[name: string]: BlockDefinition;
}
export interface BlockData {
name: string;
blockId: string|number;
data: { [name: string]: any }; data: { [name: string]: any };
} }
type ITreeElementProps = { export interface BlockProps {
id: string; blockId: string|number;
data: { [key: string]: any}; data: { [key: string]: any};
}; }
export const model = { export const model = {
prop: 'block', prop: 'block',
event: 'block-update', event: 'update',
}; };
export const blockProps = { export const blockProps = {
id: { type: String, required: true }, blockId: { type: [String, Number], required: true },
data: { type: Object, default: () => ({}) }, 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) { return { customBlocks, getBlock };
const getBlock = (name: string) => context.root.$sb.blocks[name]; }
return { getBlock }; export function useActivation(currentBlockId: string|number) {
const activeBlockId: Ref<string|number|null> = 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,
};
} }

View file

@ -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'; import './Block.scss';
@ -6,43 +16,51 @@ export default defineComponent({
name: 'sb-block', name: 'sb-block',
props: { props: {
active: { type: Boolean, default: false }, block: { type: (null as unknown) as PropType<BlockData>, default: false },
}, },
setup(props, context) { setup(props, context) {
const { isActive, activate } = useActivation(props.block.blockId);
const { getBlock } = useDynamicBlocks();
const classes = computed(() => ({ const classes = computed(() => ({
'sb-block': true, 'sb-block': true,
'sb-block_active': props.active, 'sb-block_active': isActive,
})); }));
const activate = () => { const onChildUpdate = (updated: {[key: string]: any}) => {
if (props.active) { console.log('child update', updated);
return; context.emit('update', {
} ...props.block,
data: {
context.emit('activate'); ...props.block.data,
...updated,
},
});
}; };
return { return {
getBlock,
classes, classes,
activate, activate,
onChildUpdate,
}; };
}, },
render() { render() {
return ( console.log('render block', this.block);
<div const Block = this.getBlock(this.block.name).edit;
class="sb-block" return <Block
tabindex="0" data={this.block.data}
block-id={this.block.blockId}
{...{ {...{
attrs: this.$attrs,
on: { on: {
click: this.activate, ...this.$listeners,
update: this.onChildUpdate,
'insert-block': (block: BlockDefinition) => this.$emit('insert-block', block),
'append-block': (block: BlockDefinition) => this.$emit('append-block', block),
}, },
}} }}
> />;
{this.$slots.toolbar}
{this.$slots.default ? this.$slots.default : <div>Your content here</div>}
</div>
);
}, },
}); });

View file

@ -1,25 +1,20 @@
import { defineComponent } from '@vue/composition-api'; import { defineComponent } from '@vue/composition-api';
import { treeElementProps, useDynamicComponents } from './TreeElement'; import { useDynamicComponents } from './TreeElement';
export default defineComponent({ export default defineComponent({
props: { props: {},
...treeElementProps,
orientation: String,
},
setup(props) { setup(props) {
const getComponent = useDynamicComponents(props.components || {}); const { customBlocks } = useDynamicComponents(props.components || {});
return { return {
getComponent, customBlocks,
}; };
}, },
render() { render() {
return ( return (
<div class="sb-layout"> <div class="sb-block-picker">
{{ orientation }}
<component <component
class="sb-main" class="sb-main"
v-for="child in children" v-for="child in children"

View file

@ -1,19 +1,10 @@
.sb-block-placeholder { .sb-block-placeholder {
width: 100%; width: 100%;
height: 0px;
position: relative; position: relative;
overflow: visible; overflow: visible;
&__add { &__add {
background-color: var(--grey-1); background-color: var(--grey-1);
height: 50px;
width: 100%; width: 100%;
opacity: 0;
pointer-events: none;
&:hover {
opacity: 1;
pointer-events: all;
}
} }
} }

View file

@ -13,10 +13,12 @@ export default defineComponent({
type="button" type="button"
{...{ {...{
on: { on: {
click: () => this.$emit('add-block', { click: () => this.$emit('insert-block', {
component: 'sb-paragraph', name: 'sb-paragraph',
id: +(new Date()), blockId: +(new Date()),
data: {
value: '', value: '',
},
}), }),
}, },
}} }}

View file

@ -1,12 +1,16 @@
import { import {
inject,
reactive, reactive,
computed,
defineComponent, defineComponent,
watchEffect, watch,
} from '@vue/composition-api'; } from '@vue/composition-api';
import { import {
model, model,
treeElementProps, blockProps,
useDynamicComponents, useDynamicBlocks,
useActivation,
BlockData,
} from '@components/TreeElement'; } from '@components/TreeElement';
import SbBlock from '@internal/Block'; import SbBlock from '@internal/Block';
@ -21,51 +25,52 @@ export default defineComponent({
model, model,
props: { props: {
...treeElementProps, ...blockProps,
}, },
setup(props, context) { setup(props: BlockProps, context) {
const { getComponent } = useDynamicComponents(props.userComponents); const { getBlock } = useDynamicBlocks();
const { isActive, activate } = useActivation(props.blockId);
const localData = reactive({ const localData = reactive({
orientation: props.tree.data.orientation, orientation: props.data.orientation,
children: [...props.tree.data.children], children: [...props.data.children],
}); });
watchEffect(() => { watch(() => props.data, () => {
localData.orientation = props.tree.data.orientation; localData.orientation = props.data.orientation;
localData.children = [...props.tree.data.children]; localData.children = [...props.data.children];
}); });
const classes = { const classes = computed(() => ({
'sb-layout': true, 'sb-layout': true,
'sb-layout_active': isActive,
[`sb-layout_${localData.orientation}`]: true, [`sb-layout_${localData.orientation}`]: true,
}; }));
const toggleOrientation = () => { const toggleOrientation = () => {
context.emit('data', { context.emit('update', {
id: props.blockId,
...localData,
orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical', orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical',
}); });
}; };
const onChildUpdate = (child, updated) => { const onChildUpdate = (child, updated) => {
const index = localData.children.indexOf(child); const index = localData.children.indexOf(child);
context.emit('data', { context.emit('update', {
...localData,
children: [ children: [
...localData.children.slice(0, index), ...localData.children.slice(0, index),
updated, {
...child,
...updated,
},
...localData.children.slice(index + 1), ...localData.children.slice(index + 1),
], ],
}); });
}; };
const addBlock = (block) => { const appendBlock = (block: BlockData) => {
context.emit('tree', { console.log('append block', block);
...localData, context.emit('update', {
children: [ children: [
...localData.children, ...localData.children,
block, 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 { return {
isActive,
activate,
classes, classes,
onChildUpdate, onChildUpdate,
toggleOrientation, toggleOrientation,
localData, localData,
getComponent, getBlock,
addBlock, appendBlock,
insertBlock,
}; };
}, },
render() { render() {
console.log('render layout');
return ( return (
<SbBlock class={this.classes}> <div class={this.classes}>
<SbToolbar slot="toolbar"> <SbToolbar slot="toolbar">
<button <button
type="button" type="button"
@ -94,31 +115,31 @@ export default defineComponent({
click: this.toggleOrientation, click: this.toggleOrientation,
}, },
}} }}
>{this.localTree.orientation}</button> >{this.localData.orientation}</button>
</SbToolbar> </SbToolbar>
{...this.localTree.children.map((child) => { {...this.localData.children.map((child, index) => (
const Component = this.getComponent(child.component); <SbBlock
return <Component
class="sb-main"
key={child.id} key={child.id}
components={this.components} block={child}
tree={child}
{...{ {...{
on: { on: {
tree: (updated) => this.onChildUpdate(child, updated), update: (updated) => this.onChildUpdate(child, updated),
}, 'insert-block': (block: BlockDefinition) => this.insertBlock(index, block),
}} 'append-block': this.appendBlock,
/>;
})}
<SbBlockPlaceholder
{...{
on: {
'add-block': this.addBlock,
}, },
}} }}
/> />
</SbBlock> ))}
<SbBlockPlaceholder
{...{
on: {
'insert-block': this.appendBlock,
},
}}
/>
</div>
); );
}, },
}); });

View file

@ -2,12 +2,15 @@ import {
defineComponent, defineComponent,
reactive, reactive,
ref, ref,
Ref,
onMounted, onMounted,
watch,
} from '@vue/composition-api'; } from '@vue/composition-api';
import { import {
model, model,
treeElementProps, blockProps,
useDynamicComponents, BlockProps,
useActivation,
} from '@components/TreeElement'; } from '@components/TreeElement';
import SbBlock from '@internal/Block'; import SbBlock from '@internal/Block';
@ -21,56 +24,87 @@ export default defineComponent({
model, model,
props: { props: {
...treeElementProps, ...blockProps,
}, },
setup(props, context) { setup(props: BlockProps, context) {
const { localTree } = useTree(props); const localData = reactive({
value: props.data.value,
focused: false,
});
const inputEl: Ref<null|HTMLElement> = 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) => { const onTextUpdate = ($event: InputEvent) => {
localTree.value = $event.target.innerHTML; localData.value = $event.target.innerHTML;
}; };
const focused = ref(false);
const classes = reactive({ const classes = reactive({
'sb-paragraph': true, 'sb-paragraph': true,
'sb-paragraph_focused': focused, 'sb-paragraph_focused': localData.focused,
}); });
const onFocus = () => { const onFocus = () => {
console.log('focus'); localData.focused = true;
focused.value = true;
}; };
const onBlur = () => { const onBlur = () => {
console.log('blur'); localData.focused = false;
focused.value = false; context.emit('update', {
context.emit('tree', { value: localData.value,
value: localTree.value.value,
}); });
activate(null);
}; };
const inputEl = ref(null); const onKeypress = ($event: KeyboardEvent) => {
if ($event.key === 'Enter') {
onMounted(() => { const blockId = +(new Date());
console.log(inputEl); context.emit('insert-block', {
inputEl.value.innerHTML = localTree.value; blockId,
name: 'sb-paragraph',
data: { value: '' },
}); });
activate(blockId);
$event.preventDefault();
}
};
return { return {
classes, classes,
localTree, localData,
onTextUpdate, onTextUpdate,
focused,
onFocus, onFocus,
onBlur, onBlur,
onKeypress,
inputEl, inputEl,
}; };
}, },
render() { render() {
console.log('render paragraph');
return ( return (
<SbBlock> <div class="sb-paragraph">
<SbToolbar>Paragraph editing</SbToolbar> <SbToolbar>Paragraph editing</SbToolbar>
<p <p
class={this.classes} class={this.classes}
@ -81,10 +115,11 @@ export default defineComponent({
input: this.onTextUpdate, input: this.onTextUpdate,
focus: this.onFocus, focus: this.onFocus,
blur: this.onBlur, blur: this.onBlur,
keypress: this.onKeypress,
}, },
}} }}
></p> ></p>
</SbBlock> </div>
); );
}, },
}); });

View file

@ -1,11 +1,5 @@
/* eslint no-param-reassign: 0 */ /* eslint no-param-reassign: 0 */
interface UserBlock {
name: string;
edit: () => Promise<any>;
display: () => Promise<any>;
}
function addUserBlock(Vue, block) { function addUserBlock(Vue, block) {
if (Vue.prototype.$sb.blocks[block.name]) { if (Vue.prototype.$sb.blocks[block.name]) {
console.warn(`Block ${block.name} is already registered`); console.warn(`Block ${block.name} is already registered`);

View file

@ -1,19 +1,13 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import VueCompositionApi from '@vue/composition-api'; import VueCompositionApi from '@vue/composition-api';
import VueSchlechtenburg from './lib';
import App from './App'; import App from './App';
import './main.scss'; import './main.scss';
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(Vuex);
const store = new Vuex.Store({});
Vue.use(VueCompositionApi); Vue.use(VueCompositionApi);
Vue.use(VueSchlechtenburg);
new Vue({ new Vue({
store,
render: (h) => h(App), render: (h) => h(App),
}).$mount('#app'); }).$mount('#app');