Slightly better interface

This commit is contained in:
Benjamin Bädorf 2020-05-27 15:57:57 +02:00
parent d00383892f
commit b81f0c6673
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
26 changed files with 442 additions and 87 deletions

View file

@ -3,5 +3,4 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #2c3e50; color: #2c3e50;
margin-top: 60px;
} }

View file

@ -1,7 +1,7 @@
import { import {
defineComponent, defineComponent,
reactive, reactive,
watchEffect, ref,
} from '@vue/composition-api'; } from '@vue/composition-api';
import Schlechtenburg from '@components/Schlechtenburg'; import Schlechtenburg from '@components/Schlechtenburg';
import { BlockData } from './components/TreeElement'; import { BlockData } from './components/TreeElement';
@ -12,6 +12,7 @@ export default defineComponent({
name: 'App', name: 'App',
setup() { setup() {
const activeTab = ref('edit');
const block = reactive({ const block = reactive({
name: 'sb-layout', name: 'sb-layout',
blockId: `${+(new Date())}`, blockId: `${+(new Date())}`,
@ -23,20 +24,39 @@ export default defineComponent({
return () => ( return () => (
<div id="app"> <div id="app">
<Schlechtenburg <select
block={block} value={activeTab.value}
{...{ {...{
on: { on: {
update: (newBlock: BlockData) => { change: ($event: Event) => {
block.name = newBlock.name; activeTab.value = ($event.target as HTMLSelectElement).value;
block.blockId = newBlock.blockId;
block.data = newBlock.data;
}, },
}, },
}} }}
>
<option>edit</option>
<option>display</option>
<option>json</option>
</select>
<Schlechtenburg
vShow={activeTab.value === 'edit'}
block={block}
eventUpdate={(newBlock: BlockData) => {
block.name = newBlock.name;
block.blockId = newBlock.blockId;
block.data = newBlock.data;
}}
/> />
<pre><code>{JSON.stringify(block, null, 2)}</code></pre> <Schlechtenburg
vShow={activeTab.value === 'display'}
block={block}
mode="display"
/>
<pre vShow={activeTab.value === 'json'}>
<code>{JSON.stringify(block, null, 2)}</code>
</pre>
</div> </div>
); );
}, },

View file

@ -0,0 +1,5 @@
.sb-main {
padding: 50px 20px;
background-color: var(--bg);
}

View file

@ -9,6 +9,8 @@ import {
model, model,
ActiveBlock, ActiveBlock,
BlockData, BlockData,
SbMode,
Mode,
BlockDefinition, BlockDefinition,
BlockLibraryDefinition, BlockLibraryDefinition,
BlockLibrary, BlockLibrary,
@ -21,9 +23,13 @@ import SbParagraph from '@user/Paragraph/index';
import SbImage from '@user/Image/index'; import SbImage from '@user/Image/index';
import SbHeading from '@user/Heading/index'; import SbHeading from '@user/Heading/index';
import './Schlechtenburg.scss';
export interface SchlechtenburgProps { export interface SchlechtenburgProps {
customBlocks: BlockDefinition[]; customBlocks: BlockDefinition[];
eventUpdate: (b?: BlockData) => void;
block: BlockData; block: BlockData;
mode: SbMode;
} }
export default defineComponent({ export default defineComponent({
@ -32,11 +38,25 @@ export default defineComponent({
model, model,
props: { props: {
customBlocks: { type: (null as unknown) as PropType<BlockDefinition[]>, default: () => [] }, customBlocks: { type: Array as PropType<BlockDefinition[]>, default: () => [] },
block: { type: (null as unknown) as PropType<BlockData>, required: true }, block: { type: Object as PropType<BlockData>, required: true },
eventUpdate: {
type: (Function as unknown) as (b?: BlockData) => void,
default: () => () => undefined,
},
mode: {
type: String,
validator(value: string) {
return ['edit', 'display'].includes(value);
},
default: 'edit',
},
}, },
setup(props, context) { setup(props: SchlechtenburgProps, context) {
const mode = ref(props.mode);
provide(Mode, mode);
const activeBlock = ref(null); const activeBlock = ref(null);
provide(ActiveBlock, activeBlock); provide(ActiveBlock, activeBlock);
@ -56,15 +76,12 @@ export default defineComponent({
provide(BlockLibrary, blockLibrary); provide(BlockLibrary, blockLibrary);
return () => ( return () => (
<SbBlock <div class="sb-main">
class="sb-main" <SbBlock
block={props.block} block={props.block}
{...{ eventUpdate={props.eventUpdate}
on: { />
update: (block: BlockDefinition) => context.emit('update', block), </div>
},
}}
/>
); );
}, },
}); });

View file

@ -6,9 +6,6 @@ import {
computed, computed,
} from '@vue/composition-api'; } from '@vue/composition-api';
export const ActiveBlock = Symbol('Schlechtenburg active block');
export const BlockLibrary = Symbol('Schlechtenburg block library');
export interface BlockDefinition { export interface BlockDefinition {
name: string; name: string;
getDefaultData: any; getDefaultData: any;
@ -38,16 +35,32 @@ export const model = {
export const blockProps = { export const blockProps = {
blockId: { type: String, required: true }, blockId: { type: String, required: true },
eventUpdate: {
type: (Function as unknown) as (b: any) => void,
default: () => () => undefined,
},
data: { type: Object, default: () => ({}) }, data: { type: Object, default: () => ({}) },
}; };
export enum SbMode {
Edit = 'edit',
Display = 'display',
}
export const Mode = Symbol('Schlechtenburg mode');
export const BlockLibrary = Symbol('Schlechtenburg block library');
export function useDynamicBlocks() { export function useDynamicBlocks() {
const mode = inject(Mode, ref(SbMode.Edit));
const customBlocks: BlockLibraryDefinition = inject(BlockLibrary, reactive({})); const customBlocks: BlockLibraryDefinition = inject(BlockLibrary, reactive({}));
const getBlock = (name: string) => customBlocks[name]; const getBlock = (name: string) => customBlocks[name][mode.value];
return { customBlocks, getBlock }; return {
mode,
customBlocks,
getBlock,
};
} }
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 activeBlockId: Ref<string|null> = inject(ActiveBlock, ref(null));
const isActive = computed(() => activeBlockId.value === currentBlockId); const isActive = computed(() => activeBlockId.value === currentBlockId);

View file

@ -5,7 +5,7 @@
justify-items: stretch; justify-items: stretch;
min-height: 50px; min-height: 50px;
> .sb-toolbar { > * > .sb-toolbar {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
} }
@ -13,10 +13,9 @@
&_active { &_active {
outline: 1px solid var(--grey-2); outline: 1px solid var(--grey-2);
> .sb-toolbar { > * > .sb-toolbar {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
outline: 1px solid var(--grey-2);
} }
} }
} }

View file

@ -7,7 +7,6 @@ import {
BlockData, BlockData,
useDynamicBlocks, useDynamicBlocks,
useActivation, useActivation,
BlockDefinition,
} from '@components/TreeElement'; } from '@components/TreeElement';
import './Block.scss'; import './Block.scss';
@ -17,6 +16,18 @@ export default defineComponent({
props: { props: {
block: { type: (null as unknown) as PropType<BlockData>, required: true }, block: { type: (null as unknown) as PropType<BlockData>, required: true },
eventUpdate: {
type: (Function as unknown) as (b?: BlockData) => void,
default: () => () => undefined,
},
eventInsertBlock: {
type: (Function as unknown) as (b?: BlockData) => void,
default: () => () => undefined,
},
eventAppendBlock: {
type: (Function as unknown) as (b?: BlockData) => void,
default: () => () => undefined,
},
}, },
setup(props, context) { setup(props, context) {
@ -28,7 +39,7 @@ export default defineComponent({
})); }));
const onChildUpdate = (updated: {[key: string]: any}) => { const onChildUpdate = (updated: {[key: string]: any}) => {
context.emit('update', { props.eventUpdate({
...props.block, ...props.block,
data: { data: {
...props.block.data, ...props.block.data,
@ -37,27 +48,31 @@ export default defineComponent({
}); });
}; };
const Block = getBlock(props.block.name).edit as any; const Block = getBlock(props.block.name) as any;
return () => <Block return () => <div class={classes.value}>
class={classes.value} <div class="sb-block__edit-cover"></div>
data={props.block.data} <div class="sb-block__mover"></div>
block-id={props.block.blockId} <Block
{...{ data={props.block.data}
attrs: context.attrs, block-id={props.block.blockId}
on: { eventUpdate={onChildUpdate}
...context.listeners, eventInsertBlock={props.eventInsertBlock}
update: onChildUpdate, eventAppendBlock={props.eventAppendBlock}
'insert-block': (block: BlockDefinition) => context.emit('insert-block', block), {...{
'append-block': (block: BlockDefinition) => context.emit('append-block', block), attrs: context.attrs,
}, on: {
nativeOn: { ...context.listeners,
click: ($event: MouseEvent) => { update: onChildUpdate,
$event.stopPropagation();
activate();
}, },
}, nativeOn: {
}} click: ($event: MouseEvent) => {
/>; $event.stopPropagation();
activate();
},
},
}}
/>
</div>;
}, },
}); });

View file

@ -0,0 +1,2 @@
.sb-block-picker {
}

View file

@ -1,9 +1,16 @@
import { computed, defineComponent } from '@vue/composition-api'; import {
computed,
defineComponent,
ref,
} from '@vue/composition-api';
import { import {
useDynamicBlocks, useDynamicBlocks,
BlockDefinition, BlockDefinition,
} from '../TreeElement'; } from '../TreeElement';
import SbButton from './Button';
import SbModal from './Modal';
import './BlockPicker.scss'; import './BlockPicker.scss';
export default defineComponent({ export default defineComponent({
@ -12,26 +19,45 @@ export default defineComponent({
props: {}, props: {},
setup(props, context) { setup(props, context) {
const open = ref(false);
const { customBlocks } = useDynamicBlocks(); const { customBlocks } = useDynamicBlocks();
const blockList = computed(() => Object.keys(customBlocks).map((key) => customBlocks[key])); const blockList = computed(() => Object.keys(customBlocks).map((key) => customBlocks[key]));
const selectBlock = (block: BlockDefinition) => () => {
open.value = false;
context.emit('picked-block', {
name: block.name,
blockId: `${+(new Date())}`,
data: block.getDefaultData(),
});
};
return () => ( return () => (
<div class="sb-block-picker"> <div
{...blockList.value.map((block: BlockDefinition) => ( class="sb-block-picker"
<button onClick={($event: MouseEvent) => $event.stopPropagation()}
type="button" >
{...{ <SbButton
on: { type="button"
click: () => context.emit('picked-block', { onClick={() => {
name: block.name, open.value = true;
blockId: `${+(new Date())}`, console.log(open);
data: block.getDefaultData(), }}
}), >Add a block</SbButton>
}, <SbModal
}} open={open.value}
>{block.name}</button> eventClose={() => {
))} open.value = false;
}}
>
{...blockList.value.map((block: BlockDefinition) => (
<SbButton
type="button"
onClick={selectBlock(block)}
>{block.name}</SbButton>
))}
</SbModal>
</div> </div>
); );
}, },

View file

@ -0,0 +1,10 @@
.sb-button {
border: 0;
padding: 8px 12px;
background-color: var(--grey-0);
border: 1px solid var(--grey-2);
&:hover {
border: 1px solid var(--interact);
}
}

View file

@ -0,0 +1,23 @@
import { defineComponent } from '@vue/composition-api';
import './Button.scss';
export default defineComponent({
name: 'sb-button',
inheritAttrs: false,
setup(props, context) {
return () => (
<button
class="sb-button"
{...{
attrs: context.attrs,
on: context.listeners,
}}
>
{context.slots.default()}
</button>
);
},
});

View file

@ -0,0 +1,31 @@
.sb-modal {
&__overlay {
background-color: var(--grey-3-t);
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 10vh 10vw;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
}
&__content {
width: 900px;
max-width: 100%;
height: auto;
max-height: 100%;
background-color: var(--grey-0);
padding: 24px 32px;
}
&_open #{&}__overlay {
opacity: 1;
pointer-events: all;
}
}

View file

@ -0,0 +1,45 @@
import {
defineComponent,
computed,
ref,
} from '@vue/composition-api';
import './Modal.scss';
export default defineComponent({
name: 'sb-modal',
props: {
open: {
type: Boolean,
default: false,
},
eventClose: {
type: (Function as unknown) as () => void,
default: () => () => undefined,
},
},
setup(props, context) {
const classes = computed(() => ({
'sb-modal': true,
'sb-modal_open': props.open,
}));
return () => (
<div class={classes.value}>
<div
class="sb-modal__overlay"
onClick={($event: MouseEvent) => {
$event.stopPropagation();
props.eventClose();
}}
>
<div class="sb-modal__content">
{context.slots.default()}
</div>
</div>
</div>
);
},
});

View file

@ -1,7 +1,7 @@
.sb-toolbar { .sb-toolbar {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
width: 100%; width: auto;
height: 40px; max-width: 100%;
background-color: var(--grey-1); height: auto;
} }

View file

@ -34,7 +34,7 @@ export default defineComponent({
}, },
}, },
setup(props: ImageProps) { setup(props: ImageProps, context) {
const localData = reactive({ const localData = reactive({
src: props.data.src, src: props.data.src,
alt: props.data.alt, alt: props.data.alt,
@ -56,7 +56,9 @@ export default defineComponent({
const onImageSelect = () => { const onImageSelect = () => {
if (fileInput.value && fileInput.value.files && fileInput.value.files.length) { if (fileInput.value && fileInput.value.files && fileInput.value.files.length) {
localData.src = window.URL.createObjectURL(fileInput.value.files[0]); context.emit('update', {
src: window.URL.createObjectURL(fileInput.value.files[0]),
});
} }
}; };

View file

@ -0,0 +1,57 @@
import {
reactive,
computed,
defineComponent,
watch,
PropType,
} from '@vue/composition-api';
import {
model,
blockProps,
useActivation,
BlockData,
} from '@components/TreeElement';
import SbBlock from '@internal/Block';
import SbToolbar from '@internal/Toolbar';
import SbBlockPlaceholder from '@internal/BlockPlaceholder';
import {
LayoutData,
LayoutProps,
getDefaultData,
} from './util';
import './style.scss';
export default defineComponent({
name: 'sb-layout-display',
model,
props: {
...blockProps,
data: {
type: (null as unknown) as PropType<LayoutData>,
default: getDefaultData,
},
},
setup(props: LayoutProps, context) {
const classes = computed(() => ({
'sb-layout': true,
[`sb-layout_${props.data.orientation}`]: true,
}));
return () => (
<div class={classes.value}>
{...props.data.children.map((child, index) => (
<SbBlock
key={child.blockId}
block={child}
/>
))}
</div>
);
},
});

View file

@ -7,12 +7,13 @@ import {
} from '@vue/composition-api'; } from '@vue/composition-api';
import { import {
model, model,
blockProps,
useActivation, useActivation,
BlockData, BlockData,
blockProps,
} from '@components/TreeElement'; } from '@components/TreeElement';
import SbBlock from '@internal/Block'; import SbBlock from '@internal/Block';
import SbButton from '@internal/Button';
import SbToolbar from '@internal/Toolbar'; import SbToolbar from '@internal/Toolbar';
import SbBlockPlaceholder from '@internal/BlockPlaceholder'; import SbBlockPlaceholder from '@internal/BlockPlaceholder';
@ -31,6 +32,10 @@ export default defineComponent({
props: { props: {
...blockProps, ...blockProps,
eventUpdate: {
type: (Function as unknown) as (b?: LayoutData) => void,
default: () => () => undefined,
},
data: { data: {
type: (null as unknown) as PropType<LayoutData>, type: (null as unknown) as PropType<LayoutData>,
default: getDefaultData, default: getDefaultData,
@ -56,14 +61,14 @@ export default defineComponent({
})); }));
const toggleOrientation = () => { const toggleOrientation = () => {
context.emit('update', { props.eventUpdate({
orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical', orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical',
}); });
}; };
const onChildUpdate = (child: BlockData, updated: BlockData) => { const onChildUpdate = (child: BlockData, updated: BlockData) => {
const index = localData.children.indexOf(child); const index = localData.children.indexOf(child);
context.emit('update', { props.eventUpdate({
children: [ children: [
...localData.children.slice(0, index), ...localData.children.slice(0, index),
{ {
@ -76,7 +81,7 @@ export default defineComponent({
}; };
const appendBlock = (block: BlockData) => { const appendBlock = (block: BlockData) => {
context.emit('update', { props.eventUpdate({
children: [ children: [
...localData.children, ...localData.children,
block, block,
@ -99,14 +104,14 @@ export default defineComponent({
return () => ( return () => (
<div class={classes.value}> <div class={classes.value}>
<SbToolbar slot="toolbar"> <SbToolbar slot="toolbar">
<button <SbButton
type="button" type="button"
{...{ {...{
on: { nativeOn: {
click: toggleOrientation, click: toggleOrientation,
}, },
}} }}
>{localData.orientation}</button> >{localData.orientation}</SbButton>
</SbToolbar> </SbToolbar>
{...localData.children.map((child, index) => ( {...localData.children.map((child, index) => (

View file

@ -3,6 +3,6 @@ import { getDefaultData } from './util';
export default { export default {
name: 'sb-layout', name: 'sb-layout',
getDefaultData, getDefaultData,
edit: () => import('./edit'), edit: () => import('./edit.tsx'),
display: () => import('./edit'), display: () => import('./display.tsx'),
}; };

View file

@ -1,5 +1,6 @@
.sb-layout { .sb-layout {
display: flex; display: flex;
flex-basis: 100%;
&_vertical { &_vertical {
flex-direction: column; flex-direction: column;
@ -8,4 +9,8 @@
&_horizontal { &_horizontal {
flex-direction: row; flex-direction: row;
} }
> * {
flex-basis: 100%;
}
} }

View file

@ -10,6 +10,7 @@ export interface LayoutData {
export interface LayoutProps extends BlockProps { export interface LayoutProps extends BlockProps {
data: LayoutData; data: LayoutData;
eventUpdate: (b?: BlockData) => void;
} }
export const getDefaultData: () => LayoutData = () => ({ export const getDefaultData: () => LayoutData = () => ({

View file

@ -0,0 +1,58 @@
import {
defineComponent,
reactive,
computed,
ref,
Ref,
onMounted,
watch,
PropType,
} from '@vue/composition-api';
import {
model,
blockProps,
useActivation,
} from '@components/TreeElement';
import SbToolbar from '@internal/Toolbar';
import {
getDefaultData,
ParagraphData,
ParagraphProps,
} from './util';
import './style.scss';
export default defineComponent({
name: 'sb-paragraph-edit',
model,
props: {
...blockProps,
data: {
type: (null as unknown) as PropType<ParagraphData>,
default: getDefaultData,
},
eventUpdate: {
type: (Function as unknown) as (b?: ParagraphData) => void,
default: () => () => undefined,
},
eventInsertBlock: {
type: (Function as unknown) as (b?: ParagraphData) => void,
default: () => () => undefined,
},
},
setup(props: ParagraphProps, context) {
const classes = computed(() => ({
'sb-paragraph': true,
[`sb-paragraph_align-${props.data.align}`]: true,
}));
return () => (
<p class={classes}>{props.data.value}</p>
);
},
});

View file

@ -35,6 +35,14 @@ export default defineComponent({
type: (null as unknown) as PropType<ParagraphData>, type: (null as unknown) as PropType<ParagraphData>,
default: getDefaultData, default: getDefaultData,
}, },
eventUpdate: {
type: (Function as unknown) as (b?: ParagraphData) => void,
default: () => () => undefined,
},
eventInsertBlock: {
type: (Function as unknown) as (b?: ParagraphData) => void,
default: () => () => undefined,
},
}, },
setup(props: ParagraphProps, context) { setup(props: ParagraphProps, context) {
@ -81,7 +89,7 @@ export default defineComponent({
})); }));
const setAlignment = ($event: Event) => { const setAlignment = ($event: Event) => {
context.emit('update', { align: ($event.target as HTMLSelectElement).value }); props.eventUpdate({ align: ($event.target as HTMLSelectElement).value });
}; };
const onFocus = () => { const onFocus = () => {
@ -90,7 +98,7 @@ export default defineComponent({
const onBlur = () => { const onBlur = () => {
localData.focused = false; localData.focused = false;
context.emit('update', { props.eventUpdate({
value: localData.value, value: localData.value,
}); });
activate(null); activate(null);
@ -99,7 +107,7 @@ export default defineComponent({
const onKeypress = ($event: KeyboardEvent) => { const onKeypress = ($event: KeyboardEvent) => {
if ($event.key === 'Enter') { if ($event.key === 'Enter') {
const blockId = `${+(new Date())}`; const blockId = `${+(new Date())}`;
context.emit('insert-block', { props.eventInsertBlock({
blockId, blockId,
name: 'sb-paragraph', name: 'sb-paragraph',
data: getDefaultData(), data: getDefaultData(),

View file

@ -4,5 +4,5 @@ export default {
name: 'sb-paragraph', name: 'sb-paragraph',
getDefaultData, getDefaultData,
edit: () => import('./edit'), edit: () => import('./edit'),
display: () => import('./edit'), display: () => import('./display'),
}; };

View file

@ -1,7 +1,9 @@
.sb-paragraph { .sb-paragraph {
flex-basis: 100%;
&__input { &__input {
display: block; display: block;
width: 100%; flex-basis: 100%;
} }
&_align { &_align {

View file

@ -0,0 +1,3 @@
export default {
};

View file

@ -3,7 +3,7 @@
} }
html { html {
--bg: white; --grey-0: white;
--grey-1-t: rgba(0, 0, 0, 0.05); --grey-1-t: rgba(0, 0, 0, 0.05);
--grey-1: rgb(242, 242, 242); --grey-1: rgb(242, 242, 242);
--grey-2-t: rgba(0, 0, 0, 0.1); --grey-2-t: rgba(0, 0, 0, 0.1);
@ -15,4 +15,13 @@ html {
--grey-5-t: rgba(0, 0, 0, 0.7); --grey-5-t: rgba(0, 0, 0, 0.7);
--grey-5: rgb(75, 75, 75); --grey-5: rgb(75, 75, 75);
--black: rgba(0, 0, 0, 0.9); --black: rgba(0, 0, 0, 0.9);
--bg: var(--grey-1);
--fg: var(--black);
--interact: #3f9cff;
}
body {
margin: 0;
} }