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() {
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 };

View file

@ -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<BlockDefinition[]>, 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 (
<Component
<SbBlock
class="sb-main"
user-components={this.components}
data={this.block.data}
id={this.block.id}
block={this.block}
{...{
on: {
blockUpdate: (block) => this.$emit('block-update', block),
update: (block: BlockDefinition) => this.$emit('update', block),
},
}}
/>

View file

@ -1,34 +1,61 @@
import {
Context,
Ref,
ref,
inject,
reactive,
computed,
} 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;
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 };
}
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<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';
@ -6,43 +16,51 @@ export default defineComponent({
name: 'sb-block',
props: {
active: { type: Boolean, default: false },
block: { type: (null as unknown) as PropType<BlockData>, 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 (
<div
class="sb-block"
tabindex="0"
{...{
on: {
click: this.activate,
},
}}
>
{this.$slots.toolbar}
{this.$slots.default ? this.$slots.default : <div>Your content here</div>}
</div>
);
console.log('render block', this.block);
const Block = this.getBlock(this.block.name).edit;
return <Block
data={this.block.data}
block-id={this.block.blockId}
{...{
attrs: this.$attrs,
on: {
...this.$listeners,
update: this.onChildUpdate,
'insert-block': (block: BlockDefinition) => this.$emit('insert-block', block),
'append-block': (block: BlockDefinition) => this.$emit('append-block', block),
},
}}
/>;
},
});

View file

@ -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 (
<div class="sb-layout">
{{ orientation }}
<div class="sb-block-picker">
<component
class="sb-main"
v-for="child in children"

View file

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

View file

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

View file

@ -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 (
<SbBlock class={this.classes}>
<div class={this.classes}>
<SbToolbar slot="toolbar">
<button
type="button"
@ -94,31 +115,31 @@ export default defineComponent({
click: this.toggleOrientation,
},
}}
>{this.localTree.orientation}</button>
>{this.localData.orientation}</button>
</SbToolbar>
{...this.localTree.children.map((child) => {
const Component = this.getComponent(child.component);
return <Component
class="sb-main"
{...this.localData.children.map((child, index) => (
<SbBlock
key={child.id}
components={this.components}
tree={child}
block={child}
{...{
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,
'insert-block': this.appendBlock,
},
}}
/>
</SbBlock>
</div>
);
},
});

View file

@ -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<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) => {
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 (
<SbBlock>
<div class="sb-paragraph">
<SbToolbar>Paragraph editing</SbToolbar>
<p
class={this.classes}
@ -81,10 +115,11 @@ export default defineComponent({
input: this.onTextUpdate,
focus: this.onFocus,
blur: this.onBlur,
keypress: this.onKeypress,
},
}}
></p>
</SbBlock>
</div>
);
},
});

View file

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

View file

@ -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');