221 lines
5.3 KiB
TypeScript
221 lines
5.3 KiB
TypeScript
import {
|
|
defineComponent,
|
|
reactive,
|
|
computed,
|
|
ref,
|
|
Ref,
|
|
onMounted,
|
|
watch,
|
|
PropType,
|
|
} from 'vue';
|
|
import {
|
|
model,
|
|
useActivation,
|
|
SbToolbar,
|
|
SbSelect,
|
|
OnUpdateSelfCb,
|
|
OnAppendBlockCb,
|
|
OnRemoveSelfCb,
|
|
OnActivateNextCb,
|
|
OnActivatePreviousCb,
|
|
generateBlockId,
|
|
} 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,
|
|
},
|
|
eventUpdate: {
|
|
type: (null as unknown) as PropType<OnUpdateSelfCb<IHeadingData>>,
|
|
default: () => {},
|
|
},
|
|
eventAppendBlock: {
|
|
type: (null as unknown) as PropType<OnAppendBlockCb>,
|
|
default: () => {},
|
|
},
|
|
eventRemoveSelf: {
|
|
type: (null as unknown) as PropType<OnRemoveSelfCb>,
|
|
default: () => {},
|
|
},
|
|
eventActivateNext: {
|
|
type: (null as unknown) as PropType<OnActivateNextCb>,
|
|
default: () => {},
|
|
},
|
|
eventActivatePrevious: {
|
|
type: (null as unknown) as PropType<OnActivatePreviousCb>,
|
|
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.eventUpdate({
|
|
...localData,
|
|
level: parseInt(($event.target as HTMLSelectElement).value, 10),
|
|
});
|
|
};
|
|
|
|
const setAlignment = ($event: Event) => {
|
|
props.eventUpdate({
|
|
...localData,
|
|
align: ($event.target as HTMLSelectElement).value,
|
|
});
|
|
};
|
|
|
|
const onFocus = () => {
|
|
localData.focused = true;
|
|
activate();
|
|
};
|
|
|
|
const onBlur = () => {
|
|
localData.focused = false;
|
|
props.eventUpdate({
|
|
value: localData.value,
|
|
align: localData.align,
|
|
level: localData.level,
|
|
});
|
|
};
|
|
|
|
const onKeydown = ($event: KeyboardEvent) => {
|
|
if ($event.key === 'Enter' && !$event.shiftKey) {
|
|
const id = generateBlockId();
|
|
props.eventAppendBlock({
|
|
id,
|
|
name: 'sb-paragraph',
|
|
data: getDefaultParagraphData(),
|
|
});
|
|
|
|
activate(id);
|
|
|
|
$event.preventDefault();
|
|
}
|
|
};
|
|
|
|
const onKeyup = ($event: KeyboardEvent) => {
|
|
if ($event.key === 'Backspace' && localData.value === '') {
|
|
props.eventRemoveSelf();
|
|
}
|
|
|
|
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.eventActivateNext();
|
|
break;
|
|
case 'ArrowUp':
|
|
props.eventActivatePrevious();
|
|
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>
|
|
);
|
|
},
|
|
});
|