mirror of
https://github.com/docmost/docmost.git
synced 2026-06-11 02:28:24 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ae39b522d | |||
| b4f009513e | |||
| fcffa3dfa0 | |||
| 1980b94825 | |||
| bea1637519 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.70.0",
|
"version": "0.70.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: pulse 1.2s ease-in-out infinite;
|
animation: pulse 1.2s ease-in-out infinite;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: pulse 1.2s ease-in-out infinite;
|
animation: pulse 1.2s ease-in-out infinite;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.70.0",
|
"version": "0.70.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.70.0",
|
"version": "0.70.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
||||||
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
import {
|
||||||
|
normalizeFileUrl,
|
||||||
|
applyAlignment,
|
||||||
|
createPlaceholderView,
|
||||||
|
setupMediaLoading,
|
||||||
|
} from "./media-utils";
|
||||||
|
|
||||||
export type DrawioResizeOptions = {
|
export type DrawioResizeOptions = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -204,26 +210,11 @@ export const Drawio = Node.create<DrawioOptions>({
|
|||||||
const { node, getPos, HTMLAttributes, editor } = props;
|
const { node, getPos, HTMLAttributes, editor } = props;
|
||||||
|
|
||||||
if (!node.attrs.src) {
|
if (!node.attrs.src) {
|
||||||
editor.isInitialized = true;
|
return createPlaceholderView(this.options.view, props);
|
||||||
const reactView = ReactNodeViewRenderer(this.options.view);
|
|
||||||
const view = reactView(props);
|
|
||||||
|
|
||||||
const originalUpdate = view.update?.bind(view);
|
|
||||||
view.update = (updatedNode, decorations, innerDecorations) => {
|
|
||||||
if (updatedNode.attrs.src && !node.attrs.src) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (originalUpdate) {
|
|
||||||
return originalUpdate(updatedNode, decorations, innerDecorations);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.createElement("img");
|
const el = document.createElement("img");
|
||||||
el.src = node.attrs.src;
|
el.src = normalizeFileUrl(node.attrs.src);
|
||||||
el.alt = node.attrs.title || "";
|
el.alt = node.attrs.title || "";
|
||||||
el.style.display = "block";
|
el.style.display = "block";
|
||||||
el.style.maxWidth = "100%";
|
el.style.maxWidth = "100%";
|
||||||
@@ -259,7 +250,7 @@ export const Drawio = Node.create<DrawioOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
||||||
el.src = updatedNode.attrs.src || "";
|
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = updatedNode.attrs.width;
|
const w = updatedNode.attrs.width;
|
||||||
@@ -290,52 +281,10 @@ export const Drawio = Node.create<DrawioOptions>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dom = nodeView.dom as HTMLElement;
|
setupMediaLoading(nodeView.dom as HTMLElement, el, node);
|
||||||
|
|
||||||
applyAlignment(dom, node.attrs.align || "center");
|
|
||||||
|
|
||||||
// Handle percentage width backward compat
|
|
||||||
const widthAttr = node.attrs.width;
|
|
||||||
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const parentEl = dom.parentElement;
|
|
||||||
if (parentEl) {
|
|
||||||
const containerWidth = parentEl.clientWidth;
|
|
||||||
const pctValue = parseInt(widthAttr, 10);
|
|
||||||
if (!isNaN(pctValue) && containerWidth > 0) {
|
|
||||||
const pxWidth = Math.round(
|
|
||||||
containerWidth * (pctValue / 100),
|
|
||||||
);
|
|
||||||
el.style.width = `${pxWidth}px`;
|
|
||||||
if (node.attrs.aspectRatio) {
|
|
||||||
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide until image loads
|
|
||||||
dom.style.visibility = "hidden";
|
|
||||||
dom.style.pointerEvents = "none";
|
|
||||||
el.onload = () => {
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return nodeView;
|
return nodeView;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyAlignment(container: HTMLElement, align: string) {
|
|
||||||
if (align === "left") {
|
|
||||||
container.style.justifyContent = "flex-start";
|
|
||||||
} else if (align === "right") {
|
|
||||||
container.style.justifyContent = "flex-end";
|
|
||||||
} else {
|
|
||||||
container.style.justifyContent = "center";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
||||||
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
import {
|
||||||
|
normalizeFileUrl,
|
||||||
|
applyAlignment,
|
||||||
|
createPlaceholderView,
|
||||||
|
setupMediaLoading,
|
||||||
|
} from "./media-utils";
|
||||||
|
|
||||||
export type ExcalidrawResizeOptions = {
|
export type ExcalidrawResizeOptions = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -204,26 +210,11 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
|||||||
const { node, getPos, HTMLAttributes, editor } = props;
|
const { node, getPos, HTMLAttributes, editor } = props;
|
||||||
|
|
||||||
if (!node.attrs.src) {
|
if (!node.attrs.src) {
|
||||||
editor.isInitialized = true;
|
return createPlaceholderView(this.options.view, props);
|
||||||
const reactView = ReactNodeViewRenderer(this.options.view);
|
|
||||||
const view = reactView(props);
|
|
||||||
|
|
||||||
const originalUpdate = view.update?.bind(view);
|
|
||||||
view.update = (updatedNode, decorations, innerDecorations) => {
|
|
||||||
if (updatedNode.attrs.src && !node.attrs.src) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (originalUpdate) {
|
|
||||||
return originalUpdate(updatedNode, decorations, innerDecorations);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.createElement("img");
|
const el = document.createElement("img");
|
||||||
el.src = node.attrs.src;
|
el.src = normalizeFileUrl(node.attrs.src);
|
||||||
el.alt = node.attrs.title || "";
|
el.alt = node.attrs.title || "";
|
||||||
el.style.display = "block";
|
el.style.display = "block";
|
||||||
el.style.maxWidth = "100%";
|
el.style.maxWidth = "100%";
|
||||||
@@ -259,7 +250,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
||||||
el.src = updatedNode.attrs.src || "";
|
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = updatedNode.attrs.width;
|
const w = updatedNode.attrs.width;
|
||||||
@@ -290,52 +281,10 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dom = nodeView.dom as HTMLElement;
|
setupMediaLoading(nodeView.dom as HTMLElement, el, node);
|
||||||
|
|
||||||
applyAlignment(dom, node.attrs.align || "center");
|
|
||||||
|
|
||||||
// Handle percentage width backward compat
|
|
||||||
const widthAttr = node.attrs.width;
|
|
||||||
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const parentEl = dom.parentElement;
|
|
||||||
if (parentEl) {
|
|
||||||
const containerWidth = parentEl.clientWidth;
|
|
||||||
const pctValue = parseInt(widthAttr, 10);
|
|
||||||
if (!isNaN(pctValue) && containerWidth > 0) {
|
|
||||||
const pxWidth = Math.round(
|
|
||||||
containerWidth * (pctValue / 100),
|
|
||||||
);
|
|
||||||
el.style.width = `${pxWidth}px`;
|
|
||||||
if (node.attrs.aspectRatio) {
|
|
||||||
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide until image loads
|
|
||||||
dom.style.visibility = "hidden";
|
|
||||||
dom.style.pointerEvents = "none";
|
|
||||||
el.onload = () => {
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return nodeView;
|
return nodeView;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyAlignment(container: HTMLElement, align: string) {
|
|
||||||
if (align === "left") {
|
|
||||||
container.style.justifyContent = "flex-start";
|
|
||||||
} else if (align === "right") {
|
|
||||||
container.style.justifyContent = "flex-end";
|
|
||||||
} else {
|
|
||||||
container.style.justifyContent = "center";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import {
|
|||||||
Range,
|
Range,
|
||||||
ResizableNodeView,
|
ResizableNodeView,
|
||||||
} from "@tiptap/core";
|
} from "@tiptap/core";
|
||||||
|
import {
|
||||||
|
normalizeFileUrl,
|
||||||
|
applyAlignment,
|
||||||
|
createPlaceholderView,
|
||||||
|
setupMediaLoading,
|
||||||
|
} from "../media-utils";
|
||||||
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
||||||
|
|
||||||
export type ImageResizeOptions = {
|
export type ImageResizeOptions = {
|
||||||
@@ -215,25 +221,8 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
|||||||
return (props) => {
|
return (props) => {
|
||||||
const { node, getPos, HTMLAttributes, editor } = props;
|
const { node, getPos, HTMLAttributes, editor } = props;
|
||||||
|
|
||||||
// If no src yet (placeholder/uploading), use React view for loading UI
|
|
||||||
if (!HTMLAttributes.src) {
|
if (!HTMLAttributes.src) {
|
||||||
editor.isInitialized = true;
|
return createPlaceholderView(this.options.view, props);
|
||||||
const reactView = ReactNodeViewRenderer(this.options.view);
|
|
||||||
const view = reactView(props);
|
|
||||||
|
|
||||||
// When the node gets a src, return false from update to force rebuild
|
|
||||||
const originalUpdate = view.update?.bind(view);
|
|
||||||
view.update = (updatedNode, decorations, innerDecorations) => {
|
|
||||||
if (updatedNode.attrs.src && !node.attrs.src) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (originalUpdate) {
|
|
||||||
return originalUpdate(updatedNode, decorations, innerDecorations);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has src — use ResizableNodeView
|
// Has src — use ResizableNodeView
|
||||||
@@ -252,11 +241,18 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
el.src = HTMLAttributes.src;
|
el.src = normalizeFileUrl(HTMLAttributes.src);
|
||||||
el.style.display = "block";
|
el.style.display = "block";
|
||||||
el.style.maxWidth = "100%";
|
el.style.maxWidth = "100%";
|
||||||
el.style.borderRadius = "8px";
|
el.style.borderRadius = "8px";
|
||||||
|
|
||||||
|
if (typeof node.attrs.width === "number" && node.attrs.width > 0) {
|
||||||
|
el.style.width = `${node.attrs.width}px`;
|
||||||
|
if (typeof node.attrs.height === "number" && node.attrs.height > 0) {
|
||||||
|
el.style.height = `${node.attrs.height}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let currentNode = node;
|
let currentNode = node;
|
||||||
|
|
||||||
const nodeView = new ResizableNodeView({
|
const nodeView = new ResizableNodeView({
|
||||||
@@ -287,7 +283,7 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
||||||
el.src = updatedNode.attrs.src || "";
|
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedNode.attrs.alt !== currentNode.attrs.alt) {
|
if (updatedNode.attrs.alt !== currentNode.attrs.alt) {
|
||||||
@@ -323,54 +319,10 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dom = nodeView.dom as HTMLElement;
|
setupMediaLoading(nodeView.dom as HTMLElement, el, node);
|
||||||
|
|
||||||
// Apply initial alignment
|
|
||||||
applyAlignment(dom, node.attrs.align || "center");
|
|
||||||
|
|
||||||
// Handle percentage width backward compat
|
|
||||||
const widthAttr = node.attrs.width;
|
|
||||||
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
|
|
||||||
// Defer conversion until we can measure the container
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const parentEl = dom.parentElement;
|
|
||||||
if (parentEl) {
|
|
||||||
const containerWidth = parentEl.clientWidth;
|
|
||||||
const pctValue = parseInt(widthAttr, 10);
|
|
||||||
if (!isNaN(pctValue) && containerWidth > 0) {
|
|
||||||
const pxWidth = Math.round(
|
|
||||||
containerWidth * (pctValue / 100),
|
|
||||||
);
|
|
||||||
el.style.width = `${pxWidth}px`;
|
|
||||||
if (node.attrs.aspectRatio) {
|
|
||||||
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide until image loads (official TipTap pattern)
|
|
||||||
dom.style.visibility = "hidden";
|
|
||||||
dom.style.pointerEvents = "none";
|
|
||||||
el.onload = () => {
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return nodeView;
|
return nodeView;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyAlignment(container: HTMLElement, align: string) {
|
|
||||||
if (align === "left") {
|
|
||||||
container.style.justifyContent = "flex-start";
|
|
||||||
} else if (align === "right") {
|
|
||||||
container.style.justifyContent = "flex-end";
|
|
||||||
} else {
|
|
||||||
container.style.justifyContent = "center";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,84 @@
|
|||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
export function normalizeFileUrl(src: string): string {
|
||||||
|
if (src && src.startsWith("/files/")) {
|
||||||
|
return "/api" + src;
|
||||||
|
}
|
||||||
|
return src || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAlignment(container: HTMLElement, align: string) {
|
||||||
|
if (align === "left") {
|
||||||
|
container.style.justifyContent = "flex-start";
|
||||||
|
} else if (align === "right") {
|
||||||
|
container.style.justifyContent = "flex-end";
|
||||||
|
} else {
|
||||||
|
container.style.justifyContent = "center";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPlaceholderView(viewComponent: any, props: any) {
|
||||||
|
const { node, editor } = props;
|
||||||
|
editor.isInitialized = true;
|
||||||
|
const reactView = ReactNodeViewRenderer(viewComponent);
|
||||||
|
const view = reactView(props);
|
||||||
|
|
||||||
|
const originalUpdate = view.update?.bind(view);
|
||||||
|
view.update = (updatedNode: any, decorations: any, innerDecorations: any) => {
|
||||||
|
if (updatedNode.attrs.src && !node.attrs.src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (originalUpdate) {
|
||||||
|
return originalUpdate(updatedNode, decorations, innerDecorations);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupMediaLoading(
|
||||||
|
dom: HTMLElement,
|
||||||
|
el: HTMLElement,
|
||||||
|
node: any,
|
||||||
|
loadEvent: "load" | "loadedmetadata" = "load",
|
||||||
|
) {
|
||||||
|
applyAlignment(dom, node.attrs.align || "center");
|
||||||
|
|
||||||
|
const widthAttr = node.attrs.width;
|
||||||
|
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const parentEl = dom.parentElement;
|
||||||
|
if (parentEl) {
|
||||||
|
const containerWidth = parentEl.clientWidth;
|
||||||
|
const pctValue = parseInt(widthAttr, 10);
|
||||||
|
if (!isNaN(pctValue) && containerWidth > 0) {
|
||||||
|
const pxWidth = Math.round(containerWidth * (pctValue / 100));
|
||||||
|
el.style.width = `${pxWidth}px`;
|
||||||
|
if (node.attrs.aspectRatio) {
|
||||||
|
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dom.style.visibility = "";
|
||||||
|
dom.style.pointerEvents = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.style.pointerEvents = "none";
|
||||||
|
dom.style.background =
|
||||||
|
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
|
||||||
|
|
||||||
|
el.addEventListener(
|
||||||
|
loadEvent,
|
||||||
|
() => {
|
||||||
|
dom.style.pointerEvents = "";
|
||||||
|
dom.style.background = "";
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type UploadFn = (
|
export type UploadFn = (
|
||||||
file: File,
|
file: File,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
|
||||||
|
import {
|
||||||
|
normalizeFileUrl,
|
||||||
|
applyAlignment,
|
||||||
|
createPlaceholderView,
|
||||||
|
setupMediaLoading,
|
||||||
|
} from "../media-utils";
|
||||||
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
import type { ResizableNodeViewDirection } from "@tiptap/core";
|
||||||
|
|
||||||
export type VideoResizeOptions = {
|
export type VideoResizeOptions = {
|
||||||
@@ -204,32 +210,24 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
|||||||
const { node, getPos, HTMLAttributes, editor } = props;
|
const { node, getPos, HTMLAttributes, editor } = props;
|
||||||
|
|
||||||
if (!node.attrs.src) {
|
if (!node.attrs.src) {
|
||||||
editor.isInitialized = true;
|
return createPlaceholderView(this.options.view, props);
|
||||||
const reactView = ReactNodeViewRenderer(this.options.view);
|
|
||||||
const view = reactView(props);
|
|
||||||
|
|
||||||
const originalUpdate = view.update?.bind(view);
|
|
||||||
view.update = (updatedNode, decorations, innerDecorations) => {
|
|
||||||
if (updatedNode.attrs.src && !node.attrs.src) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (originalUpdate) {
|
|
||||||
return originalUpdate(updatedNode, decorations, innerDecorations);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.createElement("video");
|
const el = document.createElement("video");
|
||||||
el.src = node.attrs.src;
|
el.src = normalizeFileUrl(node.attrs.src);
|
||||||
el.controls = true;
|
el.controls = true;
|
||||||
el.preload = "metadata";
|
el.preload = "metadata";
|
||||||
el.style.display = "block";
|
el.style.display = "block";
|
||||||
el.style.maxWidth = "100%";
|
el.style.maxWidth = "100%";
|
||||||
el.style.borderRadius = "8px";
|
el.style.borderRadius = "8px";
|
||||||
|
|
||||||
|
if (typeof node.attrs.width === "number" && node.attrs.width > 0) {
|
||||||
|
el.style.width = `${node.attrs.width}px`;
|
||||||
|
if (typeof node.attrs.height === "number" && node.attrs.height > 0) {
|
||||||
|
el.style.height = `${node.attrs.height}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let currentNode = node;
|
let currentNode = node;
|
||||||
|
|
||||||
const nodeView = new ResizableNodeView({
|
const nodeView = new ResizableNodeView({
|
||||||
@@ -260,7 +258,7 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
if (updatedNode.attrs.src !== currentNode.attrs.src) {
|
||||||
el.src = updatedNode.attrs.src || "";
|
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = updatedNode.attrs.width;
|
const w = updatedNode.attrs.width;
|
||||||
@@ -291,52 +289,10 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dom = nodeView.dom as HTMLElement;
|
setupMediaLoading(nodeView.dom as HTMLElement, el, node, "loadedmetadata");
|
||||||
|
|
||||||
applyAlignment(dom, node.attrs.align || "center");
|
|
||||||
|
|
||||||
// Handle percentage width backward compat
|
|
||||||
const widthAttr = node.attrs.width;
|
|
||||||
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const parentEl = dom.parentElement;
|
|
||||||
if (parentEl) {
|
|
||||||
const containerWidth = parentEl.clientWidth;
|
|
||||||
const pctValue = parseInt(widthAttr, 10);
|
|
||||||
if (!isNaN(pctValue) && containerWidth > 0) {
|
|
||||||
const pxWidth = Math.round(
|
|
||||||
containerWidth * (pctValue / 100),
|
|
||||||
);
|
|
||||||
el.style.width = `${pxWidth}px`;
|
|
||||||
if (node.attrs.aspectRatio) {
|
|
||||||
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide until video metadata loads
|
|
||||||
dom.style.visibility = "hidden";
|
|
||||||
dom.style.pointerEvents = "none";
|
|
||||||
el.onloadedmetadata = () => {
|
|
||||||
dom.style.visibility = "";
|
|
||||||
dom.style.pointerEvents = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return nodeView;
|
return nodeView;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyAlignment(container: HTMLElement, align: string) {
|
|
||||||
if (align === "left") {
|
|
||||||
container.style.justifyContent = "flex-start";
|
|
||||||
} else if (align === "right") {
|
|
||||||
container.style.justifyContent = "flex-end";
|
|
||||||
} else {
|
|
||||||
container.style.justifyContent = "center";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user