@estiva-app/ui 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BlockNodes.d.ts +12 -1
- package/dist/BlockNodes.d.ts.map +1 -1
- package/dist/LinkChips.d.ts +82 -0
- package/dist/LinkChips.d.ts.map +1 -0
- package/dist/ReferenceCards.d.ts +77 -0
- package/dist/ReferenceCards.d.ts.map +1 -0
- package/dist/ReferenceMenu.d.ts +18 -0
- package/dist/ReferenceMenu.d.ts.map +1 -0
- package/dist/RichText.d.ts +9 -2
- package/dist/RichText.d.ts.map +1 -1
- package/dist/SelectionToolbar.d.ts +9 -1
- package/dist/SelectionToolbar.d.ts.map +1 -1
- package/dist/editor.d.ts +3 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +589 -20
- package/dist/editor.js.map +4 -4
- package/dist/index.js +39 -19
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +290 -3
- package/src/BlockNodes.test.tsx +2 -1
- package/src/BlockNodes.tsx +51 -9
- package/src/LinkChips.mdx +57 -0
- package/src/LinkChips.stories.tsx +67 -0
- package/src/LinkChips.test.tsx +178 -0
- package/src/LinkChips.ts +243 -0
- package/src/ReferenceCards.mdx +79 -0
- package/src/ReferenceCards.stories.tsx +64 -0
- package/src/ReferenceCards.test.tsx +143 -0
- package/src/ReferenceCards.ts +225 -0
- package/src/ReferenceMenu.mdx +75 -0
- package/src/ReferenceMenu.stories.tsx +59 -0
- package/src/ReferenceMenu.test.tsx +289 -0
- package/src/ReferenceMenu.tsx +346 -0
- package/src/RichText.test.tsx +17 -0
- package/src/RichText.tsx +34 -3
- package/src/SelectionToolbar.tsx +10 -2
- package/src/editor.ts +13 -0
- package/src/pages.test.ts +3 -0
- package/src/registry/registry.test.ts +6 -6
- package/stories/Choosing.mdx +3 -0
package/dist/editor.js
CHANGED
|
@@ -83,7 +83,7 @@ var ICONS = {
|
|
|
83
83
|
var SETTLE_MS = 250;
|
|
84
84
|
var GAP = 8;
|
|
85
85
|
var POPOVER_GAP = 4;
|
|
86
|
-
function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false }) {
|
|
86
|
+
function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false, children }) {
|
|
87
87
|
const [anchor, setAnchor] = useState(null);
|
|
88
88
|
const [mode, setMode] = useState("marks");
|
|
89
89
|
const [url, setUrl] = useState("");
|
|
@@ -273,7 +273,8 @@ function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], lin
|
|
|
273
273
|
onClick: openField,
|
|
274
274
|
children: /* @__PURE__ */ jsx(IconLink, { size: 16, stroke: 1.5 })
|
|
275
275
|
}
|
|
276
|
-
)
|
|
276
|
+
),
|
|
277
|
+
children
|
|
277
278
|
] })
|
|
278
279
|
)
|
|
279
280
|
}
|
|
@@ -1062,14 +1063,540 @@ var SlashCommands = Extension3.create({
|
|
|
1062
1063
|
});
|
|
1063
1064
|
|
|
1064
1065
|
// src/BlockNodes.tsx
|
|
1065
|
-
import { Extension as
|
|
1066
|
+
import { Extension as Extension7, InputRule, Node as Node2 } from "@tiptap/core";
|
|
1066
1067
|
import { DOMSerializer } from "@tiptap/pm/model";
|
|
1067
|
-
import { Plugin as
|
|
1068
|
+
import { Plugin as Plugin6, PluginKey as PluginKey7 } from "@tiptap/pm/state";
|
|
1068
1069
|
import { Mapping, ReplaceAroundStep } from "@tiptap/pm/transform";
|
|
1069
1070
|
import { NodeViewWrapper as NodeViewWrapper2, ReactNodeViewRenderer as ReactNodeViewRenderer2, ReactRenderer as ReactRenderer2 } from "@tiptap/react";
|
|
1070
1071
|
import { Checkbox } from "@estiva-app/ui";
|
|
1071
1072
|
import { calloutLook } from "@estiva-app/ui";
|
|
1072
|
-
|
|
1073
|
+
|
|
1074
|
+
// src/ReferenceMenu.tsx
|
|
1075
|
+
import { useCallback as useCallback3 } from "react";
|
|
1076
|
+
import { Extension as Extension6 } from "@tiptap/core";
|
|
1077
|
+
import { useEditorState } from "@tiptap/react";
|
|
1078
|
+
import { NodeSelection as NodeSelection2, Plugin as Plugin5, PluginKey as PluginKey6, TextSelection as TextSelection2 } from "@tiptap/pm/state";
|
|
1079
|
+
import { ReplaceStep } from "@tiptap/pm/transform";
|
|
1080
|
+
import { IconCheck } from "@tabler/icons-react";
|
|
1081
|
+
|
|
1082
|
+
// src/LinkChips.ts
|
|
1083
|
+
import { Extension as Extension4 } from "@tiptap/core";
|
|
1084
|
+
import { Plugin as Plugin3, PluginKey as PluginKey4 } from "@tiptap/pm/state";
|
|
1085
|
+
var key = new PluginKey4("linkChips");
|
|
1086
|
+
var CHIPS_MADE = "linkChipsMade";
|
|
1087
|
+
var PASTES_KEPT = 8;
|
|
1088
|
+
var LINK_OR_URL = /(\[[^[\]\n]+\]\((?:https?:\/\/|mailto:)[^\s)]+\))|(https?:\/\/[^\s<>"'`]+)/g;
|
|
1089
|
+
var TRAILING = /* @__PURE__ */ new Set([".", ",", ";", ":", "!", "?", ")", "]"]);
|
|
1090
|
+
function urlRuns(text) {
|
|
1091
|
+
if (!text.includes("://")) return [];
|
|
1092
|
+
const out = [];
|
|
1093
|
+
for (const m of text.matchAll(LINK_OR_URL)) {
|
|
1094
|
+
if (m[1]) continue;
|
|
1095
|
+
const start = m.index;
|
|
1096
|
+
if (text.slice(Math.max(0, start - 2), start) === "](" || text[start - 1] === "[") continue;
|
|
1097
|
+
let end = m[0].length;
|
|
1098
|
+
while (end > 0 && TRAILING.has(m[0][end - 1])) end--;
|
|
1099
|
+
out.push({ url: m[0].slice(0, end), from: start, to: start + end, end: start + m[0].length });
|
|
1100
|
+
}
|
|
1101
|
+
return out;
|
|
1102
|
+
}
|
|
1103
|
+
function findLinkChips(doc, selection, referenceOf, pasted = []) {
|
|
1104
|
+
const found = { chips: [], fills: [], open: [] };
|
|
1105
|
+
const open = /* @__PURE__ */ new Set();
|
|
1106
|
+
doc.descendants((node, pos) => {
|
|
1107
|
+
if (node.type.spec.code) return false;
|
|
1108
|
+
if (node.type.name === "reference") {
|
|
1109
|
+
const href = typeof node.attrs.href === "string" ? node.attrs.href : "";
|
|
1110
|
+
if (href && !node.attrs.uri) {
|
|
1111
|
+
const uri = referenceOf?.(href);
|
|
1112
|
+
if (uri) found.fills.push({ pos, uri });
|
|
1113
|
+
else open.add(href);
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
if (!node.isText || !node.text) return true;
|
|
1118
|
+
if (node.marks.some((m) => m.type.spec.code || m.type.name === "code" || m.type.name === "link")) return false;
|
|
1119
|
+
for (const run of urlRuns(node.text)) {
|
|
1120
|
+
const from = pos + run.from;
|
|
1121
|
+
const to = pos + run.to;
|
|
1122
|
+
const wasPasted = pasted.some((range) => range.from <= from && pos + run.end <= range.to);
|
|
1123
|
+
const typing = selection && selection.to > from && selection.from <= pos + run.end;
|
|
1124
|
+
if (typing && !(selection.empty && wasPasted)) continue;
|
|
1125
|
+
const uri = referenceOf?.(run.url);
|
|
1126
|
+
if (uri) found.chips.push({ from, to, url: run.url, uri, marks: node.marks.map((m) => m.type.name), pasted: wasPasted });
|
|
1127
|
+
else open.add(run.url);
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
});
|
|
1131
|
+
found.open = [...open];
|
|
1132
|
+
return found;
|
|
1133
|
+
}
|
|
1134
|
+
function apply(state, found) {
|
|
1135
|
+
if (!found.chips.length && !found.fills.length) return null;
|
|
1136
|
+
const type = state.schema.nodes.reference;
|
|
1137
|
+
if (!type) return null;
|
|
1138
|
+
const tr = state.tr;
|
|
1139
|
+
for (const { pos, uri } of found.fills) {
|
|
1140
|
+
const node = tr.doc.nodeAt(pos);
|
|
1141
|
+
if (node) tr.setNodeMarkup(pos, void 0, { ...node.attrs, uri });
|
|
1142
|
+
}
|
|
1143
|
+
for (const chip of [...found.chips].reverse()) {
|
|
1144
|
+
tr.replaceWith(chip.from, chip.to, type.create({ uri: chip.uri, href: chip.url, marks: chip.marks }));
|
|
1145
|
+
}
|
|
1146
|
+
return tr.setMeta("addToHistory", false).setMeta(CHIPS_MADE, { pasted: found.chips.some((chip) => chip.pasted) });
|
|
1147
|
+
}
|
|
1148
|
+
function refreshLinkChips(editor) {
|
|
1149
|
+
if (editor.isDestroyed) return;
|
|
1150
|
+
const chips = key.getState(editor.state);
|
|
1151
|
+
if (!chips) return;
|
|
1152
|
+
const tr = apply(editor.state, findLinkChips(editor.state.doc, editor.state.selection, chips.options.referenceOf, chips.pasted));
|
|
1153
|
+
if (tr) editor.view.dispatch(tr);
|
|
1154
|
+
}
|
|
1155
|
+
var LinkChips = Extension4.create({
|
|
1156
|
+
name: "linkChips",
|
|
1157
|
+
addOptions() {
|
|
1158
|
+
return { referenceOf: null, ask: null, limit: 24 };
|
|
1159
|
+
},
|
|
1160
|
+
addProseMirrorPlugins() {
|
|
1161
|
+
const options = this.options;
|
|
1162
|
+
const editor = this.editor;
|
|
1163
|
+
return [
|
|
1164
|
+
new Plugin3({
|
|
1165
|
+
key,
|
|
1166
|
+
// The options, where `refreshLinkChips` can read them from the state alone, and where the last pastes landed.
|
|
1167
|
+
state: {
|
|
1168
|
+
init: () => ({ options, pasted: [] }),
|
|
1169
|
+
apply: (tr, value, before, after) => {
|
|
1170
|
+
if (!tr.docChanged) return value;
|
|
1171
|
+
const pasted = value.pasted.map((range) => ({ from: tr.mapping.map(range.from, 1), to: tr.mapping.map(range.to, -1), size: range.to - range.from })).filter((range) => range.from < range.to && range.to - range.from === range.size).map(({ from, to }) => ({ from, to }));
|
|
1172
|
+
if (tr.getMeta("uiEvent") === "paste") {
|
|
1173
|
+
const from = tr.mapping.map(before.selection.from, -1);
|
|
1174
|
+
if (from < after.selection.to) pasted.push({ from, to: after.selection.to });
|
|
1175
|
+
}
|
|
1176
|
+
return { options, pasted: pasted.slice(-PASTES_KEPT) };
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
appendTransaction: (trs, _old, state) => {
|
|
1180
|
+
if (!trs.some((tr) => tr.docChanged || tr.selectionSet)) return null;
|
|
1181
|
+
return apply(state, findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted));
|
|
1182
|
+
},
|
|
1183
|
+
view: (view) => {
|
|
1184
|
+
const asked = /* @__PURE__ */ new Set();
|
|
1185
|
+
const look = (state) => {
|
|
1186
|
+
if (!options.ask) return;
|
|
1187
|
+
const { open } = findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted);
|
|
1188
|
+
const fresh = open.filter((url) => !asked.has(url)).slice(0, Math.max(0, options.limit - asked.size));
|
|
1189
|
+
if (!fresh.length) return;
|
|
1190
|
+
for (const url of fresh) asked.add(url);
|
|
1191
|
+
const forget = () => {
|
|
1192
|
+
for (const url of fresh) asked.delete(url);
|
|
1193
|
+
};
|
|
1194
|
+
let answer;
|
|
1195
|
+
try {
|
|
1196
|
+
answer = options.ask(fresh);
|
|
1197
|
+
} catch {
|
|
1198
|
+
forget();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
void Promise.resolve(answer).then(() => refreshLinkChips(editor), forget);
|
|
1202
|
+
};
|
|
1203
|
+
look(view.state);
|
|
1204
|
+
queueMicrotask(() => {
|
|
1205
|
+
try {
|
|
1206
|
+
refreshLinkChips(editor);
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
return {
|
|
1211
|
+
update: (next, prev) => {
|
|
1212
|
+
if (next.state.doc !== prev.doc || !next.state.selection.eq(prev.selection)) look(next.state);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
})
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// src/ReferenceMenu.tsx
|
|
1222
|
+
import { MenuItem as MenuItem3, MenuSection, MenuSeparator as MenuSeparator2 } from "@estiva-app/ui";
|
|
1223
|
+
import { Popover as Popover2 } from "@estiva-app/ui";
|
|
1224
|
+
|
|
1225
|
+
// src/ReferenceCards.ts
|
|
1226
|
+
import { Extension as Extension5 } from "@tiptap/core";
|
|
1227
|
+
import { NodeSelection, Plugin as Plugin4, PluginKey as PluginKey5 } from "@tiptap/pm/state";
|
|
1228
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
|
|
1229
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
1230
|
+
function layoutOf(raw) {
|
|
1231
|
+
if (typeof raw !== "string" || raw === "" || raw === "none") return void 0;
|
|
1232
|
+
return raw === "detailed" ? "detailed" : "compact";
|
|
1233
|
+
}
|
|
1234
|
+
function referenceCardPlacements(doc) {
|
|
1235
|
+
const out = [];
|
|
1236
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1237
|
+
doc.descendants((node, pos, parent) => {
|
|
1238
|
+
if (!node.isTextblock) return true;
|
|
1239
|
+
const cards = [];
|
|
1240
|
+
node.forEach((child) => {
|
|
1241
|
+
if (child.type.name !== "reference") return;
|
|
1242
|
+
const layout = layoutOf(child.attrs.embed);
|
|
1243
|
+
if (layout && child.attrs.uri) cards.push({ uri: String(child.attrs.uri), layout });
|
|
1244
|
+
});
|
|
1245
|
+
if (cards.length) {
|
|
1246
|
+
const id = node.attrs.blockId ?? parent?.attrs.blockId;
|
|
1247
|
+
const base = id ? `b:${String(id)}` : `p:${pos}`;
|
|
1248
|
+
const n = seen.get(base) ?? 0;
|
|
1249
|
+
seen.set(base, n + 1);
|
|
1250
|
+
out.push({ key: n ? `${base}:${n}` : base, pos: pos + node.nodeSize - 1, cards });
|
|
1251
|
+
}
|
|
1252
|
+
return false;
|
|
1253
|
+
});
|
|
1254
|
+
return out;
|
|
1255
|
+
}
|
|
1256
|
+
var key2 = new PluginKey5("referenceCards");
|
|
1257
|
+
function elementFor(storage, slotKey) {
|
|
1258
|
+
let element = storage.elements.get(slotKey);
|
|
1259
|
+
if (!element) {
|
|
1260
|
+
element = document.createElement("div");
|
|
1261
|
+
element.setAttribute("data-reference-cards", slotKey);
|
|
1262
|
+
element.setAttribute("contenteditable", "false");
|
|
1263
|
+
storage.elements.set(slotKey, element);
|
|
1264
|
+
}
|
|
1265
|
+
return element;
|
|
1266
|
+
}
|
|
1267
|
+
function storageOf(editor) {
|
|
1268
|
+
return editor.storage.referenceCards;
|
|
1269
|
+
}
|
|
1270
|
+
var ReferenceCards = Extension5.create({
|
|
1271
|
+
name: "referenceCards",
|
|
1272
|
+
addStorage() {
|
|
1273
|
+
return { elements: /* @__PURE__ */ new Map() };
|
|
1274
|
+
},
|
|
1275
|
+
addProseMirrorPlugins() {
|
|
1276
|
+
const storage = this.storage;
|
|
1277
|
+
const decorate = (doc) => DecorationSet2.create(
|
|
1278
|
+
doc,
|
|
1279
|
+
referenceCardPlacements(doc).map(
|
|
1280
|
+
(p) => Decoration2.widget(p.pos, () => elementFor(storage, p.key), {
|
|
1281
|
+
key: p.key,
|
|
1282
|
+
side: 1,
|
|
1283
|
+
ignoreSelection: true,
|
|
1284
|
+
// A click on a card is the card's, not a caret placement in the line.
|
|
1285
|
+
stopEvent: () => true
|
|
1286
|
+
})
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
return [
|
|
1290
|
+
new Plugin4({
|
|
1291
|
+
key: key2,
|
|
1292
|
+
state: {
|
|
1293
|
+
init: (_, state) => decorate(state.doc),
|
|
1294
|
+
apply: (tr, set) => tr.docChanged ? decorate(tr.doc) : set
|
|
1295
|
+
},
|
|
1296
|
+
props: { decorations: (state) => key2.getState(state) }
|
|
1297
|
+
})
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
function useReferenceCards(editor) {
|
|
1302
|
+
const [slots, setSlots] = useState3([]);
|
|
1303
|
+
useEffect3(() => {
|
|
1304
|
+
if (!editor) return;
|
|
1305
|
+
const storage = storageOf(editor);
|
|
1306
|
+
if (!storage) return;
|
|
1307
|
+
let last = "";
|
|
1308
|
+
const read = () => {
|
|
1309
|
+
if (editor.isDestroyed) return;
|
|
1310
|
+
const next = referenceCardPlacements(editor.state.doc).map((p) => ({ key: p.key, element: elementFor(storage, p.key), cards: p.cards }));
|
|
1311
|
+
const signature = JSON.stringify(next.map((s) => [s.key, s.cards]));
|
|
1312
|
+
if (signature === last) return;
|
|
1313
|
+
last = signature;
|
|
1314
|
+
const live = new Set(next.map((s) => s.key));
|
|
1315
|
+
for (const k of storage.elements.keys()) if (!live.has(k)) storage.elements.delete(k);
|
|
1316
|
+
setSlots(next);
|
|
1317
|
+
};
|
|
1318
|
+
read();
|
|
1319
|
+
editor.on("transaction", read);
|
|
1320
|
+
return () => {
|
|
1321
|
+
editor.off("transaction", read);
|
|
1322
|
+
};
|
|
1323
|
+
}, [editor]);
|
|
1324
|
+
return slots;
|
|
1325
|
+
}
|
|
1326
|
+
function referenceAtSelection(state) {
|
|
1327
|
+
const { selection } = state;
|
|
1328
|
+
let found = null;
|
|
1329
|
+
if (selection instanceof NodeSelection) {
|
|
1330
|
+
if (selection.node.type.name === "reference") found = { pos: selection.from, node: selection.node };
|
|
1331
|
+
} else if (!selection.empty) {
|
|
1332
|
+
let only = true;
|
|
1333
|
+
state.doc.nodesBetween(selection.from, selection.to, (node2, pos2) => {
|
|
1334
|
+
if (node2.type.name === "reference") {
|
|
1335
|
+
if (found) only = false;
|
|
1336
|
+
found = { pos: pos2, node: node2 };
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
if (node2.isText) {
|
|
1340
|
+
const from = Math.max(selection.from, pos2) - pos2;
|
|
1341
|
+
const to = Math.min(selection.to, pos2 + node2.nodeSize) - pos2;
|
|
1342
|
+
if ((node2.text ?? "").slice(from, to).trim() !== "") only = false;
|
|
1343
|
+
}
|
|
1344
|
+
return true;
|
|
1345
|
+
});
|
|
1346
|
+
if (!only) found = null;
|
|
1347
|
+
}
|
|
1348
|
+
if (!found) return null;
|
|
1349
|
+
const { pos, node } = found;
|
|
1350
|
+
if (!node.attrs.uri) return null;
|
|
1351
|
+
return { pos, uri: String(node.attrs.uri ?? ""), layout: layoutOf(node.attrs.embed) ?? "none" };
|
|
1352
|
+
}
|
|
1353
|
+
function setReferenceCard(editor, layout) {
|
|
1354
|
+
const at = referenceAtSelection(editor.state);
|
|
1355
|
+
return at ? setReferenceCardAt(editor, at.pos, layout) : false;
|
|
1356
|
+
}
|
|
1357
|
+
function setReferenceCardAt(editor, pos, layout) {
|
|
1358
|
+
const node = editor.state.doc.nodeAt(pos);
|
|
1359
|
+
if (!node || node.type.name !== "reference" || !node.attrs.uri) return false;
|
|
1360
|
+
const wasNode = editor.state.selection instanceof NodeSelection && editor.state.selection.from === pos;
|
|
1361
|
+
const tr = editor.state.tr.setNodeMarkup(pos, void 0, { ...node.attrs, embed: layout === "none" ? null : layout });
|
|
1362
|
+
if (wasNode) tr.setSelection(NodeSelection.create(tr.doc, pos));
|
|
1363
|
+
editor.view.dispatch(tr);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/ReferenceMenu.tsx
|
|
1368
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1369
|
+
var key3 = new PluginKey6("referenceMenu");
|
|
1370
|
+
var LAYOUTS = [
|
|
1371
|
+
{ layout: "none", label: "None" },
|
|
1372
|
+
{ layout: "compact", label: "Compact" },
|
|
1373
|
+
{ layout: "detailed", label: "Detailed" }
|
|
1374
|
+
];
|
|
1375
|
+
function chipTarget(view, pos) {
|
|
1376
|
+
const dom = view.nodeDOM(pos);
|
|
1377
|
+
return dom instanceof HTMLElement ? dom.querySelector("a[href], button") : null;
|
|
1378
|
+
}
|
|
1379
|
+
function webHref(view, pos) {
|
|
1380
|
+
const href = view.state.doc.nodeAt(pos)?.attrs.href;
|
|
1381
|
+
return typeof href === "string" && /^https?:\/\//i.test(href) ? href : "";
|
|
1382
|
+
}
|
|
1383
|
+
function rowsOf(view, pos) {
|
|
1384
|
+
const open = chipTarget(view, pos) || webHref(view, pos) ? ["open"] : [];
|
|
1385
|
+
return view.state.doc.nodeAt(pos)?.attrs.uri ? [...open, ...LAYOUTS.map((card) => card.layout)] : open;
|
|
1386
|
+
}
|
|
1387
|
+
function chipAt(state, pos) {
|
|
1388
|
+
const node = state.doc.nodeAt(pos);
|
|
1389
|
+
return node && node.type.name === "reference" ? node : null;
|
|
1390
|
+
}
|
|
1391
|
+
function justPlaced(tr, state) {
|
|
1392
|
+
if (!tr.docChanged || tr.getMeta("history$") || tr.getMeta("preventUpdate")) return null;
|
|
1393
|
+
const whole = tr.steps.some((step) => step instanceof ReplaceStep && step.from === 0 && step.to === tr.before.content.size);
|
|
1394
|
+
if (whole && tr.getMeta("uiEvent") !== "paste") return null;
|
|
1395
|
+
const made = tr.getMeta(CHIPS_MADE);
|
|
1396
|
+
if (made && !made.pasted) return null;
|
|
1397
|
+
const { selection } = state;
|
|
1398
|
+
if (!selection.empty) return null;
|
|
1399
|
+
let at = selection.from;
|
|
1400
|
+
let before = selection.$from.nodeBefore;
|
|
1401
|
+
if (!made && before?.isText && before.text === " ") {
|
|
1402
|
+
at -= 1;
|
|
1403
|
+
before = state.doc.resolve(at).nodeBefore;
|
|
1404
|
+
}
|
|
1405
|
+
if (!before || before.type.name !== "reference" || !before.attrs.uri) return null;
|
|
1406
|
+
const pos = at - before.nodeSize;
|
|
1407
|
+
const old = tr.mapping.invert().map(pos);
|
|
1408
|
+
const was = tr.before.nodeAt(old);
|
|
1409
|
+
if (was && was.type.name === "reference" && was.attrs.uri === before.attrs.uri && tr.mapping.map(old, 1) === pos) return null;
|
|
1410
|
+
return pos;
|
|
1411
|
+
}
|
|
1412
|
+
function dispatchMenu(view, next) {
|
|
1413
|
+
view.dispatch(view.state.tr.setMeta(key3, next));
|
|
1414
|
+
}
|
|
1415
|
+
var passing = false;
|
|
1416
|
+
function openChip(view, pos) {
|
|
1417
|
+
const target = chipTarget(view, pos);
|
|
1418
|
+
if (target) {
|
|
1419
|
+
passing = true;
|
|
1420
|
+
try {
|
|
1421
|
+
target.click();
|
|
1422
|
+
} finally {
|
|
1423
|
+
passing = false;
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const href = webHref(view, pos);
|
|
1428
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
1429
|
+
}
|
|
1430
|
+
function choose(editor, pos, row) {
|
|
1431
|
+
dispatchMenu(editor.view, null);
|
|
1432
|
+
if (row === "open") openChip(editor.view, pos);
|
|
1433
|
+
else if (row) setReferenceCardAt(editor, pos, row);
|
|
1434
|
+
}
|
|
1435
|
+
function isReferenceMenuOpen(state) {
|
|
1436
|
+
return !!key3.getState(state);
|
|
1437
|
+
}
|
|
1438
|
+
function referenceMenuClick(editor, pos) {
|
|
1439
|
+
if (passing || editor.isDestroyed || !editor.isEditable || key3.getState(editor.state) === void 0) return false;
|
|
1440
|
+
if (!chipAt(editor.state, pos) || rowsOf(editor.view, pos).length === 0) return false;
|
|
1441
|
+
editor.view.dispatch(caretAfter(editor.state, pos).setMeta(key3, { pos, active: -1 }));
|
|
1442
|
+
return true;
|
|
1443
|
+
}
|
|
1444
|
+
function caretAfter(state, pos) {
|
|
1445
|
+
const node = state.doc.nodeAt(pos);
|
|
1446
|
+
return state.tr.setSelection(TextSelection2.create(state.doc, pos + (node?.nodeSize ?? 1)));
|
|
1447
|
+
}
|
|
1448
|
+
var ReferenceMenu = Extension6.create({
|
|
1449
|
+
name: "referenceMenu",
|
|
1450
|
+
// Before the editor's own keys, so Enter on a highlighted row is the row's, not a new line.
|
|
1451
|
+
priority: 1e3,
|
|
1452
|
+
addProseMirrorPlugins() {
|
|
1453
|
+
const editor = this.editor;
|
|
1454
|
+
return [
|
|
1455
|
+
new Plugin5({
|
|
1456
|
+
key: key3,
|
|
1457
|
+
state: {
|
|
1458
|
+
init: () => null,
|
|
1459
|
+
apply: (tr, value, before, after) => {
|
|
1460
|
+
const meta = tr.getMeta(key3);
|
|
1461
|
+
if (meta !== void 0) return meta;
|
|
1462
|
+
const placed = justPlaced(tr, after);
|
|
1463
|
+
if (placed !== null) return { pos: placed, active: -1 };
|
|
1464
|
+
if (!value) return null;
|
|
1465
|
+
const pos = tr.mapping.map(value.pos);
|
|
1466
|
+
if (!chipAt(after, pos)) return null;
|
|
1467
|
+
if (tr.docChanged && !tr.getMeta("appendedTransaction") && tr.getMeta(CHIPS_MADE) === void 0) return null;
|
|
1468
|
+
const onChip = after.selection instanceof NodeSelection2 && after.selection.from === pos;
|
|
1469
|
+
if (tr.selectionSet && !after.selection.eq(before.selection) && !onChip) return null;
|
|
1470
|
+
return pos === value.pos ? value : { ...value, pos };
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
props: {
|
|
1474
|
+
handleDOMEvents: {
|
|
1475
|
+
// Focus gone, the keys no longer reach the menu, and a card chosen now would miss the save on leaving (code review).
|
|
1476
|
+
blur: (view) => {
|
|
1477
|
+
if (key3.getState(view.state)) dispatchMenu(view, null);
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
handleKeyDown: (view, event) => {
|
|
1482
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
1483
|
+
const open = key3.getState(view.state);
|
|
1484
|
+
if (!open) {
|
|
1485
|
+
const { selection } = view.state;
|
|
1486
|
+
if (event.key !== "Enter" || !view.editable || !(selection instanceof NodeSelection2) || selection.node.type.name !== "reference") return false;
|
|
1487
|
+
if (rowsOf(view, selection.from).length === 0) return false;
|
|
1488
|
+
view.dispatch(caretAfter(view.state, selection.from).setMeta(key3, { pos: selection.from, active: 0 }));
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
const rows = rowsOf(view, open.pos);
|
|
1492
|
+
if (event.key === "Escape") {
|
|
1493
|
+
dispatchMenu(view, null);
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
if (rows.length === 0) return false;
|
|
1497
|
+
if (event.key === "ArrowDown") {
|
|
1498
|
+
dispatchMenu(view, { ...open, active: (open.active + 1) % rows.length });
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (event.key === "ArrowUp") {
|
|
1502
|
+
dispatchMenu(view, { ...open, active: open.active <= 0 ? rows.length - 1 : open.active - 1 });
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
if (event.key === "Enter" && open.active >= 0) {
|
|
1506
|
+
choose(editor, open.pos, rows[open.active]);
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
})
|
|
1513
|
+
];
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
function ReferenceMenuView({ editor }) {
|
|
1517
|
+
const shown = useEditorState({
|
|
1518
|
+
editor,
|
|
1519
|
+
selector: ({ editor: ed }) => {
|
|
1520
|
+
if (!ed || ed.isDestroyed) return null;
|
|
1521
|
+
const open = key3.getState(ed.state);
|
|
1522
|
+
if (!open) return null;
|
|
1523
|
+
const node = chipAt(ed.state, open.pos);
|
|
1524
|
+
if (!node) return null;
|
|
1525
|
+
const embed = node.attrs.embed;
|
|
1526
|
+
const layout = embed === "detailed" ? "detailed" : typeof embed === "string" && embed !== "" && embed !== "none" ? "compact" : "none";
|
|
1527
|
+
return { pos: open.pos, active: open.active, named: !!node.attrs.uri, layout };
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
const keepFocus2 = useCallback3((list) => {
|
|
1531
|
+
const panel = list?.closest('[role="dialog"]');
|
|
1532
|
+
if (!panel) return;
|
|
1533
|
+
const keep = (event) => event.preventDefault();
|
|
1534
|
+
panel.addEventListener("mousedown", keep);
|
|
1535
|
+
return () => panel.removeEventListener("mousedown", keep);
|
|
1536
|
+
}, []);
|
|
1537
|
+
const live = shown && editor && !editor.isDestroyed ? editor : null;
|
|
1538
|
+
const dom = live && shown ? live.view.nodeDOM(shown.pos) : null;
|
|
1539
|
+
const anchor = dom instanceof HTMLElement ? dom : null;
|
|
1540
|
+
const rows = live && shown ? rowsOf(live.view, shown.pos) : [];
|
|
1541
|
+
const opens = rows[0] === "open";
|
|
1542
|
+
const run = (row) => {
|
|
1543
|
+
if (editor && shown) choose(editor, shown.pos, row);
|
|
1544
|
+
};
|
|
1545
|
+
return (
|
|
1546
|
+
// finalFocus={false}: focus never left the text.
|
|
1547
|
+
/* @__PURE__ */ jsx6(
|
|
1548
|
+
Popover2,
|
|
1549
|
+
{
|
|
1550
|
+
anchor,
|
|
1551
|
+
open: !!shown && !!anchor && rows.length > 0,
|
|
1552
|
+
onOpenChange: (next) => {
|
|
1553
|
+
if (!next && editor && !editor.isDestroyed && key3.getState(editor.state)) dispatchMenu(editor.view, null);
|
|
1554
|
+
},
|
|
1555
|
+
side: "bottom",
|
|
1556
|
+
align: "left",
|
|
1557
|
+
ariaLabel: "Link",
|
|
1558
|
+
finalFocus: false,
|
|
1559
|
+
children: shown && /* @__PURE__ */ jsxs5("div", { ref: keepFocus2, role: "menu", "aria-label": "Link", className: "flex flex-col gap-0.5", children: [
|
|
1560
|
+
opens && /* @__PURE__ */ jsx6(
|
|
1561
|
+
MenuItem3,
|
|
1562
|
+
{
|
|
1563
|
+
role: "menuitem",
|
|
1564
|
+
tabIndex: -1,
|
|
1565
|
+
label: "Open",
|
|
1566
|
+
selected: shown.active === 0,
|
|
1567
|
+
onMouseDown: (event) => {
|
|
1568
|
+
event.preventDefault();
|
|
1569
|
+
run("open");
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
),
|
|
1573
|
+
shown.named && /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
1574
|
+
opens && /* @__PURE__ */ jsx6(MenuSeparator2, {}),
|
|
1575
|
+
/* @__PURE__ */ jsx6("div", { role: "group", "aria-label": "Card", children: /* @__PURE__ */ jsx6(MenuSection, { label: "Card", children: LAYOUTS.map(({ layout, label }, i) => /* @__PURE__ */ jsx6(
|
|
1576
|
+
MenuItem3,
|
|
1577
|
+
{
|
|
1578
|
+
role: "menuitemradio",
|
|
1579
|
+
"aria-checked": shown.layout === layout,
|
|
1580
|
+
tabIndex: -1,
|
|
1581
|
+
label,
|
|
1582
|
+
selected: shown.active === i + (opens ? 1 : 0),
|
|
1583
|
+
trailing: shown.layout === layout ? /* @__PURE__ */ jsx6(IconCheck, { size: 16, "aria-hidden": true }) : void 0,
|
|
1584
|
+
onMouseDown: (event) => {
|
|
1585
|
+
event.preventDefault();
|
|
1586
|
+
run(layout);
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
layout
|
|
1590
|
+
)) }) })
|
|
1591
|
+
] })
|
|
1592
|
+
] })
|
|
1593
|
+
}
|
|
1594
|
+
)
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/BlockNodes.tsx
|
|
1599
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1073
1600
|
var BLOCK_TYPES = [
|
|
1074
1601
|
"paragraph",
|
|
1075
1602
|
"heading",
|
|
@@ -1227,7 +1754,7 @@ function bringsInBlocks(transactions) {
|
|
|
1227
1754
|
})
|
|
1228
1755
|
);
|
|
1229
1756
|
}
|
|
1230
|
-
var BlockId =
|
|
1757
|
+
var BlockId = Extension7.create({
|
|
1231
1758
|
name: "blockId",
|
|
1232
1759
|
onCreate() {
|
|
1233
1760
|
const { state, view } = this.editor;
|
|
@@ -1239,8 +1766,8 @@ var BlockId = Extension4.create({
|
|
|
1239
1766
|
},
|
|
1240
1767
|
addProseMirrorPlugins() {
|
|
1241
1768
|
return [
|
|
1242
|
-
new
|
|
1243
|
-
key: new
|
|
1769
|
+
new Plugin6({
|
|
1770
|
+
key: new PluginKey7("blockIdSplit"),
|
|
1244
1771
|
appendTransaction: (transactions, old, state) => {
|
|
1245
1772
|
if (!bringsInBlocks(transactions)) return null;
|
|
1246
1773
|
const mapping = new Mapping();
|
|
@@ -1274,6 +1801,7 @@ var BlockId = Extension4.create({
|
|
|
1274
1801
|
];
|
|
1275
1802
|
}
|
|
1276
1803
|
});
|
|
1804
|
+
var webUrl = (raw) => raw && /^https?:\/\/\S+$/i.test(raw) ? raw : null;
|
|
1277
1805
|
var ReferenceNode = Node2.create({
|
|
1278
1806
|
name: "reference",
|
|
1279
1807
|
group: "inline",
|
|
@@ -1286,20 +1814,48 @@ var ReferenceNode = Node2.create({
|
|
|
1286
1814
|
},
|
|
1287
1815
|
addAttributes() {
|
|
1288
1816
|
return {
|
|
1289
|
-
|
|
1290
|
-
|
|
1817
|
+
// A pasted URL chip keeps only its URL: what it names is the editor's to resolve (`LinkChips`), never
|
|
1818
|
+
// the clipboard's, which could show one object while saving another URL (security review).
|
|
1819
|
+
uri: { default: "", parseHTML: (element) => webUrl(element.getAttribute("data-href")) ? "" : element.getAttribute("data-reference") ?? "" },
|
|
1820
|
+
marks: { default: [] },
|
|
1821
|
+
embed: { default: null, parseHTML: (element) => element.getAttribute("data-embed") },
|
|
1822
|
+
extra: { default: null, parseHTML: () => null },
|
|
1823
|
+
// An http(s) URL only: `LinkChips` makes no other, and a pasted `javascript:` must not reach a saved link.
|
|
1824
|
+
href: { default: null, parseHTML: (element) => webUrl(element.getAttribute("data-href")) }
|
|
1291
1825
|
};
|
|
1292
1826
|
},
|
|
1293
1827
|
parseHTML() {
|
|
1294
1828
|
return [{ tag: "span[data-reference]" }];
|
|
1295
1829
|
},
|
|
1296
1830
|
renderHTML({ node }) {
|
|
1297
|
-
|
|
1831
|
+
const embed = typeof node.attrs.embed === "string" ? { "data-embed": node.attrs.embed } : {};
|
|
1832
|
+
const href = typeof node.attrs.href === "string" && node.attrs.href ? { "data-href": node.attrs.href } : {};
|
|
1833
|
+
return ["span", { "data-reference": String(node.attrs.uri), ...embed, ...href }, String(node.attrs.href || node.attrs.uri)];
|
|
1834
|
+
},
|
|
1835
|
+
renderText({ node }) {
|
|
1836
|
+
return String(node.attrs.href || node.attrs.uri);
|
|
1298
1837
|
},
|
|
1299
1838
|
addNodeView() {
|
|
1300
1839
|
const View = this.options.view;
|
|
1301
1840
|
if (!View) return null;
|
|
1302
|
-
return ReactNodeViewRenderer2(({ node
|
|
1841
|
+
return ReactNodeViewRenderer2(({ node, editor, getPos }) => {
|
|
1842
|
+
const uri = String(node.attrs.uri ?? "");
|
|
1843
|
+
const href = typeof node.attrs.href === "string" && node.attrs.href ? node.attrs.href : void 0;
|
|
1844
|
+
return /* @__PURE__ */ jsx7(
|
|
1845
|
+
NodeViewWrapper2,
|
|
1846
|
+
{
|
|
1847
|
+
as: "span",
|
|
1848
|
+
className: "inline",
|
|
1849
|
+
onClickCapture: (event) => {
|
|
1850
|
+
const pos = getPos();
|
|
1851
|
+
if (typeof pos !== "number" || !referenceMenuClick(editor, pos)) return;
|
|
1852
|
+
event.preventDefault();
|
|
1853
|
+
event.stopPropagation();
|
|
1854
|
+
},
|
|
1855
|
+
children: uri ? /* @__PURE__ */ jsx7(View, { uri, ...href ? { href } : {} }) : /* @__PURE__ */ jsx7("span", { children: href })
|
|
1856
|
+
}
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1303
1859
|
}
|
|
1304
1860
|
});
|
|
1305
1861
|
var UnknownBlock = Node2.create({
|
|
@@ -1327,7 +1883,7 @@ var UnknownBlock = Node2.create({
|
|
|
1327
1883
|
function UnknownBlockView({ node }) {
|
|
1328
1884
|
const source = node.attrs.source;
|
|
1329
1885
|
const text = String(node.attrs.text ?? "");
|
|
1330
|
-
return /* @__PURE__ */
|
|
1886
|
+
return /* @__PURE__ */ jsx7(
|
|
1331
1887
|
NodeViewWrapper2,
|
|
1332
1888
|
{
|
|
1333
1889
|
"data-block-id": source?.id,
|
|
@@ -1367,7 +1923,7 @@ var AttachmentNode = Node2.create({
|
|
|
1367
1923
|
addNodeView() {
|
|
1368
1924
|
const View = this.options.view;
|
|
1369
1925
|
if (!View) return null;
|
|
1370
|
-
return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */
|
|
1926
|
+
return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */ jsx7(NodeViewWrapper2, { className: "my-2", children: /* @__PURE__ */ jsx7(View, { attrs: node.attrs }) }));
|
|
1371
1927
|
}
|
|
1372
1928
|
});
|
|
1373
1929
|
var TICK = "checklistTick";
|
|
@@ -1385,7 +1941,7 @@ function itemAtCaret(state) {
|
|
|
1385
1941
|
if (item.type.name !== "listItem" || $from.index(-1) !== 0) return null;
|
|
1386
1942
|
return { node: item, pos: $from.before(-1) };
|
|
1387
1943
|
}
|
|
1388
|
-
var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((
|
|
1944
|
+
var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((key4) => a.attrs[key4] === b.attrs[key4]);
|
|
1389
1945
|
function listItemView(editor, node, view, getPos) {
|
|
1390
1946
|
const toDOM = node.type.spec.toDOM;
|
|
1391
1947
|
const { dom, contentDOM } = toDOM ? DOMSerializer.renderSpec(document, toDOM(node)) : { dom: document.createElement("li"), contentDOM: void 0 };
|
|
@@ -1436,7 +1992,7 @@ function listItemView(editor, node, view, getPos) {
|
|
|
1436
1992
|
}
|
|
1437
1993
|
};
|
|
1438
1994
|
}
|
|
1439
|
-
var Checklist =
|
|
1995
|
+
var Checklist = Extension7.create({
|
|
1440
1996
|
name: "checklist",
|
|
1441
1997
|
// Before the list item's own Enter and Backspace.
|
|
1442
1998
|
priority: 101,
|
|
@@ -1501,14 +2057,14 @@ var Checklist = Extension4.create({
|
|
|
1501
2057
|
addProseMirrorPlugins() {
|
|
1502
2058
|
const editor = this.editor;
|
|
1503
2059
|
return [
|
|
1504
|
-
new
|
|
1505
|
-
key: new
|
|
2060
|
+
new Plugin6({
|
|
2061
|
+
key: new PluginKey7("checklist"),
|
|
1506
2062
|
props: { nodeViews: { listItem: (node, view, getPos) => listItemView(editor, node, view, getPos) } }
|
|
1507
2063
|
})
|
|
1508
2064
|
];
|
|
1509
2065
|
}
|
|
1510
2066
|
});
|
|
1511
|
-
var Callout =
|
|
2067
|
+
var Callout = Extension7.create({
|
|
1512
2068
|
name: "callout",
|
|
1513
2069
|
addGlobalAttributes() {
|
|
1514
2070
|
return [
|
|
@@ -1537,8 +2093,12 @@ export {
|
|
|
1537
2093
|
EditorTableControls,
|
|
1538
2094
|
FORMATS,
|
|
1539
2095
|
KeptSelection,
|
|
2096
|
+
LinkChips,
|
|
1540
2097
|
PastedImages,
|
|
1541
2098
|
PersonMention,
|
|
2099
|
+
ReferenceCards,
|
|
2100
|
+
ReferenceMenu,
|
|
2101
|
+
ReferenceMenuView,
|
|
1542
2102
|
ReferenceNode,
|
|
1543
2103
|
ReferenceTrigger,
|
|
1544
2104
|
SelectionToolbar,
|
|
@@ -1556,8 +2116,10 @@ export {
|
|
|
1556
2116
|
filterPeople,
|
|
1557
2117
|
filterReferences,
|
|
1558
2118
|
filterSlashSections,
|
|
2119
|
+
findLinkChips,
|
|
1559
2120
|
formatSection,
|
|
1560
2121
|
isChecklistTick,
|
|
2122
|
+
isReferenceMenuOpen,
|
|
1561
2123
|
isSuggestionActive,
|
|
1562
2124
|
isSuggestionOpen,
|
|
1563
2125
|
moveBlock,
|
|
@@ -1565,10 +2127,17 @@ export {
|
|
|
1565
2127
|
pastedImageName,
|
|
1566
2128
|
pastedImages,
|
|
1567
2129
|
peopleMenu,
|
|
2130
|
+
referenceAtSelection,
|
|
2131
|
+
referenceCardPlacements,
|
|
1568
2132
|
referenceMenu,
|
|
2133
|
+
referenceMenuClick,
|
|
2134
|
+
refreshLinkChips,
|
|
2135
|
+
setReferenceCard,
|
|
2136
|
+
setReferenceCardAt,
|
|
1569
2137
|
startBlockDrag,
|
|
1570
2138
|
suggestionPopup,
|
|
1571
2139
|
typeTrigger,
|
|
1572
|
-
urgentPubkeys
|
|
2140
|
+
urgentPubkeys,
|
|
2141
|
+
useReferenceCards
|
|
1573
2142
|
};
|
|
1574
2143
|
//# sourceMappingURL=editor.js.map
|