|
@@ -94,16 +94,7 @@ function PostEditor() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// Improved cursor tracker
|
|
// Improved cursor tracker
|
|
|
- const handleEditorChange = (value, event, state) => {
|
|
|
|
|
- handleInputChange("content", value || "");
|
|
|
|
|
|
|
|
|
|
- // Calculate line number from selection start
|
|
|
|
|
- // MDEditor expose state with selection
|
|
|
|
|
- if (state && state.selection && state.selection.start) {
|
|
|
|
|
- const lines = value.substr(0, state.selection.start).split("\n");
|
|
|
|
|
- setCursorLine(lines.length);
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
|
|
|
const insertTextAtCursor = (text) => {
|
|
const insertTextAtCursor = (text) => {
|
|
|
setFormData(prev => ({
|
|
setFormData(prev => ({
|
|
@@ -112,6 +103,22 @@ function PostEditor() {
|
|
|
}));
|
|
}));
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // Improved cursor tracker
|
|
|
|
|
+ const handleCursorSelect = (e) => {
|
|
|
|
|
+ // e.target is the textarea
|
|
|
|
|
+ if (!e || !e.target) return;
|
|
|
|
|
+ const { selectionStart, value } = e.target;
|
|
|
|
|
+ const lines = value.substr(0, selectionStart).split("\n");
|
|
|
|
|
+ // Markdown-it lines are 0-based. lines.length is 1-based count.
|
|
|
|
|
+ // So line index is lines.length - 1.
|
|
|
|
|
+ setCursorLine(lines.length - 1);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleEditorChange = (value, event, state) => {
|
|
|
|
|
+ handleInputChange("content", value || "");
|
|
|
|
|
+ // We rely on handleCursorSelect via onSelect/onClick/onKeyUp for cursor tracking now.
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handlePaste = async (event) => {
|
|
const handlePaste = async (event) => {
|
|
|
const items = event.clipboardData.items;
|
|
const items = event.clipboardData.items;
|
|
|
for (const item of items) {
|
|
for (const item of items) {
|
|
@@ -283,16 +290,22 @@ function PostEditor() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const [viewMode, setViewMode] = useState("tabs"); // "tabs" | "split"
|
|
|
|
|
+
|
|
|
// Process markdown for preview
|
|
// Process markdown for preview
|
|
|
const renderContent = () => {
|
|
const renderContent = () => {
|
|
|
return md.render(formData.content || "");
|
|
return md.render(formData.content || "");
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const previewHtml = activeTab === "preview" ? renderContent() : "";
|
|
|
|
|
- const sanitizedPreview = activeTab === "preview" ? DOMPurify.sanitize(previewHtml, {
|
|
|
|
|
|
|
+ // Calculate preview for both modes
|
|
|
|
|
+ // Helper to sanitize
|
|
|
|
|
+ const getSanitizedHtml = (html) => DOMPurify.sanitize(html, {
|
|
|
ADD_TAGS: ["input"],
|
|
ADD_TAGS: ["input"],
|
|
|
- ADD_ATTR: ["type", "min", "max", "value", "step", "style", "width", "height", "class"],
|
|
|
|
|
- }) : "";
|
|
|
|
|
|
|
+ ADD_ATTR: ["type", "min", "max", "value", "step", "style", "width", "height", "class", "data-line"], // Added data-line
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const previewHtml = renderContent();
|
|
|
|
|
+ const sanitizedPreview = getSanitizedHtml(previewHtml);
|
|
|
|
|
|
|
|
|
|
|
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
@@ -306,11 +319,10 @@ function PostEditor() {
|
|
|
}
|
|
}
|
|
|
}, [formData.title]);
|
|
}, [formData.title]);
|
|
|
|
|
|
|
|
- // Scroll Sync Effect
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Scroll Sync & Flicker Effect
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- if (activeTab === "preview") {
|
|
|
|
|
- // Find the element with the closest data-line attribute
|
|
|
|
|
- // We search for elements with data-line <= cursorLine, picking the largest one
|
|
|
|
|
|
|
+ if (activeTab === "preview" || viewMode === "split") {
|
|
|
const elements = document.querySelectorAll('[data-line]');
|
|
const elements = document.querySelectorAll('[data-line]');
|
|
|
let target = null;
|
|
let target = null;
|
|
|
let maxLine = -1;
|
|
let maxLine = -1;
|
|
@@ -325,11 +337,15 @@ function PostEditor() {
|
|
|
|
|
|
|
|
if (target) {
|
|
if (target) {
|
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
+
|
|
|
|
|
+ // Add flicker effect
|
|
|
|
|
+ target.classList.add('highlight-pulse');
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ target.classList.remove('highlight-pulse');
|
|
|
|
|
+ }, 1500);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }, [activeTab]);
|
|
|
|
|
-
|
|
|
|
|
- // ... existing interactions ...
|
|
|
|
|
|
|
+ }, [activeTab, cursorLine, viewMode]);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="min-h-screen theme-bg font-sans theme-text selection:bg-blue-100 selection:text-blue-900 flex flex-col transition-colors duration-300">
|
|
<div className="min-h-screen theme-bg font-sans theme-text selection:bg-blue-100 selection:text-blue-900 flex flex-col transition-colors duration-300">
|
|
@@ -356,30 +372,48 @@ function PostEditor() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-3">
|
|
|
- {/* View Toggle */}
|
|
|
|
|
- <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
|
|
|
|
|
|
+ {/* View Mode Toggle */}
|
|
|
|
|
+ <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg mr-2">
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- onClick={() => setActiveTab("write")}
|
|
|
|
|
- className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "write"
|
|
|
|
|
|
|
+ onClick={() => setViewMode(viewMode === "tabs" ? "split" : "tabs")}
|
|
|
|
|
+ className={`px-2 py-1.5 rounded-md text-xs font-semibold transition-all flex items-center gap-1 ${viewMode === "split"
|
|
|
? "theme-surface theme-text shadow-sm"
|
|
? "theme-surface theme-text shadow-sm"
|
|
|
: "theme-text-secondary hover:theme-text"
|
|
: "theme-text-secondary hover:theme-text"
|
|
|
}`}
|
|
}`}
|
|
|
|
|
+ title="Toggle Split View"
|
|
|
>
|
|
>
|
|
|
- Write
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => setActiveTab("preview")}
|
|
|
|
|
- className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "preview"
|
|
|
|
|
- ? "theme-surface theme-text shadow-sm"
|
|
|
|
|
- : "theme-text-secondary hover:theme-text"
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- Preview
|
|
|
|
|
|
|
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m-4 10a2 2 0 002 2h2a2 2 0 002-2m0 0V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /></svg>
|
|
|
|
|
+ <span className="hidden sm:inline">Split</span>
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* Tab Toggles (Only visible in Tabs mode) */}
|
|
|
|
|
+ {viewMode === "tabs" && (
|
|
|
|
|
+ <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveTab("write")}
|
|
|
|
|
+ className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "write"
|
|
|
|
|
+ ? "theme-surface theme-text shadow-sm"
|
|
|
|
|
+ : "theme-text-secondary hover:theme-text"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ Write
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveTab("preview")}
|
|
|
|
|
+ className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "preview"
|
|
|
|
|
+ ? "theme-surface theme-text shadow-sm"
|
|
|
|
|
+ : "theme-text-secondary hover:theme-text"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ Preview
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
|
<div className="h-6 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
|
|
|
|
|
|
|
<button
|
|
<button
|
|
@@ -401,9 +435,9 @@ function PostEditor() {
|
|
|
</div>
|
|
</div>
|
|
|
</nav>
|
|
</nav>
|
|
|
|
|
|
|
|
- {/* Main Content Area - Centered & Focused -> Increased max-width */}
|
|
|
|
|
|
|
+ {/* Main Content Area */}
|
|
|
<div className="flex-grow overflow-y-auto">
|
|
<div className="flex-grow overflow-y-auto">
|
|
|
- <div className="max-w-7xl mx-auto px-6 py-12 md:py-20 lg:py-24">
|
|
|
|
|
|
|
+ <div className={`max-w-7xl mx-auto px-6 py-12 md:py-20 lg:py-24 ${viewMode === "split" ? "max-w-[95vw]" : ""}`}>
|
|
|
{error && (
|
|
{error && (
|
|
|
<div className="mb-8 p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
|
|
<div className="mb-8 p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
|
|
|
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
@@ -436,104 +470,119 @@ function PostEditor() {
|
|
|
style={{ minHeight: '3em' }}
|
|
style={{ minHeight: '3em' }}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {activeTab === "write" ? (
|
|
|
|
|
- <div className="prose-editor-wrapper -mx-4 md:mx-0">
|
|
|
|
|
- <MDEditor
|
|
|
|
|
- value={formData.content}
|
|
|
|
|
- onChange={handleEditorChange}
|
|
|
|
|
- commands={[...commands.getCommands(), ...customCommands]}
|
|
|
|
|
- height={window.innerHeight * 0.7} // Increased height to 70% of viewport
|
|
|
|
|
- minHeight={500}
|
|
|
|
|
- preview="edit"
|
|
|
|
|
- visibleDragBar={false}
|
|
|
|
|
- textareaProps={{
|
|
|
|
|
- placeholder: "Tell your story...",
|
|
|
|
|
- onPaste: handlePaste
|
|
|
|
|
- }}
|
|
|
|
|
- className="shadow-sm" // Add slight shadow for separation
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <article className="markdown-content prose dark:prose-invert max-w-none prose-lg">
|
|
|
|
|
- <div
|
|
|
|
|
- dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
|
|
|
|
|
- ref={(node) => {
|
|
|
|
|
- if (node) {
|
|
|
|
|
- // Initialize comparison sliders if present in preview
|
|
|
|
|
- const sliders = node.querySelectorAll(".comparison-slider");
|
|
|
|
|
- sliders.forEach(slider => {
|
|
|
|
|
- slider.oninput = (e) => {
|
|
|
|
|
- const container = e.target.closest(".comparison-wrapper");
|
|
|
|
|
- const topImage = container.querySelector(".comparison-top");
|
|
|
|
|
- const handle = container.querySelector(".slider-handle");
|
|
|
|
|
- const val = e.target.value;
|
|
|
|
|
-
|
|
|
|
|
- if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
|
|
|
|
|
- if (handle) handle.style.left = `${val}%`;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Initialize Zoom Reels in Preview (Re-using logic from PostView slightly simplified)
|
|
|
|
|
- const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
|
|
|
|
|
- zoomReels.forEach(reel => {
|
|
|
|
|
- const images = reel.querySelectorAll(".zoom-reel-img");
|
|
|
|
|
- const viewports = reel.querySelectorAll(".zoom-reel-viewport");
|
|
|
|
|
- const slider = reel.querySelector(".zoom-slider");
|
|
|
|
|
- const resetBtn = reel.querySelector(".reset-zoom");
|
|
|
|
|
-
|
|
|
|
|
- let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
|
|
|
|
|
- const updateTransform = () => {
|
|
|
|
|
- images.forEach(img => {
|
|
|
|
|
- img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
|
|
|
|
|
- });
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- slider.oninput = (e) => {
|
|
|
|
|
- state.zoom = parseFloat(e.target.value);
|
|
|
|
|
- if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- resetBtn.onclick = () => {
|
|
|
|
|
- state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // Simplified drag for preview
|
|
|
|
|
- reel.onmousedown = (e) => {
|
|
|
|
|
- if (state.zoom <= 1) return;
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- state.isDragging = true;
|
|
|
|
|
- state.startX = e.clientX;
|
|
|
|
|
- state.startY = e.clientY;
|
|
|
|
|
- state.initialPanX = state.panX;
|
|
|
|
|
- state.initialPanY = state.panY;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grabbing");
|
|
|
|
|
- };
|
|
|
|
|
- reel.onmousemove = (e) => {
|
|
|
|
|
- if (!state.isDragging) return;
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- const dx = e.clientX - state.startX;
|
|
|
|
|
- const dy = e.clientY - state.startY;
|
|
|
|
|
- state.panX = state.initialPanX + dx;
|
|
|
|
|
- state.panY = state.initialPanY + dy;
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- };
|
|
|
|
|
- reel.onmouseup = () => {
|
|
|
|
|
- state.isDragging = false;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
- };
|
|
|
|
|
- reel.onmouseleave = () => {
|
|
|
|
|
- state.isDragging = false;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ {/* Split Editor/Preview Layout */}
|
|
|
|
|
+ <div className={`grid gap-6 ${viewMode === "split" ? "grid-cols-1 lg:grid-cols-2" : "grid-cols-1"}`}>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Editor Column - Visible if write tab active OR split mode */}
|
|
|
|
|
+ {activeTab === "write" || viewMode === "split" ? (
|
|
|
|
|
+ <div className="prose-editor-wrapper -mx-4 md:mx-0">
|
|
|
|
|
+ <MDEditor
|
|
|
|
|
+ value={formData.content}
|
|
|
|
|
+ onChange={handleEditorChange}
|
|
|
|
|
+ commands={[...commands.getCommands(), ...customCommands]}
|
|
|
|
|
+ height={viewMode === "split" ? window.innerHeight * 0.8 : window.innerHeight * 0.7}
|
|
|
|
|
+ minHeight={500}
|
|
|
|
|
+ preview="edit"
|
|
|
|
|
+ visibleDragBar={false}
|
|
|
|
|
+ textareaProps={{
|
|
|
|
|
+ placeholder: "Tell your story...",
|
|
|
|
|
+ onPaste: handlePaste,
|
|
|
|
|
+ onSelect: handleCursorSelect,
|
|
|
|
|
+ onClick: handleCursorSelect,
|
|
|
|
|
+ onKeyUp: handleCursorSelect
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="shadow-sm sticky top-24"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Preview Column - Visible if preview tab active OR split mode */}
|
|
|
|
|
+ {(activeTab === "preview" || viewMode === "split") && (
|
|
|
|
|
+ <article
|
|
|
|
|
+ className={`markdown-content prose dark:prose-invert max-w-none prose-lg ${viewMode === "split" ? "border-l pl-6 border-gray-200 dark:border-gray-800" : ""}`}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ height: viewMode === "split" ? window.innerHeight * 0.8 : 'auto',
|
|
|
|
|
+ overflowY: viewMode === "split" ? 'auto' : 'visible'
|
|
|
}}
|
|
}}
|
|
|
- />
|
|
|
|
|
- </article>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
|
|
|
|
|
+ ref={(node) => {
|
|
|
|
|
+ if (node) {
|
|
|
|
|
+ // Initialize comparison sliders if present in preview
|
|
|
|
|
+ const sliders = node.querySelectorAll(".comparison-slider");
|
|
|
|
|
+ sliders.forEach(slider => {
|
|
|
|
|
+ slider.oninput = (e) => {
|
|
|
|
|
+ const container = e.target.closest(".comparison-wrapper");
|
|
|
|
|
+ const topImage = container.querySelector(".comparison-top");
|
|
|
|
|
+ const handle = container.querySelector(".slider-handle");
|
|
|
|
|
+ const val = e.target.value;
|
|
|
|
|
+
|
|
|
|
|
+ if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
|
|
|
|
|
+ if (handle) handle.style.left = `${val}%`;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // Initialize Zoom Reels in Preview (Re-using logic from PostView slightly simplified)
|
|
|
|
|
+ const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
|
|
|
|
|
+ zoomReels.forEach(reel => {
|
|
|
|
|
+ const images = reel.querySelectorAll(".zoom-reel-img");
|
|
|
|
|
+ const viewports = reel.querySelectorAll(".zoom-reel-viewport");
|
|
|
|
|
+ const slider = reel.querySelector(".zoom-slider");
|
|
|
|
|
+ const resetBtn = reel.querySelector(".reset-zoom");
|
|
|
|
|
+
|
|
|
|
|
+ let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
|
|
|
|
|
+ const updateTransform = () => {
|
|
|
|
|
+ images.forEach(img => {
|
|
|
|
|
+ img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ slider.oninput = (e) => {
|
|
|
|
|
+ state.zoom = parseFloat(e.target.value);
|
|
|
|
|
+ if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ resetBtn.onclick = () => {
|
|
|
|
|
+ state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Simplified drag for preview
|
|
|
|
|
+ reel.onmousedown = (e) => {
|
|
|
|
|
+ if (state.zoom <= 1) return;
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ state.isDragging = true;
|
|
|
|
|
+ state.startX = e.clientX;
|
|
|
|
|
+ state.startY = e.clientY;
|
|
|
|
|
+ state.initialPanX = state.panX;
|
|
|
|
|
+ state.initialPanY = state.panY;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grabbing");
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmousemove = (e) => {
|
|
|
|
|
+ if (!state.isDragging) return;
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const dx = e.clientX - state.startX;
|
|
|
|
|
+ const dy = e.clientY - state.startY;
|
|
|
|
|
+ state.panX = state.initialPanX + dx;
|
|
|
|
|
+ state.panY = state.initialPanY + dy;
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmouseup = () => {
|
|
|
|
|
+ state.isDragging = false;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmouseleave = () => {
|
|
|
|
|
+ state.isDragging = false;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </article>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|