mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 10:05:03 +08:00
feat(editor): audio and PDF nodes (#2064)
* use local resizable * feat: aduio * support audio imports * feat: use confluence real file names * cleanup * error handling * hide notice * add audio * fix pulse * Fix import and export * unify pulse * hide in readonly mode * keywords * keyword * translations * better sort * feat: PDF embed * cleanup * remove audio menu * open active * hide focus on readonly mode * increase iframe default dimension
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { TiptapPdf } from "./pdf";
|
||||
export * from "./pdf-upload";
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MediaUploadOptions, UploadFn } from "../media-utils";
|
||||
import { IAttachment } from "../types";
|
||||
import { generateNodeId } from "../utils";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { Command } from "@tiptap/core";
|
||||
|
||||
const findPdfNodeByPlaceholderId = (
|
||||
doc: Node,
|
||||
placeholderId: string,
|
||||
): { node: Node; pos: number } | null => {
|
||||
let result: { node: Node; pos: number } | null = null;
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (result) return false;
|
||||
|
||||
if (
|
||||
node.type.name === "pdf" &&
|
||||
node.attrs.placeholder?.id === placeholderId
|
||||
) {
|
||||
result = { node, pos };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const handlePdfUpload =
|
||||
({ validateFn, onUpload }: MediaUploadOptions): UploadFn =>
|
||||
async (file, editor, pos, pageId) => {
|
||||
const validated = validateFn?.(file);
|
||||
// @ts-ignore
|
||||
if (!validated) return;
|
||||
|
||||
const placeholderId = generateNodeId();
|
||||
|
||||
let placeholderInserted = false;
|
||||
|
||||
const insertPlaceholder = (): Command => {
|
||||
return ({ tr, state }) => {
|
||||
const initialPlaceholderNode = state.schema.nodes.pdf?.create({
|
||||
placeholder: {
|
||||
id: placeholderId,
|
||||
name: file.name,
|
||||
},
|
||||
});
|
||||
|
||||
if (!initialPlaceholderNode) return false;
|
||||
|
||||
const { parent } = tr.doc.resolve(pos);
|
||||
const isEmptyTextBlock = parent.isTextblock && !parent.childCount;
|
||||
|
||||
if (isEmptyTextBlock) {
|
||||
tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode);
|
||||
} else {
|
||||
tr.insert(pos, initialPlaceholderNode);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const replacePlaceholderWithPdf = (attachment: IAttachment): Command => {
|
||||
return ({ tr }) => {
|
||||
const { pos: currentPos = null } =
|
||||
findPdfNodeByPlaceholderId(tr.doc, placeholderId) || {};
|
||||
|
||||
if (currentPos === null || !attachment) return;
|
||||
|
||||
tr.setNodeMarkup(currentPos, undefined, {
|
||||
src: `/api/files/${attachment.id}/${attachment.fileName}`,
|
||||
name: attachment.fileName,
|
||||
attachmentId: attachment.id,
|
||||
size: attachment.fileSize,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const removePlaceholder = (): Command => {
|
||||
return ({ tr }) => {
|
||||
const { pos: currentPos = null } =
|
||||
findPdfNodeByPlaceholderId(tr.doc, placeholderId) || {};
|
||||
|
||||
if (currentPos === null) return false;
|
||||
|
||||
tr.delete(currentPos, currentPos + 2);
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const insertPlaceholderTimeout = setTimeout(() => {
|
||||
editor.commands.command(insertPlaceholder());
|
||||
placeholderInserted = true;
|
||||
}, 250);
|
||||
|
||||
try {
|
||||
const attachment: IAttachment = await onUpload(file, pageId);
|
||||
|
||||
clearTimeout(insertPlaceholderTimeout);
|
||||
|
||||
if (placeholderInserted) {
|
||||
setTimeout(() => {
|
||||
editor.commands.command(replacePlaceholderWithPdf(attachment));
|
||||
}, 100);
|
||||
} else {
|
||||
editor
|
||||
.chain()
|
||||
.command(insertPlaceholder())
|
||||
.command(replacePlaceholderWithPdf(attachment))
|
||||
.run();
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(insertPlaceholderTimeout);
|
||||
editor.commands.command(removePlaceholder());
|
||||
}
|
||||
};
|
||||
|
||||
export { handlePdfUpload };
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { sanitizeUrl, isInternalFileUrl } from "../utils";
|
||||
|
||||
export type PdfOptions = {
|
||||
view: any;
|
||||
HTMLAttributes: Record<string, any>;
|
||||
};
|
||||
|
||||
export type PdfAttributes = {
|
||||
src?: string;
|
||||
name?: string;
|
||||
attachmentId?: string;
|
||||
size?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
placeholder?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
pdfBlock: {
|
||||
setPdf: (attributes: PdfAttributes) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const TiptapPdf = Node.create<PdfOptions>({
|
||||
name: "pdf",
|
||||
|
||||
group: "block",
|
||||
isolating: true,
|
||||
atom: true,
|
||||
defining: true,
|
||||
draggable: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
view: null,
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: "",
|
||||
parseHTML: (element) => {
|
||||
const src = element.getAttribute("src");
|
||||
const sanitized = sanitizeUrl(src);
|
||||
return isInternalFileUrl(sanitized) ? sanitized : "";
|
||||
},
|
||||
renderHTML: (attributes) => ({
|
||||
src: isInternalFileUrl(attributes.src) ? sanitizeUrl(attributes.src) : "",
|
||||
}),
|
||||
},
|
||||
name: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("data-name"),
|
||||
renderHTML: (attributes: PdfAttributes) => ({
|
||||
"data-name": attributes.name,
|
||||
}),
|
||||
},
|
||||
attachmentId: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("data-attachment-id"),
|
||||
renderHTML: (attributes: PdfAttributes) => ({
|
||||
"data-attachment-id": attributes.attachmentId,
|
||||
}),
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute("data-size"),
|
||||
renderHTML: (attributes: PdfAttributes) => ({
|
||||
"data-size": attributes.size,
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
default: 800,
|
||||
parseHTML: (element) => {
|
||||
const raw = element.getAttribute("width");
|
||||
if (!raw) return null;
|
||||
const num = parseFloat(raw);
|
||||
return isNaN(num) ? null : num;
|
||||
},
|
||||
renderHTML: (attributes: PdfAttributes) => ({
|
||||
width: attributes.width,
|
||||
}),
|
||||
},
|
||||
height: {
|
||||
default: 600,
|
||||
parseHTML: (element) => {
|
||||
const raw = element.getAttribute("height");
|
||||
if (!raw) return null;
|
||||
const num = parseFloat(raw);
|
||||
return isNaN(num) ? null : num;
|
||||
},
|
||||
renderHTML: (attributes: PdfAttributes) => ({
|
||||
height: attributes.height,
|
||||
}),
|
||||
},
|
||||
placeholder: {
|
||||
default: null,
|
||||
rendered: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(
|
||||
{ "data-type": this.name },
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes,
|
||||
),
|
||||
[
|
||||
"iframe",
|
||||
{
|
||||
src: isInternalFileUrl(HTMLAttributes.src) ? sanitizeUrl(HTMLAttributes.src) : "",
|
||||
width: HTMLAttributes.width || 800,
|
||||
height: HTMLAttributes.height || 600,
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setPdf:
|
||||
(attrs: PdfAttributes) =>
|
||||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: "pdf",
|
||||
attrs,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
this.editor.isInitialized = true;
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user