Skip to content

Commit 7d11728

Browse files
Use FormattedItemName in CrossReference
1 parent 87b7064 commit 7d11728

File tree

2 files changed

+85
-73
lines changed

2 files changed

+85
-73
lines changed
Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
<template>
22
<node-view-wrapper class="cross-reference-wrapper" contenteditable="false">
3-
<span
4-
v-if="isValid"
5-
class="formatted-cross-reference badge badge-light clickable"
6-
:style="{ backgroundColor: badgeColor }"
7-
@click="openEditPage"
8-
>
9-
@{{ node.attrs.itemId }}
10-
</span>
11-
<span v-else class="badge badge-secondary"> @{{ node.attrs.itemId }} </span>
12-
<span v-if="isValid && displayName">
13-
{{ displayName }}
14-
<span v-if="node.attrs.chemform">
15-
[ <ChemicalFormula :formula="node.attrs.chemform" /> ]
16-
</span>
17-
</span>
3+
<FormattedItemName
4+
:item_id="node.attrs.itemId"
5+
:item-type="node.attrs.itemType"
6+
:selecting="true"
7+
enable-click
8+
:max-length="formattedItemNameMaxLength"
9+
class="formatted-cross-reference-badge"
10+
/>
1811
</node-view-wrapper>
1912
</template>
2013

2114
<script>
2215
import { NodeViewWrapper } from "@tiptap/vue-3";
23-
import ChemicalFormula from "@/components/ChemicalFormula.vue";
24-
import { itemTypes } from "@/resources.js";
16+
import FormattedItemName from "@/components/FormattedItemName.vue";
2517
2618
export default {
2719
components: {
2820
NodeViewWrapper,
29-
ChemicalFormula,
21+
FormattedItemName,
3022
},
3123
props: {
3224
node: {
@@ -37,39 +29,14 @@ export default {
3729
type: Object,
3830
required: true,
3931
},
40-
},
41-
data() {
42-
return {
43-
isValid: false,
44-
};
45-
},
46-
computed: {
47-
badgeColor() {
48-
const itemType = this.node.attrs.itemType || "samples";
49-
return itemTypes[itemType]?.lightColor || "LightGrey";
50-
},
51-
displayName() {
52-
return this.node.attrs.name || "";
53-
},
54-
},
55-
watch: {
56-
"node.attrs.itemId": {
57-
handler() {
58-
this.checkValidity();
59-
},
60-
immediate: true,
32+
formattedItemNameMaxLength: {
33+
type: Number,
34+
default: NaN,
6135
},
6236
},
6337
methods: {
64-
async checkValidity() {
65-
if (!this.node.attrs.itemId) {
66-
this.isValid = false;
67-
return;
68-
}
69-
70-
this.isValid = true;
71-
},
7238
openEditPage() {
39+
if (!this.node.attrs.itemId) return;
7340
window.open(`/edit/${this.node.attrs.itemId}`, "_blank");
7441
},
7542
},
@@ -81,13 +48,8 @@ export default {
8148
display: inline;
8249
}
8350
84-
.formatted-cross-reference {
85-
border: 2px solid transparent;
51+
.formatted-cross-reference-badge {
8652
cursor: pointer;
87-
}
88-
89-
.formatted-cross-reference.clickable:hover {
90-
border: 2px solid rgba(0, 0, 0, 0.6);
91-
box-shadow: 0 0 2px gray;
53+
display: inline-flex;
9254
}
9355
</style>

webapp/src/editor/extensions/CrossReferenceInputRule.js

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ItemSelect from "@/components/ItemSelect.vue";
55

66
let suggestionApp = null;
77
let suggestionEl = null;
8+
let cleanupListeners = null;
89

910
export const CrossReferenceInputRule = Extension.create({
1011
name: "crossReferenceInputRule",
@@ -53,23 +54,41 @@ function createSuggestionPlugin(options, editor) {
5354

5455
state: {
5556
init() {
56-
return { active: false, query: null, range: null };
57+
return { active: false, range: null, query: null };
5758
},
58-
5959
apply(tr, prev, oldState, newState) {
6060
const { selection } = newState;
6161
const { empty, from } = selection;
62-
if (!empty) return { ...prev, active: false };
62+
if (!empty) return { active: false, range: null, query: null };
63+
6364
const $pos = selection.$from;
6465
const textBefore = $pos.parent.textContent.slice(0, $pos.parentOffset);
6566
const match = textBefore.match(/@(\w*)$/);
67+
6668
if (!match) {
6769
hideSuggestions();
68-
return { ...prev, active: false };
70+
return { active: false, range: null, query: null };
6971
}
72+
7073
const query = match[1];
7174
const range = { from: from - match[0].length, to: from };
72-
return { active: true, query, range };
75+
return { active: true, range, query };
76+
},
77+
},
78+
79+
props: {
80+
handleKeyDown(view, event) {
81+
const state = pluginKey.getState(view.state);
82+
if (!state?.active) return false;
83+
84+
if (event.key === "Backspace" && state.query === "" && state.range) {
85+
const tr = view.state.tr.deleteRange(state.range);
86+
view.dispatch(tr);
87+
hideSuggestions();
88+
return true;
89+
}
90+
91+
return false;
7392
},
7493
},
7594

@@ -95,6 +114,10 @@ function showSuggestions(view, state, options, editor) {
95114
if (!suggestionEl) {
96115
suggestionEl = document.createElement("div");
97116
suggestionEl.className = "dropdown-menu show p-2 tiptap-suggestions";
117+
suggestionEl.style.position = "fixed";
118+
suggestionEl.style.minWidth = "350px";
119+
suggestionEl.style.maxWidth = "600px";
120+
suggestionEl.style.zIndex = 2000;
98121
document.body.appendChild(suggestionEl);
99122
}
100123

@@ -104,32 +127,33 @@ function showSuggestions(view, state, options, editor) {
104127
placeholder: "Search items...",
105128
typesToQuery: ["samples", "cells", "starting_materials"],
106129
"onUpdate:modelValue": (item) => {
107-
if (!item) return;
130+
const state = options.pluginKey.getState(editor.state);
131+
if (!item || !state?.range) return;
132+
108133
options.command({ editor, range: state.range, props: item });
109134
hideSuggestions();
110135
},
111136
});
112137
suggestionApp.mount(suggestionEl);
113-
window.addEventListener("scroll", () => hideSuggestions(), true);
114-
window.addEventListener("resize", () => hideSuggestions(), true);
138+
cleanupListeners = setupGlobalListeners();
115139
}
116140

117-
reposition(view, state);
141+
reposition(view, state.range);
118142
suggestionEl.style.display = "block";
119-
suggestionEl.querySelector("input")?.focus();
143+
144+
const input = suggestionEl.querySelector("input");
145+
if (input && document.activeElement !== input) {
146+
input.focus();
147+
}
120148
}
121149

122-
function reposition(view, state) {
123-
if (!suggestionEl) return;
124-
const coords = view.coordsAtPos(state.range.from);
150+
function reposition(view, range) {
151+
if (!suggestionEl || !range) return;
125152

126-
suggestionEl.style.position = "absolute";
127-
suggestionEl.style.left = `${coords.left}px`;
128-
suggestionEl.style.top = `${coords.bottom - 20}px`;
129-
suggestionEl.style.zIndex = 1050;
153+
const coords = view.coordsAtPos(range.from);
130154

131-
suggestionEl.style.minWidth = "350px";
132-
suggestionEl.style.maxWidth = "600px";
155+
suggestionEl.style.left = `${coords.left}px`;
156+
suggestionEl.style.top = `${coords.bottom}px`;
133157
}
134158

135159
function hideSuggestions(destroy = false) {
@@ -139,6 +163,32 @@ function hideSuggestions(destroy = false) {
139163
suggestionEl.remove();
140164
suggestionEl = null;
141165
suggestionApp = null;
166+
if (cleanupListeners) {
167+
cleanupListeners();
168+
cleanupListeners = null;
169+
}
142170
}
143171
}
144172
}
173+
174+
function setupGlobalListeners() {
175+
const onClickOutside = (event) => {
176+
if (suggestionEl && !suggestionEl.contains(event.target)) {
177+
hideSuggestions();
178+
}
179+
};
180+
181+
const onScrollOrResize = () => {
182+
if (suggestionEl) hideSuggestions();
183+
};
184+
185+
window.addEventListener("mousedown", onClickOutside);
186+
window.addEventListener("scroll", onScrollOrResize, true);
187+
window.addEventListener("resize", onScrollOrResize, true);
188+
189+
return () => {
190+
window.removeEventListener("mousedown", onClickOutside);
191+
window.removeEventListener("scroll", onScrollOrResize, true);
192+
window.removeEventListener("resize", onScrollOrResize, true);
193+
};
194+
}

0 commit comments

Comments
 (0)