mdwysiwyg 0.2.1 → 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 +1 -0
- package/dist/index.cjs +152 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +152 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,7 +38,7 @@ 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
|
|
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");
|
|
@@ -2820,6 +2820,31 @@ 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 }),
|
|
@@ -2831,7 +2856,7 @@ function createEditorExtensions(options) {
|
|
|
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({
|
|
4052
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -4168,9 +4294,28 @@ var MarkdownEditor = (0, import_react11.forwardRef)(function MarkdownEditor2({
|
|
|
4168
4294
|
},
|
|
4169
4295
|
isModified() {
|
|
4170
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);
|
|
4171
4316
|
}
|
|
4172
4317
|
}),
|
|
4173
|
-
[editor]
|
|
4318
|
+
[editor, effectiveViewMode]
|
|
4174
4319
|
);
|
|
4175
4320
|
const closeLinkPopover = () => setLinkPopover(null);
|
|
4176
4321
|
const closeTableMenu = () => setTableMenu(null);
|