schlechtenburg/packages/core/lib/rich-text/FormattingToolbar.tsx

125 lines
3.2 KiB
TypeScript

import {
ref,
Ref,
onMounted,
onBeforeUnmount,
defineComponent,
computed,
} from 'vue';
import debounce from 'lodash/debounce';
import { useRootElement } from '../use-root-element';
import { useFormattingToolbar } from './use-formatting-toolbar';
import {
getSelection,
getAnchorElementForSelection,
getRangeFromSelection,
getRect,
} from './selection';
import './FormattingToolbar.scss';
export const SbFormattingToolbar = defineComponent({
name: 'sb-inlinetoolbar',
setup() {
const { rootElement } = useRootElement();
const {
activeClient,
setActiveClient,
clients,
getAllTools,
} = useFormattingToolbar();
const allTools = computed(() => getAllTools());
const selection: Ref<Selection|null>= ref(null);
const selectionRect: Ref<DOMRect|null>= ref(null);
const updateSelectionRect = () => {
selectionRect.value = getRect(selection.value!);
};
const showing: Ref<boolean> = ref(false);
const show = () => {
showing.value = true;
updateSelectionRect();
};
const hide = () => {
showing.value = false;
setActiveClient(null);
};
const style = computed(() => {
const rootRect = rootElement.value?.getBoundingClientRect();
const x = (selectionRect.value?.x || 0)
- (rootRect?.left || 0);
const y = (selectionRect.value?.y || 0)
+ (selectionRect.value?.height || 0)
- (rootRect?.top || 0);
return {
left: Math.floor(x) + 'px',
top: Math.floor(y) + 'px',
};
});
const classes = computed(() => ({
'sb-inline-toolbar': true,
}));
const onSelectionChanged = debounce(() => {
selection.value = getSelection();
if (!selection.value) {
console.warn('Could not get selection');
return
}
const range = getRangeFromSelection(selection.value);
// If we're not selecting anything, bail
if (!range || range.endOffset === range.startOffset) {
hide();
return;
}
const focusedElement = document.activeElement;
if (!focusedElement) {
hide();
return;
}
// If new selection is not in registered clients, close it
if (!clients.value.find(client => client === focusedElement)) {
hide();
return;
}
setActiveClient(document.activeElement as HTMLElement|null);
show();
}, 50);
onMounted(() => {
rootElement.value?.addEventListener('click', close);
document.addEventListener('selectionchange', onSelectionChanged, true);
});
onBeforeUnmount(() => {
rootElement.value?.removeEventListener('click', close);
document.removeEventListener('selectionchange', onSelectionChanged);
});
return () => (allTools.value.length && showing.value)
? <div
style={style.value}
class={classes.value}
onClick={(event:MouseEvent) => { event.stopPropagation(); }}
>{allTools.value.map((tool) => {
const ToolUI = tool.ui as any;
return <ToolUI
activeClient={activeClient}
selection={selection.value}
></ToolUI>;
})}</div>
: null;
},
});