mdwysiwyg 0.2.0 → 0.3.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/README.md CHANGED
@@ -117,6 +117,7 @@ Try it in your browser, no install required: **[ferkiernan.github.io/mdwysiwyg](
117
117
  | Insert | Images (by URL), links (URL-only dialog when text is already selected, auto `http://` prefix), tables (visual size picker), horizontal rules, embedded HTML |
118
118
  | Links | Click an existing link to open a quick popover: go to URL, copy URL, or edit/remove it inline |
119
119
  | Tables | Click an already-focused cell again for a context menu: add/move/delete columns (header cells) or rows (data cells) |
120
+ | Cursor between blocks | Click the free area to the right of a table (or the gap between two blocks) to put the cursor there; press Enter to add a blank line, or just type. When the document ends in a block (table, code, Mermaid, quote, list, rule, image) an empty line always follows it |
120
121
  | Code | Fenced code blocks with a language picker (JSON/SQL/TypeScript/JavaScript/Java/custom) and syntax highlighting |
121
122
  | Diagrams | ` ```mermaid ` blocks render as live diagrams; pasted Mermaid syntax is auto-detected on HTML insert. Lazy-loaded, `securityLevel: strict` |
122
123
  | Export | Copy as Markdown or as HTML, one click |
package/dist/index.cjs CHANGED
@@ -38,12 +38,12 @@ module.exports = __toCommonJS(index_exports);
38
38
  var import_react11 = require("react");
39
39
  var import_react12 = require("@tiptap/react");
40
40
  var import_core2 = require("@tiptap/core");
41
- var import_state = require("@tiptap/pm/state");
41
+ var import_state2 = require("@tiptap/pm/state");
42
42
 
43
43
  // src/components/MarkdownEditor/markdown/tiptapMarkdownBridge.ts
44
44
  var import_core = require("@tiptap/core");
45
45
  var import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
46
- var import_extension_table = __toESM(require("@tiptap/extension-table"), 1);
46
+ var import_extension_table = require("@tiptap/extension-table");
47
47
  var import_extension_table_row = __toESM(require("@tiptap/extension-table-row"), 1);
48
48
  var import_extension_table_cell = __toESM(require("@tiptap/extension-table-cell"), 1);
49
49
  var import_extension_table_header = __toESM(require("@tiptap/extension-table-header"), 1);
@@ -2820,18 +2820,43 @@ var CodeBlockWithMermaid = import_extension_code_block_lowlight.default.extend({
2820
2820
  };
2821
2821
  }
2822
2822
  });
2823
+ function createResolvedImage(getResolver) {
2824
+ return import_extension_image.default.extend({
2825
+ addNodeView() {
2826
+ return ({ node }) => {
2827
+ const img = document.createElement("img");
2828
+ const paint = (attrs) => {
2829
+ const src = String(attrs["src"] ?? "");
2830
+ img.src = getResolver()?.(src) ?? src;
2831
+ img.alt = String(attrs["alt"] ?? "");
2832
+ if (attrs["title"]) img.title = String(attrs["title"]);
2833
+ else img.removeAttribute("title");
2834
+ };
2835
+ paint(node.attrs);
2836
+ return {
2837
+ dom: img,
2838
+ update(updated) {
2839
+ if (updated.type.name !== node.type.name) return false;
2840
+ paint(updated.attrs);
2841
+ return true;
2842
+ }
2843
+ };
2844
+ };
2845
+ }
2846
+ });
2847
+ }
2823
2848
  function createEditorExtensions(options) {
2824
2849
  return [
2825
2850
  import_starter_kit.default.configure({ codeBlock: false }),
2826
2851
  CodeBlockWithMermaid.configure({ lowlight }),
2827
- import_extension_table.default.configure({ resizable: false }),
2852
+ import_extension_table.Table.configure({ resizable: false }),
2828
2853
  import_extension_table_row.default,
2829
2854
  import_extension_table_header.default,
2830
2855
  import_extension_table_cell.default,
2831
2856
  import_extension_task_list.default,
2832
2857
  import_extension_task_item.default.configure({ nested: true }),
2833
2858
  import_extension_link.default.configure({ openOnClick: false }),
2834
- import_extension_image.default,
2859
+ options.resolveImageSrc ? createResolvedImage(options.resolveImageSrc) : import_extension_image.default,
2835
2860
  HtmlBlock.configure({ sanitize: options.sanitizeEmbeddedHtml })
2836
2861
  ];
2837
2862
  }
@@ -2916,6 +2941,95 @@ function moveCurrentRow(editor, direction) {
2916
2941
  (0, import_tables.moveTableRow)({ from, to })(state, editor.view.dispatch, editor.view);
2917
2942
  }
2918
2943
 
2944
+ // src/components/MarkdownEditor/markdown/trailingLine.ts
2945
+ var import_gapcursor = require("@tiptap/pm/gapcursor");
2946
+ var import_state = require("@tiptap/pm/state");
2947
+ function ensureTrailingParagraph(editor) {
2948
+ if (!editor.isEditable) return;
2949
+ const { state, view } = editor;
2950
+ const paragraph = state.schema.nodes["paragraph"];
2951
+ if (!paragraph) return;
2952
+ if (state.doc.lastChild?.type === paragraph) return;
2953
+ view.dispatch(
2954
+ state.tr.insert(state.doc.content.size, paragraph.create()).setMeta("preventUpdate", true).setMeta("addToHistory", false)
2955
+ );
2956
+ }
2957
+ function topLevelBlocks(view) {
2958
+ const blocks = [];
2959
+ view.state.doc.forEach((node, pos) => {
2960
+ const dom = view.nodeDOM(pos);
2961
+ blocks.push({ node, pos, el: dom instanceof HTMLElement ? dom : null });
2962
+ });
2963
+ return blocks;
2964
+ }
2965
+ function isEmptyParagraph(node) {
2966
+ return node !== void 0 && node.type.name === "paragraph" && node.content.size === 0;
2967
+ }
2968
+ function isOnScrollbar(element, event) {
2969
+ const rect = element.getBoundingClientRect();
2970
+ return element.clientWidth > 0 && event.clientX - rect.left >= element.clientWidth || element.clientHeight > 0 && event.clientY - rect.top >= element.clientHeight;
2971
+ }
2972
+ function findBoundary(view, target, event) {
2973
+ const blocks = topLevelBlocks(view);
2974
+ const last = blocks[blocks.length - 1];
2975
+ if (last === void 0 || last.el === null) return null;
2976
+ const tableIndex = blocks.findIndex(
2977
+ (block) => block.el === target && block.node.type.name === "table"
2978
+ );
2979
+ if (tableIndex !== -1) {
2980
+ if (isOnScrollbar(target, event)) return null;
2981
+ const tableTop = target.querySelector("table")?.getBoundingClientRect().top;
2982
+ return tableTop !== void 0 && event.clientY < tableTop ? tableIndex : tableIndex + 1;
2983
+ }
2984
+ if (target !== view.dom) return null;
2985
+ const beforeLast = blocks[blocks.length - 2];
2986
+ const lastIsEmpty = isEmptyParagraph(last.node);
2987
+ const thresholdEl = lastIsEmpty && beforeLast?.node.type.name !== "paragraph" ? beforeLast?.el ?? last.el : last.el;
2988
+ const threshold = thresholdEl.getBoundingClientRect();
2989
+ if (event.clientY >= (lastIsEmpty ? threshold.top : threshold.bottom)) {
2990
+ return last.node.type.name === "paragraph" ? blocks.length : null;
2991
+ }
2992
+ const inside = blocks.findIndex((block) => {
2993
+ const rect = block.el?.getBoundingClientRect();
2994
+ return rect !== void 0 && event.clientY >= rect.top && event.clientY <= rect.bottom;
2995
+ });
2996
+ if (inside !== -1) {
2997
+ return blocks[inside]?.node.type.name === "table" ? inside + 1 : null;
2998
+ }
2999
+ const next = blocks.findIndex(
3000
+ (block) => block.el !== null && block.el.getBoundingClientRect().top > event.clientY
3001
+ );
3002
+ if (next < 1) return null;
3003
+ const prevNode = blocks[next - 1]?.node;
3004
+ const nextNode = blocks[next]?.node;
3005
+ return prevNode?.isTextblock && nextNode?.isTextblock ? null : next;
3006
+ }
3007
+ function selectionAtBoundary(doc, index) {
3008
+ let pos = 0;
3009
+ for (let i = 0; i < index; i++) pos += doc.child(i).nodeSize;
3010
+ const prev = index > 0 ? doc.child(index - 1) : void 0;
3011
+ const next = index < doc.childCount ? doc.child(index) : void 0;
3012
+ if (isEmptyParagraph(next)) return import_state.TextSelection.create(doc, pos + 1);
3013
+ if (isEmptyParagraph(prev)) return import_state.TextSelection.create(doc, pos - 1);
3014
+ if (next === void 0 && prev?.isTextblock) {
3015
+ return import_state.TextSelection.create(doc, pos - 1);
3016
+ }
3017
+ return new import_gapcursor.GapCursor(doc.resolve(pos));
3018
+ }
3019
+ function handleBlankAreaMouseDown(view, event) {
3020
+ if (!view.editable || event.button !== 0 || event.shiftKey) return false;
3021
+ const target = event.target;
3022
+ if (!(target instanceof Element)) return false;
3023
+ const boundary = findBoundary(view, target, event);
3024
+ if (boundary === null) return false;
3025
+ event.preventDefault();
3026
+ view.dispatch(
3027
+ view.state.tr.setSelection(selectionAtBoundary(view.state.doc, boundary)).scrollIntoView()
3028
+ );
3029
+ view.focus();
3030
+ return true;
3031
+ }
3032
+
2919
3033
  // src/components/MarkdownEditor/views/RenderedView.tsx
2920
3034
  var import_react = require("@tiptap/react");
2921
3035
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -4031,7 +4145,8 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4031
4145
  className,
4032
4146
  onlyView = false,
4033
4147
  resizable = true,
4034
- onlyViewNotice
4148
+ onlyViewNotice,
4149
+ resolveImageSrc
4035
4150
  }, ref) {
4036
4151
  const [source, setSource] = (0, import_react11.useState)(initialContent);
4037
4152
  const [viewMode, setViewMode] = (0, import_react11.useState)("wysiwyg");
@@ -4047,9 +4162,15 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4047
4162
  onChangeRef.current = onChange;
4048
4163
  const sourceRef = (0, import_react11.useRef)(source);
4049
4164
  sourceRef.current = source;
4165
+ const resolveImageSrcRef = (0, import_react11.useRef)(resolveImageSrc);
4166
+ resolveImageSrcRef.current = resolveImageSrc;
4167
+ const hasImageResolver = resolveImageSrc !== void 0;
4050
4168
  const extensions = (0, import_react11.useMemo)(
4051
- () => createEditorExtensions({ sanitizeEmbeddedHtml }),
4052
- [sanitizeEmbeddedHtml]
4169
+ () => createEditorExtensions({
4170
+ sanitizeEmbeddedHtml,
4171
+ resolveImageSrc: hasImageResolver ? () => resolveImageSrcRef.current : void 0
4172
+ }),
4173
+ [sanitizeEmbeddedHtml, hasImageResolver]
4053
4174
  );
4054
4175
  const editor = (0, import_react12.useEditor)(
4055
4176
  {
@@ -4063,6 +4184,8 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4063
4184
  "aria-label": "Editor de texto enriquecido"
4064
4185
  },
4065
4186
  handleDOMEvents: {
4187
+ // Clic en el área libre tras el último bloque → línea posterior.
4188
+ mousedown: handleBlankAreaMouseDown,
4066
4189
  click(view, event) {
4067
4190
  const target = event.target;
4068
4191
  if (!(target instanceof Element)) return false;
@@ -4076,7 +4199,7 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4076
4199
  if (lastFocusedCellPosRef.current === cellPos) {
4077
4200
  event.preventDefault();
4078
4201
  const tr = view.state.tr.setSelection(
4079
- import_state.TextSelection.near(view.state.doc.resolve(cellPos + 1))
4202
+ import_state2.TextSelection.near(view.state.doc.resolve(cellPos + 1))
4080
4203
  );
4081
4204
  view.dispatch(tr);
4082
4205
  setTableMenu({
@@ -4110,6 +4233,9 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4110
4233
  }
4111
4234
  }
4112
4235
  },
4236
+ onCreate({ editor: createdEditor }) {
4237
+ ensureTrailingParagraph(createdEditor);
4238
+ },
4113
4239
  onUpdate({ editor: currentEditor }) {
4114
4240
  const markdown = editorToMarkdown(currentEditor);
4115
4241
  sourceRef.current = markdown;
@@ -4135,10 +4261,9 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4135
4261
  });
4136
4262
  } else {
4137
4263
  const offset = textareaRef.current?.selectionStart ?? 0;
4138
- editor?.commands.setContent(
4139
- markdownToEditorContent(sourceRef.current),
4140
- false
4141
- );
4264
+ editor?.commands.setContent(markdownToEditorContent(sourceRef.current), {
4265
+ emitUpdate: false
4266
+ });
4142
4267
  setViewMode("wysiwyg");
4143
4268
  if (editor !== null) {
4144
4269
  const pos = markdownOffsetToProseMirrorPos(
@@ -4163,13 +4288,34 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
4163
4288
  const original = originalContentRef.current;
4164
4289
  sourceRef.current = original;
4165
4290
  setSource(original);
4166
- editor?.commands.setContent(markdownToEditorContent(original), false);
4291
+ editor?.commands.setContent(markdownToEditorContent(original), {
4292
+ emitUpdate: false
4293
+ });
4167
4294
  },
4168
4295
  isModified() {
4169
4296
  return sourceRef.current !== originalContentRef.current;
4297
+ },
4298
+ insertMarkdown(markdown) {
4299
+ const textarea = textareaRef.current;
4300
+ if (effectiveViewMode === "markdown" && textarea) {
4301
+ const start = textarea.selectionStart;
4302
+ const end = textarea.selectionEnd;
4303
+ const current = sourceRef.current;
4304
+ handleSourceChange(current.slice(0, start) + markdown + current.slice(end));
4305
+ return;
4306
+ }
4307
+ editor?.chain().focus().insertContent(markdownToEditorContent(markdown)).run();
4308
+ },
4309
+ setMarkdown(markdown) {
4310
+ sourceRef.current = markdown;
4311
+ setSource(markdown);
4312
+ editor?.commands.setContent(markdownToEditorContent(markdown), {
4313
+ emitUpdate: false
4314
+ });
4315
+ onChangeRef.current?.(markdown);
4170
4316
  }
4171
4317
  }),
4172
- [editor]
4318
+ [editor, effectiveViewMode]
4173
4319
  );
4174
4320
  const closeLinkPopover = () => setLinkPopover(null);
4175
4321
  const closeTableMenu = () => setTableMenu(null);