links: clickable [[wiki-links]] on cards
LinkedText now makes each [[link]] segment interactive: click (or Enter when focused) resolves the title and opens the target note, creating it first if it doesn't exist. Reuses the palette's /?open=<id> open path. @click.stop / keydown.stop keep the surrounding card's click-to-open from also firing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,16 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useNotesStore } from "../stores/notes";
|
||||||
|
import { useTitlesStore } from "../stores/titles";
|
||||||
|
|
||||||
const props = defineProps<{ text: string }>();
|
const props = defineProps<{ text: string }>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const notes = useNotesStore();
|
||||||
|
const titles = useTitlesStore();
|
||||||
|
|
||||||
interface Part {
|
interface Part {
|
||||||
text: string;
|
text: string;
|
||||||
link: boolean;
|
link: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the body into plain segments and [[wiki-link]] segments (styled, non-
|
// Split the body into plain segments and [[wiki-link]] segments. Link segments
|
||||||
// interactive here — navigation happens from the editor's Links / Linked-from
|
// are clickable here on the card: they resolve the title and open the target
|
||||||
// lists so we don't nest interactive controls inside the card's open target).
|
// note (creating it first if it doesn't exist yet).
|
||||||
const parts = computed<Part[]>(() => {
|
const parts = computed<Part[]>(() => {
|
||||||
const result: Part[] = [];
|
const result: Part[] = [];
|
||||||
const re = /\[\[([^[\]]+)\]\]/g;
|
const re = /\[\[([^[\]]+)\]\]/g;
|
||||||
@@ -24,12 +31,32 @@ const parts = computed<Part[]>(() => {
|
|||||||
if (last < props.text.length) result.push({ text: props.text.slice(last), link: false });
|
if (last < props.text.length) result.push({ text: props.text.slice(last), link: false });
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reuses the command palette's open mechanism: navigate to the board with
|
||||||
|
// ?open=<id>, which BoardView watches to open the editor.
|
||||||
|
async function follow(title: string) {
|
||||||
|
await titles.load();
|
||||||
|
let hit = titles.resolve(title);
|
||||||
|
if (!hit) {
|
||||||
|
const created = await notes.createTitled(title);
|
||||||
|
await titles.reload();
|
||||||
|
hit = { id: created.id, title: created.title ?? title };
|
||||||
|
}
|
||||||
|
void router.push({ path: "/", query: { open: hit.id } });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="whitespace-pre-wrap break-words"
|
<span class="whitespace-pre-wrap break-words"
|
||||||
><template v-for="(part, i) in parts" :key="i"
|
><template v-for="(part, i) in parts" :key="i"
|
||||||
><span v-if="part.link" class="font-medium text-brand-700 dark:text-brand">{{ part.text }}</span
|
><span
|
||||||
|
v-if="part.link"
|
||||||
|
role="link"
|
||||||
|
tabindex="0"
|
||||||
|
class="cursor-pointer font-medium text-brand-700 underline-offset-2 hover:underline dark:text-brand"
|
||||||
|
@click.stop="follow(part.text)"
|
||||||
|
@keydown.enter.stop.prevent="follow(part.text)"
|
||||||
|
>{{ part.text }}</span
|
||||||
><template v-else>{{ part.text }}</template></template
|
><template v-else>{{ part.text }}</template></template
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user