Providing/injecting, adding blocks
This commit is contained in:
parent
2153b06b2d
commit
d49ea3e99f
11
src/App.tsx
11
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 };
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue