Basic heading

This commit is contained in:
Benjamin Bädorf 2022-03-11 18:23:14 +01:00
parent 01c2644a36
commit 4ffafd4bb5
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
19 changed files with 333 additions and 29 deletions

View file

@ -5,6 +5,8 @@
align-items: stretch; align-items: stretch;
justify-items: stretch; justify-items: stretch;
height: auto; height: auto;
min-width: 32px;
min-height: 32px;
> * > .sb-toolbar { > * > .sb-toolbar {
opacity: 0; opacity: 0;

View file

@ -25,7 +25,7 @@ export const SbBlockPicker = defineComponent({
const blockList = computed(() => Object.keys(customBlocks).map((key) => customBlocks[key])); const blockList = computed(() => Object.keys(customBlocks).map((key) => customBlocks[key]));
const selectBlock = (block: IBlockDefinition<any>) => () => { const selectBlock = (block: IBlockDefinition<any>) => {
open.value = false; open.value = false;
props.onPickedBlock({ props.onPickedBlock({
name: block.name, name: block.name,

View file

@ -2,6 +2,7 @@
background-color: var(--grey-0); background-color: var(--grey-0);
border: 1px solid var(--grey-2); border: 1px solid var(--grey-2);
position: relative; position: relative;
font-size: 1rem;
&:hover { &:hover {
border: 1px solid var(--interact); border: 1px solid var(--interact);

View file

@ -4,6 +4,15 @@
"data": { "data": {
"orientation": "vertical", "orientation": "vertical",
"children": [ "children": [
{
"name": "sb-heading",
"id": "1480592112212",
"data": {
"value": "A pretty heading",
"align": "center",
"level": 1
}
},
{ {
"name": "sb-paragraph", "name": "sb-paragraph",
"id": "1590592112200", "id": "1590592112200",

View file

@ -0,0 +1,44 @@
import {
defineComponent,
computed,
PropType,
h,
} from 'vue';
import {
model,
} from '@schlechtenburg/core';
import {
getDefaultData,
IHeadingData,
} from './util';
import './style.scss';
export default defineComponent({
name: 'sb-heading-display',
model,
props: {
data: {
type: Object as PropType<IHeadingData>,
default: getDefaultData,
},
},
setup(props) {
const classes = computed(() => ({
'sb-heading': true,
[`sb-heading_align-${props.data.align}`]: true,
[`sb-heading_${props.data.level}`]: true,
}));
return () => h(
`h${props.data.level}`,
{
class: classes,
innerHTML: props.data.value,
},
);
},
});

View file

@ -1 +1,199 @@
export default {}; import {
defineComponent,
reactive,
computed,
ref,
Ref,
onMounted,
watch,
PropType,
} from 'vue';
import {
model,
useActivation,
SbToolbar,
SbSelect,
} from '@schlechtenburg/core';
import {
getDefaultData,
IHeadingData
} from './util';
import { getDefaultData as getDefaultParagraphData } from '@schlechtenburg/paragraph';
import './style.scss';
export default defineComponent({
name: 'sb-heading-edit',
model,
props: {
blockId: { type: String, required: true },
data: {
type: (null as unknown) as PropType<IHeadingData>,
default: getDefaultData,
},
onUpdate: { type: Function, default: () => {} },
onAppendBlock: { type: Function, default: () => {} },
onRemoveSelf: { type: Function, default: () => {} },
onActivateNext: { type: Function, default: () => {} },
onActivatePrevious: { type: Function, default: () => {} },
},
setup(props) {
const localData = (reactive({
value: props.data.value,
align: props.data.align,
level: props.data.level,
focused: false,
}) as unknown) as {
value: string;
align: string;
level: number;
focused: boolean;
};
const inputEl: Ref<null|HTMLElement> = ref(null);
const { isActive, activate } = useActivation(props.blockId);
const focusInput = () => {
if (inputEl.value && isActive.value) {
inputEl.value.focus();
}
};
onMounted(() => {
focusInput();
if (inputEl.value) {
inputEl.value.innerHTML = localData.value;
}
});
watch(isActive, focusInput);
watch(() => props.data, () => {
localData.value = props.data.value;
localData.align = props.data.align;
localData.level = props.data.level;
if (inputEl.value) {
inputEl.value.innerHTML = localData.value;
}
});
const onTextUpdate = ($event: Event) => {
localData.value = ($event.target as HTMLElement).innerHTML;
};
const classes = computed(() => ({
'sb-heading': true,
'sb-heading_focused': localData.focused,
[`sb-heading_align${localData.align}`]: true,
[`sb-heading_${localData.level}`]: true,
}));
const setLevel = ($event: Event) => {
props.onUpdate({
...localData,
level: parseInt(($event.target as HTMLSelectElement).value, 10),
});
};
const setAlignment = ($event: Event) => {
props.onUpdate({
...localData,
align: ($event.target as HTMLSelectElement).value,
});
};
const onFocus = () => {
localData.focused = true;
activate();
};
const onBlur = () => {
localData.focused = false;
props.onUpdate({
value: localData.value,
align: localData.align,
level: localData.level,
});
};
const onKeydown = ($event: KeyboardEvent) => {
if ($event.key === 'Enter' && !$event.shiftKey) {
const id = `${+(new Date())}`;
props.onAppendBlock({
id,
name: 'sb-paragraph',
data: getDefaultParagraphData(),
});
activate(id);
$event.preventDefault();
}
};
const onKeyup = ($event: KeyboardEvent) => {
if ($event.key === 'Backspace' && localData.value === '') {
props.onRemoveSelf();
}
const selection = window.getSelection();
const node = selection?.focusNode;
const childNodes = Array.from(inputEl?.value?.childNodes || []);
const index = node ? childNodes.indexOf(node as ChildNode) : -1;
if (node === inputEl.value || index === 0 || index === childNodes.length -1) {
switch ($event.key) {
case 'ArrowDown':
props.onActivateNext();
break;
case 'ArrowUp':
props.onActivatePrevious();
break;
}
}
};
return () => (
<div class={classes.value}>
<SbToolbar>
<SbSelect
{...{
value: localData.level,
onChange: setLevel,
}}
>
<option value={1}>h1</option>
<option value={2}>h2</option>
<option value={3}>h3</option>
<option value={4}>h4</option>
<option value={5}>h5</option>
<option value={6}>h6</option>
</SbSelect>
<SbSelect
{...{
value: localData.align,
onChange: setAlignment,
}}
>
<option>left</option>
<option>center</option>
<option>right</option>
</SbSelect>
</SbToolbar>
<p
class="sb-heading__input"
ref={inputEl}
contenteditable
onInput={onTextUpdate}
onFocus={onFocus}
onBlur={onBlur}
onKeydown={onKeydown}
onKeyup={onKeyup}
></p>
</div>
);
},
});

View file

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

View file

@ -0,0 +1,39 @@
.sb-heading {
flex-basis: 100%;
font-weight: bold;
&_1 {
font-size: 4rem;
}
&_2 {
font-size: 3rem;
}
&_3 {
font-size: 2rem;
}
&_4 {
font-size: 1.6rem;
}
&_5 {
font-size: 1.2rem;
}
&_6 {
font-size: 1rem;
}
&__input {
display: block;
flex-basis: 100%;
}
&_align {
&-left { &, .sb-heading__input { text-align: left; } }
&-right { &, .sb-heading__input { text-align: right; } }
&-center { &, .sb-heading__input { text-align: center; } }
}
}

View file

@ -1,2 +1,11 @@
export const a = 1; export interface IHeadingData {
export const getDefaultData = () => ({}); value: string;
align: string;
level: number;
}
export const getDefaultData: () => IHeadingData = () => ({
value: '',
align: 'left',
level: 1,
});

View file

@ -25,6 +25,7 @@
}, },
"dependencies": { "dependencies": {
"@schlechtenburg/core": "^0.0.0", "@schlechtenburg/core": "^0.0.0",
"@schlechtenburg/paragraph": "^0.0.0",
"vue": "^3.0.7" "vue": "^3.0.7"
} }
} }

View file

@ -5,7 +5,7 @@ import {
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
getDefaultData, getDefaultData,
ImageData, IImageData,
} from './util'; } from './util';
import './style.scss'; import './style.scss';
@ -17,7 +17,7 @@ export default defineComponent({
props: { props: {
data: { data: {
type: (null as unknown) as PropType<ImageData>, type: (null as unknown) as PropType<IImageData>,
default: getDefaultData, default: getDefaultData,
}, },
}, },

View file

@ -13,10 +13,10 @@ import {
SbBlock, SbBlock,
IBlockData, IBlockData,
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { ParagraphData } from '@schlechtenburg/paragraph'; import { IParagraphData } from '@schlechtenburg/paragraph';
import { import {
getDefaultData, getDefaultData,
ImageData, IImageData,
} from './util'; } from './util';
import './style.scss'; import './style.scss';
@ -29,7 +29,7 @@ export default defineComponent({
props: { props: {
onUpdate: { type: Function, default: () => {} }, onUpdate: { type: Function, default: () => {} },
data: { data: {
type: (null as unknown) as PropType<ImageData>, type: (null as unknown) as PropType<IImageData>,
default: getDefaultData, default: getDefaultData,
}, },
}, },
@ -75,7 +75,7 @@ export default defineComponent({
} }
}; };
const onDescriptionUpdate = (description: IBlockData<ParagraphData>) => { const onDescriptionUpdate = (description: IBlockData<IParagraphData>) => {
props.onUpdate({ props.onUpdate({
...props.data, ...props.data,
description, description,
@ -104,7 +104,7 @@ export default defineComponent({
/> />
<SbBlock <SbBlock
block={localData.description} block={localData.description}
onUpdate={(updated: IBlockData<ParagraphData>) => onDescriptionUpdate(updated)} onUpdate={(updated: IBlockData<IParagraphData>) => onDescriptionUpdate(updated)}
/> />
</> </>
: <SbButton {...{ onClick: selectImage }}>Select Image</SbButton> : <SbButton {...{ onClick: selectImage }}>Select Image</SbButton>

View file

@ -4,17 +4,17 @@ import {
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
name as paragraphName, name as paragraphName,
ParagraphData, IParagraphData,
getDefaultData as getDefaultParagraphData getDefaultData as getDefaultParagraphData
} from '@schlechtenburg/paragraph'; } from '@schlechtenburg/paragraph';
export interface ImageData { export interface IImageData {
src: string; src: string;
alt: string; alt: string;
description: IBlockData<ParagraphData>; description: IBlockData<IParagraphData>;
} }
export const getDefaultData: () => ImageData = () => ({ export const getDefaultData: () => IImageData = () => ({
src: '', src: '',
alt: '', alt: '',
description: { description: {

View file

@ -8,7 +8,7 @@ import {
SbBlock, SbBlock,
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
LayoutData, ILayoutData,
getDefaultData, getDefaultData,
} from './util'; } from './util';
@ -21,7 +21,7 @@ export default defineComponent({
props: { props: {
data: { data: {
type: (null as unknown) as PropType<LayoutData>, type: (null as unknown) as PropType<ILayoutData>,
default: getDefaultData, default: getDefaultData,
}, },
}, },

View file

@ -18,7 +18,7 @@ import {
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
LayoutData, ILayoutData,
getDefaultData, getDefaultData,
} from './util'; } from './util';
@ -32,7 +32,7 @@ export default defineComponent({
props: { props: {
onUpdate: { type: Function, default: () => {} }, onUpdate: { type: Function, default: () => {} },
data: { data: {
type: (null as unknown) as PropType<LayoutData>, type: (null as unknown) as PropType<ILayoutData>,
default: getDefaultData, default: getDefaultData,
}, },
}, },
@ -40,7 +40,7 @@ export default defineComponent({
setup(props) { setup(props) {
const { activate } = useActivation(); const { activate } = useActivation();
const localData: LayoutData = reactive({ const localData: ILayoutData = reactive({
orientation: props.data.orientation, orientation: props.data.orientation,
children: [...props.data.children], children: [...props.data.children],
}); });
@ -79,6 +79,7 @@ export default defineComponent({
}; };
const appendBlock = (block: IBlockData<any>) => { const appendBlock = (block: IBlockData<any>) => {
console.log(appendBlock);
localData.children = [ localData.children = [
...localData.children, ...localData.children,
block, block,

View file

@ -1,11 +1,11 @@
import { IBlockData } from '@schlechtenburg/core'; import { IBlockData } from '@schlechtenburg/core';
export interface LayoutData { export interface ILayoutData {
orientation: string; orientation: string;
children: IBlockData<any>[]; children: IBlockData<any>[];
} }
export const getDefaultData: () => LayoutData = () => ({ export const getDefaultData: () => ILayoutData = () => ({
orientation: 'vertical', orientation: 'vertical',
children: [], children: [],
}); });

View file

@ -8,7 +8,7 @@ import {
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
getDefaultData, getDefaultData,
ParagraphData, IParagraphData,
} from './util'; } from './util';
import './style.scss'; import './style.scss';
@ -20,7 +20,7 @@ export default defineComponent({
props: { props: {
data: { data: {
type: Object as PropType<ParagraphData>, type: Object as PropType<IParagraphData>,
default: getDefaultData, default: getDefaultData,
}, },
}, },

View file

@ -16,7 +16,7 @@ import {
} from '@schlechtenburg/core'; } from '@schlechtenburg/core';
import { import {
getDefaultData, getDefaultData,
ParagraphData, IParagraphData,
} from './util'; } from './util';
import './style.scss'; import './style.scss';
@ -29,7 +29,7 @@ export default defineComponent({
props: { props: {
blockId: { type: String, required: true }, blockId: { type: String, required: true },
data: { data: {
type: (null as unknown) as PropType<ParagraphData>, type: (null as unknown) as PropType<IParagraphData>,
default: getDefaultData, default: getDefaultData,
}, },
onUpdate: { type: Function, default: () => {} }, onUpdate: { type: Function, default: () => {} },

View file

@ -1,9 +1,9 @@
export interface ParagraphData { export interface IParagraphData {
value: string; value: string;
align: string; align: string;
} }
export const getDefaultData: () => ParagraphData = () => ({ export const getDefaultData: () => IParagraphData = () => ({
value: '', value: '',
align: 'left', align: 'left',
}); });