125 lines
3.2 KiB
TypeScript
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;
|
|
},
|
|
});
|