blixify-ui-web 1.2.126 → 1.2.128

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.
@@ -90,6 +90,7 @@ var TableCellsIcon_1 = __importDefault(require("@heroicons/react/24/solid/TableC
90
90
  var core_1 = require("@tiptap/core");
91
91
  var filePreview_1 = require("../../display/filePreview");
92
92
  var state_1 = require("@tiptap/pm/state");
93
+ var view_1 = require("@tiptap/pm/view");
93
94
  var extension_image_1 = __importDefault(require("@tiptap/extension-image"));
94
95
  var extensions_1 = require("@tiptap/extensions");
95
96
  var extension_subscript_1 = __importDefault(require("@tiptap/extension-subscript"));
@@ -108,6 +109,7 @@ var turndown_1 = __importDefault(require("turndown"));
108
109
  var infoWindow_1 = require("../../advanced/infoWindow");
109
110
  var loading_1 = require("../../display/loading");
110
111
  var mermaid_1 = require("./mermaid");
112
+ var headings_1 = require("./headings");
111
113
  var md = new markdown_it_1.default({
112
114
  html: true,
113
115
  breaks: true,
@@ -222,6 +224,17 @@ turndownService.addRule("image", {
222
224
  var alt = img.getAttribute("alt") || "";
223
225
  var src = img.getAttribute("src") || "";
224
226
  var title = img.getAttribute("title") || "";
227
+ var width = img.getAttribute("width") || "";
228
+ // INFO: markdown has no syntax for a size, so a resized image is written as
229
+ // html — the same route <video>, the file attachment and the space embed
230
+ // take, and markdown-it runs with html:true so it parses back on load.
231
+ // Written ONLY when a width was actually set: an image nobody has resized
232
+ // still serialises as ![alt](src), so every document saved before resizing
233
+ // existed round-trips byte-for-byte.
234
+ if (width) {
235
+ var titleAttr = title ? " title=\"".concat(title, "\"") : "";
236
+ return "<img src=\"".concat(src, "\" alt=\"").concat(alt, "\"").concat(titleAttr, " width=\"").concat(width, "\" />");
237
+ }
225
238
  if (title) {
226
239
  return "![".concat(alt, "](").concat(src, " \"").concat(title, "\")");
227
240
  }
@@ -771,6 +784,40 @@ var TrailingParagraph = core_1.Extension.create({
771
784
  ];
772
785
  },
773
786
  });
787
+ /**
788
+ * Gives every h1-h3 in the editor the id its outline entry scrolls to.
789
+ *
790
+ * INFO: a view decoration, so it is recomputed from the document on every redraw
791
+ * and never enters the document itself. The two alternatives both fail: a node
792
+ * attribute reaches `getHTML()` and would be written into the saved markdown,
793
+ * and a DOM write is undone the next time ProseMirror redraws the heading.
794
+ */
795
+ var HeadingAnchors = core_1.Extension.create({
796
+ name: "headingAnchors",
797
+ addProseMirrorPlugins: function () {
798
+ return [
799
+ new state_1.Plugin({
800
+ key: new state_1.PluginKey("headingAnchors"),
801
+ props: {
802
+ // INFO: cast because the tree carries two copies of prosemirror-view —
803
+ // one hoisted, one under @tiptap/pm — so the DecorationSet built from
804
+ // `@tiptap/pm/view` is a different nominal type to the one tiptap's own
805
+ // Plugin signature was compiled against, despite being the same class
806
+ // at runtime. Importing the hoisted copy directly would fix the types
807
+ // by adding an undeclared dependency, which is the worse trade.
808
+ decorations: function (state) {
809
+ return view_1.DecorationSet.create(state.doc, (0, headings_1.handleBuildHeadingAnchors)(state.doc).map(function (eachAnchor) {
810
+ return view_1.Decoration.node(eachAnchor.from, eachAnchor.to, {
811
+ id: eachAnchor.id,
812
+ "data-heading-id": eachAnchor.id,
813
+ });
814
+ }));
815
+ },
816
+ },
817
+ }),
818
+ ];
819
+ },
820
+ });
774
821
  var SpaceEmbed = core_1.Node.create({
775
822
  name: "spaceEmbed",
776
823
  group: "block",
@@ -944,6 +991,206 @@ var DiagramCodeBlock = extension_code_block_1.default.extend({
944
991
  return (0, react_1.ReactNodeViewRenderer)(CodeBlockComponent);
945
992
  },
946
993
  });
994
+ // INFO: below this an image is a thumbnail nobody can see the content of, and
995
+ // dragging past it is almost always an accident
996
+ var MIN_IMAGE_WIDTH = 80;
997
+ /**
998
+ * An image with drag handles on its left and right edges.
999
+ *
1000
+ * INFO: the width is previewed in local state while dragging and only committed
1001
+ * to the node on release. Writing it on every pointermove would put one
1002
+ * transaction per mouse move into the history — undo would take fifty presses to
1003
+ * get back — and each one fires onUpdate, which in a host with autosave is fifty
1004
+ * saves for one resize.
1005
+ */
1006
+ var ResizableImageComponent = function (_a) {
1007
+ var _b;
1008
+ var node = _a.node, updateAttributes = _a.updateAttributes, editor = _a.editor, selected = _a.selected;
1009
+ var _c = (0, react_2.useState)(null), preview = _c[0], setPreview = _c[1];
1010
+ var _d = (0, react_2.useState)(false), hovered = _d[0], setHovered = _d[1];
1011
+ var _e = (0, react_2.useState)(null), dragging = _e[0], setDragging = _e[1];
1012
+ var imageRef = (0, react_2.useRef)(null);
1013
+ var startRef = (0, react_2.useRef)({ x: 0, width: 0 });
1014
+ // INFO: the pointerup handler needs the width the last move produced, and it
1015
+ // is registered once — reading it from state would read the value from the
1016
+ // render that registered it, which is the width before the drag started
1017
+ var previewRef = (0, react_2.useRef)(null);
1018
+ var _f = node.attrs, src = _f.src, alt = _f.alt, title = _f.title, width = _f.width;
1019
+ var editable = !!(editor === null || editor === void 0 ? void 0 : editor.isEditable);
1020
+ // INFO: the preview wins while dragging so the image tracks the pointer
1021
+ // without the document being edited on the way
1022
+ var shownWidth = (_b = preview !== null && preview !== void 0 ? preview : width) !== null && _b !== void 0 ? _b : null;
1023
+ (0, react_2.useEffect)(function () {
1024
+ if (!dragging)
1025
+ return;
1026
+ return handleObserveDrag();
1027
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1028
+ }, [dragging]);
1029
+ var handleObserveDrag = function () {
1030
+ var edge = dragging;
1031
+ var handleMove = function (event) {
1032
+ var _a, _b, _c;
1033
+ var delta = event.clientX - startRef.current.x;
1034
+ // INFO: dragging the left handle outward is dragging left, so the sign
1035
+ // flips — the alternative is a handle that shrinks when pulled away
1036
+ var next = startRef.current.width + (edge === "left" ? -delta : delta);
1037
+ // INFO: an event that carries no coordinate makes this NaN, and NaN is
1038
+ // truthy enough to be stored and falsy enough to render as no width at
1039
+ // all — so the image would silently lose the size it already had. There is
1040
+ // nothing to resize to without a coordinate, so the move is ignored.
1041
+ if (!Number.isFinite(next))
1042
+ return;
1043
+ // INFO: capped at the width of the COLUMN the image sits in, because a
1044
+ // wider image would be clipped by the page with no handle left to drag it
1045
+ // back. Measured from the editor element, not from the node view wrapper:
1046
+ // the wrapper is `fit-content` so the selection outline hugs the image,
1047
+ // which means measuring it caps the image at its own current width — an
1048
+ // image could then be shrunk but never grown again. A container that
1049
+ // cannot be measured is no reason to refuse the resize.
1050
+ var column = (_c = (_b = (_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.closest(".ProseMirror")) === null || _b === void 0 ? void 0 : _b.clientWidth) !== null && _c !== void 0 ? _c : 0;
1051
+ var max = column > 0 ? column : Number.MAX_SAFE_INTEGER;
1052
+ var resolved = Math.round(Math.max(MIN_IMAGE_WIDTH, Math.min(next, max)));
1053
+ previewRef.current = resolved;
1054
+ setPreview(resolved);
1055
+ };
1056
+ /**
1057
+ * INFO: the single point where the document is edited. Committing here
1058
+ * rather than from the effect's cleanup keeps it out of a state updater —
1059
+ * React may run an updater more than once, and a write to the document is
1060
+ * not something that may happen twice.
1061
+ */
1062
+ var handleUp = function () {
1063
+ var resolved = previewRef.current;
1064
+ previewRef.current = null;
1065
+ setDragging(null);
1066
+ setPreview(null);
1067
+ if (resolved !== null && resolved !== width)
1068
+ updateAttributes({ width: resolved });
1069
+ };
1070
+ window.addEventListener("pointermove", handleMove);
1071
+ window.addEventListener("pointerup", handleUp);
1072
+ window.addEventListener("pointercancel", handleUp);
1073
+ return function () {
1074
+ window.removeEventListener("pointermove", handleMove);
1075
+ window.removeEventListener("pointerup", handleUp);
1076
+ window.removeEventListener("pointercancel", handleUp);
1077
+ };
1078
+ };
1079
+ var handleStartDrag = function (edge) { return function (event) {
1080
+ var _a, _b, _c, _d;
1081
+ if (!editable)
1082
+ return;
1083
+ // INFO: capture phase, and propagation stopped. ProseMirror handles pointer
1084
+ // events on the editor element to move the selection and to drag a node
1085
+ // around the document — an image IS draggable — and its listener sits on an
1086
+ // ancestor of this handle, so a bubble-phase handler never runs: the drag
1087
+ // silently did nothing in a real browser while passing in jsdom, where no
1088
+ // editor view competes for the event.
1089
+ event.preventDefault();
1090
+ event.stopPropagation();
1091
+ // INFO: on a touch screen the finger leaves the 6px handle almost at once,
1092
+ // and without capture every later move goes to whatever is underneath — the
1093
+ // drag appeared to stick after a few pixels. Capture keeps the whole stream
1094
+ // coming here until release.
1095
+ try {
1096
+ (_b = (_a = event.currentTarget) === null || _a === void 0 ? void 0 : _a.setPointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, event.pointerId);
1097
+ }
1098
+ catch (err) {
1099
+ // INFO: a mouse event carries no pointerId in every browser; the drag
1100
+ // works from the window listeners regardless
1101
+ }
1102
+ startRef.current = {
1103
+ x: event.clientX,
1104
+ width: (_d = (_c = imageRef.current) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect().width) !== null && _d !== void 0 ? _d : 0,
1105
+ };
1106
+ setDragging(edge);
1107
+ }; };
1108
+ var renderHandle = function (edge) { return (react_2.default.createElement("div", { role: "presentation", "data-testid": "image-resize-".concat(edge), onPointerDownCapture: handleStartDrag(edge),
1109
+ // INFO: the editor must not treat a grab of the handle as a text selection
1110
+ // or a node drag either, and it listens for mousedown as well
1111
+ onMouseDownCapture: function (event) {
1112
+ event.preventDefault();
1113
+ event.stopPropagation();
1114
+ }, draggable: false,
1115
+ // INFO: the hit area is far larger than the bar it draws. A 6px target is
1116
+ // reachable with a mouse and not with a thumb, which is most of why
1117
+ // resizing felt unresponsive on a phone rather than anything in the maths.
1118
+ className: "absolute ".concat(edge === "left" ? "-left-1" : "-right-1", " top-1/2 flex h-16 w-8 -translate-y-1/2 cursor-ew-resize items-center justify-center"),
1119
+ // INFO: touch-action none is what makes a touch drag possible at all —
1120
+ // otherwise the browser claims the gesture as a scroll and cancels the
1121
+ // pointer stream after the first few pixels, which reads as the handle
1122
+ // being unresponsive
1123
+ style: { touchAction: "none" } },
1124
+ react_2.default.createElement("div", { className: "h-10 w-1.5 rounded-full bg-white/90 shadow ring-1 ring-gray-900/20" }))); };
1125
+ /**
1126
+ * INFO: a double click returns the image to its natural width by clearing the
1127
+ * attribute rather than measuring one — so it goes back to serialising as
1128
+ * plain `![alt](src)`, exactly as it did before it was ever resized.
1129
+ */
1130
+ var handleResetWidth = function () {
1131
+ if (!editable || !width)
1132
+ return;
1133
+ updateAttributes({ width: null });
1134
+ };
1135
+ var showHandles = editable && (hovered || selected || !!dragging);
1136
+ return (react_2.default.createElement(react_1.NodeViewWrapper, { className: "my-4",
1137
+ // INFO: the selection outline and the handles sit on a box the size of the
1138
+ // image, not the width of the line, so they do not float away from it
1139
+ style: { width: "fit-content", maxWidth: "100%" }, onMouseEnter: function () { return setHovered(true); }, onMouseLeave: function () { return setHovered(false); },
1140
+ // INFO: a touch screen has no hover, so a tap has to be what reveals the
1141
+ // handles. ProseMirror also selects the node on tap, but only once the
1142
+ // editor has focus — relying on that alone left an image on a phone with
1143
+ // no handles at all on first touch.
1144
+ onPointerDown: function (event) {
1145
+ if (event.pointerType !== "mouse")
1146
+ setHovered(true);
1147
+ } },
1148
+ react_2.default.createElement("div", { className: "relative inline-block max-w-full leading-none" },
1149
+ react_2.default.createElement("img", { ref: imageRef, src: src, alt: alt !== null && alt !== void 0 ? alt : "", title: title !== null && title !== void 0 ? title : undefined, draggable: false, onDoubleClick: handleResetWidth, className: "block h-auto max-w-full rounded ".concat(selected ? "ring-2 ring-primary-500" : ""), style: {
1150
+ // INFO: no width attribute means the image draws at its own size,
1151
+ // capped the way it always was
1152
+ width: shownWidth ? "".concat(shownWidth, "px") : undefined,
1153
+ maxWidth: shownWidth ? "100%" : "600px",
1154
+ // INFO: the browser would otherwise keep showing the resize cursor
1155
+ // and the pointer could slip onto the image mid-drag
1156
+ pointerEvents: dragging ? "none" : undefined,
1157
+ } }),
1158
+ showHandles && renderHandle("left"),
1159
+ showHandles && renderHandle("right"))));
1160
+ };
1161
+ /**
1162
+ * INFO: only a width is stored, never a height — the image keeps its own aspect
1163
+ * ratio, which is what makes a one-axis handle enough and makes it impossible to
1164
+ * save a distorted image.
1165
+ */
1166
+ var ResizableImage = extension_image_1.default.extend({
1167
+ addAttributes: function () {
1168
+ var _a;
1169
+ return __assign(__assign({}, (_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)), { width: {
1170
+ default: null,
1171
+ parseHTML: function (element) {
1172
+ var _a, _b;
1173
+ var attr = element.getAttribute("width");
1174
+ if (attr && !Number.isNaN(Number(attr)))
1175
+ return Number(attr);
1176
+ // INFO: a document written by an older editor carries the size in the
1177
+ // inline style instead, so it is read back rather than being lost
1178
+ var style = (_b = (_a = element.style) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : "";
1179
+ var match = style.match(/^(\d+(?:\.\d+)?)px$/);
1180
+ return match ? Math.round(Number(match[1])) : null;
1181
+ },
1182
+ // INFO: the attribute, not a style — this is what turndown reads to
1183
+ // decide between html and `![alt](src)`, and what a browser honours when
1184
+ // the published page sets the html raw
1185
+ renderHTML: function (attributes) {
1186
+ return attributes.width ? { width: attributes.width } : {};
1187
+ },
1188
+ } });
1189
+ },
1190
+ addNodeView: function () {
1191
+ return (0, react_1.ReactNodeViewRenderer)(ResizableImageComponent);
1192
+ },
1193
+ });
947
1194
  // Slash command items
948
1195
  var slashCommands = [
949
1196
  {
@@ -1061,7 +1308,7 @@ var SlashMenuList = function (_a) {
1061
1308
  react_2.default.createElement("span", { className: "text-sm ".concat(darkMode ? "text-gray-200" : "text-gray-700") }, item.label))); })) : (react_2.default.createElement("div", { className: "px-4 py-2 text-sm text-gray-500" }, "No results"))));
1062
1309
  };
1063
1310
  var MarkdownInputContent = function (_a) {
1064
- var innerRef = _a.innerRef, id = _a.id, value = _a.value, label = _a.label, subLabel = _a.subLabel, labelExtra = _a.labelExtra, infoElement = _a.infoElement, placeholder = _a.placeholder, optional = _a.optional, disabled = _a.disabled, containerClassName = _a.containerClassName, heightClassName = _a.heightClassName, borderless = _a.borderless, onChange = _a.onChange, compress = _a.compress, lib = _a.lib, uploadEndpoint = _a.uploadEndpoint, enableImageUpload = _a.enableImageUpload, enableVideoUpload = _a.enableVideoUpload, enableFileUpload = _a.enableFileUpload, darkMode = _a.darkMode, readOnly = _a.readOnly, customElement = _a.customElement, renderEmbed = _a.renderEmbed, onRequestRemove = _a.onRequestRemove, customCommands = _a.customCommands;
1311
+ var innerRef = _a.innerRef, id = _a.id, value = _a.value, label = _a.label, subLabel = _a.subLabel, labelExtra = _a.labelExtra, infoElement = _a.infoElement, placeholder = _a.placeholder, optional = _a.optional, disabled = _a.disabled, containerClassName = _a.containerClassName, heightClassName = _a.heightClassName, borderless = _a.borderless, onChange = _a.onChange, compress = _a.compress, lib = _a.lib, uploadEndpoint = _a.uploadEndpoint, enableImageUpload = _a.enableImageUpload, enableVideoUpload = _a.enableVideoUpload, enableFileUpload = _a.enableFileUpload, darkMode = _a.darkMode, readOnly = _a.readOnly, customElement = _a.customElement, renderEmbed = _a.renderEmbed, onRequestRemove = _a.onRequestRemove, onHeadingsChange = _a.onHeadingsChange, customCommands = _a.customCommands;
1065
1312
  var _b = (0, react_2.useState)(""), error = _b[0], setError = _b[1];
1066
1313
  var _c = (0, react_2.useState)(false), showTableMenu = _c[0], setShowTableMenu = _c[1];
1067
1314
  var _d = (0, react_2.useState)(3), tableCols = _d[0], setTableCols = _d[1];
@@ -1228,6 +1475,7 @@ var MarkdownInputContent = function (_a) {
1228
1475
  extension_list_1.TaskItem.configure({
1229
1476
  nested: true,
1230
1477
  }),
1478
+ HeadingAnchors,
1231
1479
  SpaceEmbed.configure({
1232
1480
  getRenderEmbed: function () { return renderEmbedRef.current; },
1233
1481
  getOnRequestRemove: function () { return onRequestRemoveRef.current; },
@@ -1240,7 +1488,10 @@ var MarkdownInputContent = function (_a) {
1240
1488
  extension_table_1.TableRow,
1241
1489
  extension_table_1.TableHeader,
1242
1490
  extension_table_1.TableCell,
1243
- extension_image_1.default.configure({
1491
+ // INFO: the same image node with drag handles. The style stays for the
1492
+ // html path — getHTML() is turndown's input, and a document opened by
1493
+ // anything that renders that html directly still gets the 600px cap.
1494
+ ResizableImage.configure({
1244
1495
  inline: false,
1245
1496
  allowBase64: true,
1246
1497
  HTMLAttributes: {
@@ -1763,6 +2014,18 @@ var MarkdownInputContent = function (_a) {
1763
2014
  }
1764
2015
  }
1765
2016
  }, [value, editor, pendingImagesCount]);
2017
+ /**
2018
+ * INFO: the outline of the document being edited. The ids the headings carry
2019
+ * in the view are produced by the HeadingAnchors plugin from the same slug
2020
+ * function, so an entry here and the anchor it scrolls to cannot disagree.
2021
+ */
2022
+ var headings = (0, react_2.useMemo)(function () { return (0, headings_1.handleParseMarkdownHeadings)(value); }, [value]);
2023
+ var onHeadingsChangeRef = (0, react_2.useRef)(onHeadingsChange);
2024
+ onHeadingsChangeRef.current = onHeadingsChange;
2025
+ (0, react_2.useEffect)(function () {
2026
+ var _a;
2027
+ (_a = onHeadingsChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onHeadingsChangeRef, headings);
2028
+ }, [headings]);
1766
2029
  (0, react_2.useImperativeHandle)(innerRef, function () {
1767
2030
  var methods = {
1768
2031
  handleSubmit: function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -2152,7 +2415,7 @@ var MarkdownInputContent = function (_a) {
2152
2415
  * instances or a heavy re-parse.
2153
2416
  */
2154
2417
  var MarkdownStatic = function (_a) {
2155
- var id = _a.id, label = _a.label, labelExtra = _a.labelExtra, subLabel = _a.subLabel, value = _a.value, darkMode = _a.darkMode, containerClassName = _a.containerClassName, lib = _a.lib;
2418
+ var id = _a.id, label = _a.label, labelExtra = _a.labelExtra, subLabel = _a.subLabel, value = _a.value, darkMode = _a.darkMode, containerClassName = _a.containerClassName, onHeadingsChange = _a.onHeadingsChange, lib = _a.lib;
2156
2419
  var contentRef = (0, react_2.useRef)(null);
2157
2420
  var labelColor = darkMode ? "text-white" : "text-gray-700";
2158
2421
  var subColor = darkMode ? "text-gray-300" : "text-gray-500";
@@ -2178,9 +2441,17 @@ var MarkdownStatic = function (_a) {
2178
2441
  };
2179
2442
  // INFO: markdown-it is fast (~ms/field); memoize on value so a read view with
2180
2443
  // many fields never re-parses on re-render — the source of the repeated freeze.
2181
- var html = (0, react_2.useMemo)(function () { return (value ? handleRenderAttachments(md.render(value)) : ""); },
2444
+ var headings = (0, react_2.useMemo)(function () { return (0, headings_1.handleParseMarkdownHeadings)(value); }, [value]);
2445
+ var html = (0, react_2.useMemo)(function () {
2446
+ return value
2447
+ ? // INFO: the ids go into the html here, not onto the DOM afterwards —
2448
+ // React re-commits this string on re-render and would wipe anything
2449
+ // added to the mounted nodes
2450
+ (0, headings_1.handleInjectHeadingIds)((0, headings_1.handleCapImages)(handleRenderAttachments(md.render(value))), headings)
2451
+ : "";
2452
+ },
2182
2453
  // eslint-disable-next-line react-hooks/exhaustive-deps
2183
- [value]);
2454
+ [value, headings]);
2184
2455
  // INFO: the html is set raw, so diagrams are drawn over the mounted DOM once
2185
2456
  // it matches this value. A document with no mermaid block never loads mermaid.
2186
2457
  var mermaidLoader = lib === null || lib === void 0 ? void 0 : lib.mermaid;
@@ -2189,6 +2460,17 @@ var MarkdownStatic = function (_a) {
2189
2460
  return;
2190
2461
  (0, mermaid_1.handleRenderMermaidBlocks)(contentRef.current, mermaidLoader);
2191
2462
  }, [html, mermaidLoader]);
2463
+ /**
2464
+ * INFO: the same outline the editor reports, from the same function — a
2465
+ * published page and the note it was published from cannot list different
2466
+ * sections.
2467
+ */
2468
+ var onHeadingsChangeRef = (0, react_2.useRef)(onHeadingsChange);
2469
+ onHeadingsChangeRef.current = onHeadingsChange;
2470
+ (0, react_2.useEffect)(function () {
2471
+ var _a;
2472
+ (_a = onHeadingsChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onHeadingsChangeRef, headings);
2473
+ }, [headings]);
2192
2474
  return (react_2.default.createElement("div", { className: defaultClassName },
2193
2475
  react_2.default.createElement("div", { className: "flex flex-col" },
2194
2476
  react_2.default.createElement("label", { className: "block text-sm font-medium ".concat(labelColor) },