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;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 60px;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ import {
BlockData,
useDynamicBlocks,
useActivation,
BlockDefinition,
} from '@components/TreeElement';
import './Block.scss';
@ -17,6 +16,18 @@ export default defineComponent({
props: {
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) {
@ -28,7 +39,7 @@ export default defineComponent({
}));
const onChildUpdate = (updated: {[key: string]: any}) => {
context.emit('update', {
props.eventUpdate({
...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
class={classes.value}
data={props.block.data}
block-id={props.block.blockId}
{...{
attrs: context.attrs,
on: {
...context.listeners,
update: onChildUpdate,
'insert-block': (block: BlockDefinition) => context.emit('insert-block', block),
'append-block': (block: BlockDefinition) => context.emit('append-block', block),
},
nativeOn: {
click: ($event: MouseEvent) => {
$event.stopPropagation();
activate();
return () => <div class={classes.value}>
<div class="sb-block__edit-cover"></div>
<div class="sb-block__mover"></div>
<Block
data={props.block.data}
block-id={props.block.blockId}
eventUpdate={onChildUpdate}
eventInsertBlock={props.eventInsertBlock}
eventAppendBlock={props.eventAppendBlock}
{...{
attrs: context.attrs,
on: {
...context.listeners,
update: onChildUpdate,
},
},
}}
/>;
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 {
useDynamicBlocks,
BlockDefinition,
} from '../TreeElement';
import SbButton from './Button';
import SbModal from './Modal';
import './BlockPicker.scss';
export default defineComponent({
@ -12,26 +19,45 @@ export default defineComponent({
props: {},
setup(props, context) {
const open = ref(false);
const { customBlocks } = useDynamicBlocks();
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 () => (
<div class="sb-block-picker">
{...blockList.value.map((block: BlockDefinition) => (
<button
type="button"
{...{
on: {
click: () => context.emit('picked-block', {
name: block.name,
blockId: `${+(new Date())}`,
data: block.getDefaultData(),
}),
},
}}
>{block.name}</button>
))}
<div
class="sb-block-picker"
onClick={($event: MouseEvent) => $event.stopPropagation()}
>
<SbButton
type="button"
onClick={() => {
open.value = true;
console.log(open);
}}
>Add a block</SbButton>
<SbModal
open={open.value}
eventClose={() => {
open.value = false;
}}
>
{...blockList.value.map((block: BlockDefinition) => (
<SbButton
type="button"
onClick={selectBlock(block)}
>{block.name}</SbButton>
))}
</SbModal>
</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 {
position: absolute;
bottom: 100%;
width: 100%;
height: 40px;
background-color: var(--grey-1);
width: auto;
max-width: 100%;
height: auto;
}

View file

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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ export interface LayoutData {
export interface LayoutProps extends BlockProps {
data: LayoutData;
eventUpdate: (b?: BlockData) => void;
}
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>,
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) {
@ -81,7 +89,7 @@ export default defineComponent({
}));
const setAlignment = ($event: Event) => {
context.emit('update', { align: ($event.target as HTMLSelectElement).value });
props.eventUpdate({ align: ($event.target as HTMLSelectElement).value });
};
const onFocus = () => {
@ -90,7 +98,7 @@ export default defineComponent({
const onBlur = () => {
localData.focused = false;
context.emit('update', {
props.eventUpdate({
value: localData.value,
});
activate(null);
@ -99,7 +107,7 @@ export default defineComponent({
const onKeypress = ($event: KeyboardEvent) => {
if ($event.key === 'Enter') {
const blockId = `${+(new Date())}`;
context.emit('insert-block', {
props.eventInsertBlock({
blockId,
name: 'sb-paragraph',
data: getDefaultData(),

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
}
html {
--bg: white;
--grey-0: white;
--grey-1-t: rgba(0, 0, 0, 0.05);
--grey-1: rgb(242, 242, 242);
--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: rgb(75, 75, 75);
--black: rgba(0, 0, 0, 0.9);
--bg: var(--grey-1);
--fg: var(--black);
--interact: #3f9cff;
}
body {
margin: 0;
}