Skip to content

Commit d798891

Browse files
committed
WIP: impl TextSelectionmenu by floating-ui
TODO: - document - menu impl - delay to show
1 parent e98c298 commit d798891

File tree

3 files changed

+82
-13
lines changed

3 files changed

+82
-13
lines changed

src/components/Floating.vue

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,21 @@
4141
// .floating-content[data-theme~="warning"] { ... }
4242
4343
import { ref, watch, computed, toRefs } from "vue";
44+
import type { Ref } from "vue";
4445
import { useElementHover } from "@vueuse/core";
4546
import * as floating from "@floating-ui/vue";
46-
import type { Placement } from "@floating-ui/vue";
47+
import type { Placement, VirtualElement } from "@floating-ui/vue";
4748
4849
// interface ----
4950
5051
type OnShowFn = () => void;
5152
type OnHideFn = () => void;
5253
5354
type Props = {
55+
target?: HTMLElement | VirtualElement;
5456
placement?: Placement;
5557
interactive?: boolean;
58+
inline?: boolean;
5659
delay?: number | [number, number];
5760
arrow?: boolean;
5861
theme?: string;
@@ -61,8 +64,10 @@ type Props = {
6164
onHide?: OnHideFn;
6265
};
6366
const props = withDefaults(defineProps<Props>(), {
67+
target: undefined,
6468
placement: "top",
6569
interactive: false,
70+
inline: false,
6671
delay: 0,
6772
arrow: true,
6873
theme: "light",
@@ -73,15 +78,20 @@ const slots = defineSlots();
7378
// define varibales ----
7479
7580
const {
81+
target: targetProp,
7682
placement: placementProp,
7783
interactive,
84+
inline,
7885
delay,
7986
arrow: arrowProp,
8087
theme: themeProp,
81-
trigger: triggerRef,
88+
trigger: triggerProp,
8289
} = toRefs(props);
8390
84-
const targetRef = ref();
91+
const defaultSlotRef = ref();
92+
const targetRef = computed((): HTMLElement | VirtualElement =>
93+
targetProp.value ?? defaultSlotRef.value
94+
); // targetRef変数を消せるかも?HMRで動作する?
8595
const floatingRef = ref();
8696
const arrowRef = ref();
8797
@@ -96,6 +106,7 @@ const {
96106
whileElementsMounted: floating.autoUpdate,
97107
placement: placementProp,
98108
middleware: [
109+
...(inline.value ? [floating.inline()] : []),
99110
floating.offset(6),
100111
floating.flip({ fallbackAxisSideDirection: "end", crossAxis: false }),
101112
floating.shift({ padding: 5 }),
@@ -110,19 +121,24 @@ const delayOptions = computed(() => {
110121
return { delayEnter, delayLeave };
111122
});
112123
113-
// define hover states ----
114-
115-
const isTargetHovered = useElementHover(targetRef, {
116-
delayEnter: delayOptions.value.delayEnter,
117-
delayLeave: delayOptions.value.delayLeave + (interactive.value ? 100 : 0),
124+
const isUsingDefaultSlot = computed((): boolean => {
125+
return !targetProp.value;
118126
});
127+
128+
// define trigger states ----
129+
130+
const triggerRef = triggerProp.value !== undefined ?
131+
triggerProp as Ref<boolean> :
132+
useElementHover(defaultSlotRef, {
133+
delayEnter: delayOptions.value.delayEnter,
134+
delayLeave: delayOptions.value.delayLeave + (interactive.value ? 100 : 0),
135+
});
119136
const isTooltipHovered = useElementHover(floatingRef, {
120137
delayEnter: 0, // keep tooltip open when hovering over the tooltip
121138
delayLeave: delayOptions.value.delayLeave,
122139
});
123140
const isTriggered = computed((): boolean => {
124-
const triggered = triggerRef.value ?? isTargetHovered.value;
125-
const t = triggered || (interactive.value && isTooltipHovered.value);
141+
const t = triggerRef.value || (interactive.value && isTooltipHovered.value);
126142
return t;
127143
});
128144
@@ -155,7 +171,7 @@ const arrowStyles = computed(() => {
155171
</script>
156172

157173
<template>
158-
<span ref="targetRef">
174+
<span v-if="isUsingDefaultSlot" ref="defaultSlotRef">
159175
<slot />
160176
</span>
161177
<Teleport v-if="slots.content" to="body">

src/components/Preview.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import MarkdownIt from 'markdown-it';
33
import type { Mermaid } from 'mermaid';
44
import MarkdownRenderer from './MarkdownRenderer';
55
import Floating from './Floating.vue';
6-
import { onMounted, watch, toRefs, nextTick, inject } from 'vue';
6+
import TextSelectionMenu from './TextSelectionMenu.vue';
7+
import { onMounted, watch, toRefs, nextTick, inject, ref } from 'vue';
78
89
const $md = inject('$md') as MarkdownIt;
910
const $mermaid = inject('$mermaid') as Mermaid;
11+
const preview = ref();
1012
1113
type Props = {
1214
content: string,
@@ -55,7 +57,8 @@ const render = (text: string) => $md.render(text);
5557
</Floating>
5658
</span>
5759
</span>
58-
<MarkdownRenderer class="preview" :content="render(content)" />
60+
<MarkdownRenderer ref="preview" class="preview" :content="render(content)" />
61+
<TextSelectionMenu :baseEl="preview" />
5962
</div>
6063
<div class="inner-html">
6164
innerHTML

src/components/TextSelectionMenu.vue

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { computed, toRefs } from "vue";
3+
import { useTextSelection, unrefElement } from "@vueuse/core";
4+
import Floating from "./Floating.vue";
5+
import type { MaybeElement } from "@vueuse/core";
6+
import type { VirtualElement } from "@floating-ui/vue";
7+
8+
// interface ----
9+
10+
type Props = {
11+
baseEl: MaybeElement;
12+
};
13+
const props = defineProps<Props>();
14+
const { baseEl } = toRefs(props);
15+
16+
// define textselection ----
17+
18+
const { ranges } = useTextSelection();
19+
20+
const trigger = computed((): boolean => {
21+
const el = unrefElement(baseEl);
22+
const range = ranges.value?.[0];
23+
if (!range || range.collapsed || !el || !range.intersectsNode(el)) {
24+
return false;
25+
}
26+
return true;
27+
});
28+
29+
const target = computed((): VirtualElement | undefined => {
30+
const range = ranges.value?.[0];
31+
if (!range) {
32+
return undefined;
33+
}
34+
const virtualEl = {
35+
getBoundingClientRect: () => range.getBoundingClientRect(),
36+
getClientRects: () => range.getClientRects(),
37+
};
38+
return virtualEl;
39+
});
40+
41+
</script>
42+
43+
<template>
44+
TODO: use target if specified instead of #default slot
45+
<Floating placement="top" interactive inline :target="target" :trigger="trigger">
46+
<template #content>
47+
ここにメニュー要素を配置
48+
</template>
49+
</Floating>
50+
</template>

0 commit comments

Comments
 (0)