@growgroup/visual-editor 0.1.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/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paste-processors.ts
|
|
3
|
+
*
|
|
4
|
+
* Content type detection and format-specific processors for rich clipboard paste.
|
|
5
|
+
* Handles: Browser HTML, Excel, Word, SVG, and plain text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateElementId } from './dom-utils';
|
|
9
|
+
import {
|
|
10
|
+
sanitizePastedHtml,
|
|
11
|
+
cleanOfficeMarkup,
|
|
12
|
+
cleanExcelMarkup,
|
|
13
|
+
detectDangerousContent,
|
|
14
|
+
} from './paste-sanitizer';
|
|
15
|
+
import { isFigmaContent, processFigmaPaste, type FigmaPasteProcessResult } from './figma-paste';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Types of content that can be detected in clipboard
|
|
19
|
+
*/
|
|
20
|
+
export type ClipboardContentType =
|
|
21
|
+
| 'image'
|
|
22
|
+
| 'html-figma'
|
|
23
|
+
| 'html-excel'
|
|
24
|
+
| 'html-word'
|
|
25
|
+
| 'html-browser'
|
|
26
|
+
| 'svg'
|
|
27
|
+
| 'plain-text'
|
|
28
|
+
| 'unknown';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result of a paste processing operation
|
|
32
|
+
*/
|
|
33
|
+
export interface PasteProcessResult {
|
|
34
|
+
success: boolean;
|
|
35
|
+
elements: HTMLElement[];
|
|
36
|
+
contentType: ClipboardContentType;
|
|
37
|
+
error?: string;
|
|
38
|
+
/** When true, caller should fallback to image paste handling */
|
|
39
|
+
shouldFallbackToImage?: boolean;
|
|
40
|
+
/** Figma file key for fetching images (only for html-figma type) */
|
|
41
|
+
figmaFileKey?: string;
|
|
42
|
+
/** Image hashes that need to be fetched from Figma API */
|
|
43
|
+
figmaImageHashes?: string[];
|
|
44
|
+
/** Node IDs for fallback rendering when hashes can't be extracted */
|
|
45
|
+
figmaImageNodeIds?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect the type of content in clipboard data.
|
|
50
|
+
*
|
|
51
|
+
* Priority:
|
|
52
|
+
* 1. HTML content from Office apps (Excel, Word) - these also include images, but HTML is preferred
|
|
53
|
+
* 2. Images (screenshots, pure image paste)
|
|
54
|
+
* 3. Other HTML content (Browser, SVG)
|
|
55
|
+
* 4. Plain text
|
|
56
|
+
*
|
|
57
|
+
* @param clipboardData - DataTransfer object from paste event
|
|
58
|
+
* @returns Detected content type
|
|
59
|
+
*/
|
|
60
|
+
export function detectContentType(clipboardData: DataTransfer): ClipboardContentType {
|
|
61
|
+
const types = Array.from(clipboardData.types);
|
|
62
|
+
const items = Array.from(clipboardData.items);
|
|
63
|
+
|
|
64
|
+
// Check for HTML content FIRST
|
|
65
|
+
// Various apps (Figma, Excel, Word) include both HTML and image data,
|
|
66
|
+
// but we want to use the HTML for better fidelity
|
|
67
|
+
if (types.includes('text/html')) {
|
|
68
|
+
const html = clipboardData.getData('text/html');
|
|
69
|
+
|
|
70
|
+
// Detect Figma - highest priority (contains kiwi-encoded design data)
|
|
71
|
+
if (isFigmaContent(html)) {
|
|
72
|
+
return 'html-figma';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Detect Excel - prioritize over image
|
|
76
|
+
if (isExcelContent(html)) {
|
|
77
|
+
return 'html-excel';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Detect Word - prioritize over image
|
|
81
|
+
if (isWordContent(html)) {
|
|
82
|
+
return 'html-word';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For non-Office HTML, check if it's a pure image paste
|
|
86
|
+
// (browsers sometimes wrap pasted images in HTML)
|
|
87
|
+
const hasImage = items.some(item => item.type.startsWith('image/'));
|
|
88
|
+
if (hasImage && isPureImageHtml(html)) {
|
|
89
|
+
return 'image';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Detect standalone SVG (not SVG embedded in HTML document)
|
|
93
|
+
if (isSvgContent(html)) {
|
|
94
|
+
return 'svg';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Generic browser HTML
|
|
98
|
+
return 'html-browser';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for images (no HTML available)
|
|
102
|
+
const hasImage = items.some(item => item.type.startsWith('image/'));
|
|
103
|
+
if (hasImage) {
|
|
104
|
+
return 'image';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Plain text fallback
|
|
108
|
+
if (types.includes('text/plain')) {
|
|
109
|
+
const text = clipboardData.getData('text/plain');
|
|
110
|
+
// Check if plain text is pure SVG code
|
|
111
|
+
if (isPureSvgText(text)) {
|
|
112
|
+
return 'svg';
|
|
113
|
+
}
|
|
114
|
+
return 'plain-text';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 'unknown';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if HTML content is from Microsoft Excel
|
|
122
|
+
*/
|
|
123
|
+
function isExcelContent(html: string): boolean {
|
|
124
|
+
return (
|
|
125
|
+
html.includes('xmlns:x="urn:schemas-microsoft-com:office:excel"') ||
|
|
126
|
+
html.includes('urn:schemas-microsoft-com:office:excel') ||
|
|
127
|
+
html.includes('mso-number-format') ||
|
|
128
|
+
(html.includes('<table') && html.includes('x:num'))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if HTML content is from Microsoft Word
|
|
134
|
+
*/
|
|
135
|
+
function isWordContent(html: string): boolean {
|
|
136
|
+
return (
|
|
137
|
+
html.includes('xmlns:w="urn:schemas-microsoft-com:office:word"') ||
|
|
138
|
+
html.includes('urn:schemas-microsoft-com:office:word') ||
|
|
139
|
+
html.includes('<o:p>') ||
|
|
140
|
+
html.includes('class="Mso') ||
|
|
141
|
+
html.includes('mso-bidi-font-family') ||
|
|
142
|
+
html.includes('mso-fareast-font-family')
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if HTML is just a wrapper for an image (browser's image paste)
|
|
148
|
+
*/
|
|
149
|
+
function isPureImageHtml(html: string): boolean {
|
|
150
|
+
// Browser wraps pasted images in minimal HTML
|
|
151
|
+
// Check if the HTML contains only an img tag with no other significant content
|
|
152
|
+
const imgMatch = html.match(/<img[^>]*>/gi);
|
|
153
|
+
if (!imgMatch) return false;
|
|
154
|
+
|
|
155
|
+
// Remove img tags and check if there's any other meaningful content
|
|
156
|
+
const withoutImg = html
|
|
157
|
+
.replace(/<img[^>]*>/gi, '')
|
|
158
|
+
.replace(/<\/?html[^>]*>/gi, '')
|
|
159
|
+
.replace(/<\/?head[^>]*>/gi, '')
|
|
160
|
+
.replace(/<\/?body[^>]*>/gi, '')
|
|
161
|
+
.replace(/<\/?meta[^>]*>/gi, '')
|
|
162
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
163
|
+
.trim();
|
|
164
|
+
|
|
165
|
+
// If only whitespace remains, it's a pure image paste
|
|
166
|
+
return withoutImg.length < 50; // Allow some minimal wrapper content
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if plain text is pure SVG code
|
|
171
|
+
*/
|
|
172
|
+
function isPureSvgText(text: string): boolean {
|
|
173
|
+
const trimmed = text.trim();
|
|
174
|
+
// Must start with <svg or <?xml followed by <svg
|
|
175
|
+
return (
|
|
176
|
+
trimmed.startsWith('<svg') ||
|
|
177
|
+
(trimmed.startsWith('<?xml') && trimmed.includes('<svg'))
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if content is standalone SVG (not SVG embedded in HTML document)
|
|
183
|
+
*/
|
|
184
|
+
function isSvgContent(html: string): boolean {
|
|
185
|
+
const trimmed = html.trim();
|
|
186
|
+
|
|
187
|
+
// Must start with <svg tag directly (standalone SVG)
|
|
188
|
+
if (trimmed.startsWith('<svg')) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Or XML declaration followed by SVG
|
|
193
|
+
if (trimmed.startsWith('<?xml') && trimmed.includes('<svg')) {
|
|
194
|
+
// Make sure it's not a full HTML document with embedded SVG
|
|
195
|
+
if (!trimmed.includes('<html') && !trimmed.includes('<body') && !trimmed.includes('<div')) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// NOT SVG if:
|
|
201
|
+
// - Contains HTML document structure (even without <body>, browsers may add wrapper elements)
|
|
202
|
+
// - Contains multiple top-level elements
|
|
203
|
+
// - Has HTML-like structure around the SVG
|
|
204
|
+
|
|
205
|
+
// Check for HTML wrapper patterns that browsers add
|
|
206
|
+
const hasHtmlWrapper =
|
|
207
|
+
trimmed.includes('<html') ||
|
|
208
|
+
trimmed.includes('<!DOCTYPE') ||
|
|
209
|
+
trimmed.includes('<meta') ||
|
|
210
|
+
trimmed.includes('<span') ||
|
|
211
|
+
trimmed.includes('<div') ||
|
|
212
|
+
trimmed.includes('<p>') ||
|
|
213
|
+
trimmed.includes('<body');
|
|
214
|
+
|
|
215
|
+
if (hasHtmlWrapper) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Add editor-specific attributes to an element and its children.
|
|
224
|
+
*
|
|
225
|
+
* @param element - Element to process
|
|
226
|
+
* @param prefix - Prefix for element IDs
|
|
227
|
+
*/
|
|
228
|
+
function addEditorAttributes(element: HTMLElement, prefix: string = 'paste'): void {
|
|
229
|
+
element.setAttribute('data-editable', 'true');
|
|
230
|
+
element.setAttribute('data-element-id', generateElementId(prefix));
|
|
231
|
+
|
|
232
|
+
// Process children recursively (for nested elements)
|
|
233
|
+
const children = element.querySelectorAll('*');
|
|
234
|
+
children.forEach((child) => {
|
|
235
|
+
if (child instanceof HTMLElement) {
|
|
236
|
+
// Only add data-editable to direct block-level children
|
|
237
|
+
// to avoid making every span/inline element separately selectable
|
|
238
|
+
const display = window.getComputedStyle(child).display;
|
|
239
|
+
if (display === 'block' || display === 'flex' || display === 'grid' || display === 'table') {
|
|
240
|
+
child.setAttribute('data-editable', 'true');
|
|
241
|
+
child.setAttribute('data-element-id', generateElementId(prefix));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Process browser HTML content.
|
|
249
|
+
* Preserves inline styles that Chrome automatically adds from computed styles.
|
|
250
|
+
*
|
|
251
|
+
* @param html - HTML string from clipboard
|
|
252
|
+
* @param doc - Document to create elements in (iframe document)
|
|
253
|
+
* @returns Array of processed HTMLElements
|
|
254
|
+
*/
|
|
255
|
+
export function processBrowserHtml(html: string, doc: Document): HTMLElement[] {
|
|
256
|
+
// Log any dangerous content for debugging
|
|
257
|
+
const dangers = detectDangerousContent(html);
|
|
258
|
+
if (dangers.dangerousPatterns.length > 0) {
|
|
259
|
+
console.warn('[paste-processors] Dangerous content detected:', dangers.dangerousPatterns);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Sanitize while preserving styles
|
|
263
|
+
const sanitized = sanitizePastedHtml(html);
|
|
264
|
+
|
|
265
|
+
// Parse the sanitized HTML
|
|
266
|
+
const parser = new DOMParser();
|
|
267
|
+
const parsedDoc = parser.parseFromString(sanitized, 'text/html');
|
|
268
|
+
|
|
269
|
+
// Extract elements from body
|
|
270
|
+
const elements: HTMLElement[] = [];
|
|
271
|
+
const bodyChildren = Array.from(parsedDoc.body.children);
|
|
272
|
+
|
|
273
|
+
bodyChildren.forEach((child) => {
|
|
274
|
+
if (child instanceof HTMLElement) {
|
|
275
|
+
// Clone to the target document
|
|
276
|
+
const cloned = doc.importNode(child, true) as HTMLElement;
|
|
277
|
+
addEditorAttributes(cloned, 'paste');
|
|
278
|
+
elements.push(cloned);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// If no block elements found, wrap text content in a div
|
|
283
|
+
if (elements.length === 0 && parsedDoc.body.textContent?.trim()) {
|
|
284
|
+
const wrapper = doc.createElement('div');
|
|
285
|
+
wrapper.innerHTML = sanitized;
|
|
286
|
+
addEditorAttributes(wrapper, 'paste');
|
|
287
|
+
elements.push(wrapper);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return elements;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Process Excel content.
|
|
295
|
+
* Preserves table structure and cell styles while removing Excel-specific markup.
|
|
296
|
+
*
|
|
297
|
+
* @param html - HTML string from Excel clipboard
|
|
298
|
+
* @param doc - Document to create elements in
|
|
299
|
+
* @returns Array of processed HTMLElements
|
|
300
|
+
*/
|
|
301
|
+
export function processExcelContent(html: string, doc: Document): HTMLElement[] {
|
|
302
|
+
// Clean Excel-specific markup
|
|
303
|
+
let cleaned = cleanExcelMarkup(html);
|
|
304
|
+
|
|
305
|
+
// Sanitize
|
|
306
|
+
cleaned = sanitizePastedHtml(cleaned);
|
|
307
|
+
|
|
308
|
+
// Parse
|
|
309
|
+
const parser = new DOMParser();
|
|
310
|
+
const parsedDoc = parser.parseFromString(cleaned, 'text/html');
|
|
311
|
+
|
|
312
|
+
const elements: HTMLElement[] = [];
|
|
313
|
+
|
|
314
|
+
// Find tables (Excel primarily exports as tables)
|
|
315
|
+
const tables = parsedDoc.querySelectorAll('table');
|
|
316
|
+
|
|
317
|
+
if (tables.length > 0) {
|
|
318
|
+
tables.forEach((table) => {
|
|
319
|
+
const cloned = doc.importNode(table, true) as HTMLTableElement;
|
|
320
|
+
|
|
321
|
+
// Add default styling if no styles present
|
|
322
|
+
if (!cloned.style.borderCollapse) {
|
|
323
|
+
cloned.style.borderCollapse = 'collapse';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Add borders to cells if not styled
|
|
327
|
+
const cells = cloned.querySelectorAll('td, th');
|
|
328
|
+
cells.forEach((cell) => {
|
|
329
|
+
const cellEl = cell as HTMLElement;
|
|
330
|
+
if (!cellEl.style.border && !cellEl.style.borderWidth) {
|
|
331
|
+
cellEl.style.border = '1px solid #d1d5db';
|
|
332
|
+
cellEl.style.padding = '8px 12px';
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
addEditorAttributes(cloned, 'excel');
|
|
337
|
+
elements.push(cloned);
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
// Fallback to browser HTML processing if no tables found
|
|
341
|
+
return processBrowserHtml(cleaned, doc);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return elements;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Process Word content.
|
|
349
|
+
* Preserves text formatting (bold, italic, colors) while removing Word-specific markup.
|
|
350
|
+
*
|
|
351
|
+
* @param html - HTML string from Word clipboard
|
|
352
|
+
* @param doc - Document to create elements in
|
|
353
|
+
* @returns Array of processed HTMLElements
|
|
354
|
+
*/
|
|
355
|
+
export function processWordContent(html: string, doc: Document): HTMLElement[] {
|
|
356
|
+
// Clean Word-specific markup
|
|
357
|
+
let cleaned = cleanOfficeMarkup(html);
|
|
358
|
+
|
|
359
|
+
// Sanitize
|
|
360
|
+
cleaned = sanitizePastedHtml(cleaned);
|
|
361
|
+
|
|
362
|
+
// Parse
|
|
363
|
+
const parser = new DOMParser();
|
|
364
|
+
const parsedDoc = parser.parseFromString(cleaned, 'text/html');
|
|
365
|
+
|
|
366
|
+
const elements: HTMLElement[] = [];
|
|
367
|
+
const bodyChildren = Array.from(parsedDoc.body.children);
|
|
368
|
+
|
|
369
|
+
bodyChildren.forEach((child) => {
|
|
370
|
+
if (child instanceof HTMLElement) {
|
|
371
|
+
const cloned = doc.importNode(child, true) as HTMLElement;
|
|
372
|
+
|
|
373
|
+
// Clean up empty paragraphs from Word
|
|
374
|
+
if (cloned.tagName === 'P' && !cloned.textContent?.trim() && !cloned.querySelector('img')) {
|
|
375
|
+
return; // Skip empty paragraphs
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
addEditorAttributes(cloned, 'word');
|
|
379
|
+
elements.push(cloned);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// If no elements, fallback to wrapper
|
|
384
|
+
if (elements.length === 0 && parsedDoc.body.textContent?.trim()) {
|
|
385
|
+
const wrapper = doc.createElement('div');
|
|
386
|
+
wrapper.innerHTML = parsedDoc.body.innerHTML;
|
|
387
|
+
addEditorAttributes(wrapper, 'word');
|
|
388
|
+
elements.push(wrapper);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return elements;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Process SVG content.
|
|
396
|
+
* Wraps SVG in a div container and ensures proper attributes.
|
|
397
|
+
*
|
|
398
|
+
* @param content - SVG string (from HTML or plain text)
|
|
399
|
+
* @param doc - Document to create elements in
|
|
400
|
+
* @returns Array of processed HTMLElements
|
|
401
|
+
*/
|
|
402
|
+
export function processSvgContent(content: string, doc: Document): HTMLElement[] {
|
|
403
|
+
// Extract SVG element
|
|
404
|
+
let svgString = content;
|
|
405
|
+
|
|
406
|
+
// If it's HTML containing SVG, extract just the SVG
|
|
407
|
+
if (content.includes('<body')) {
|
|
408
|
+
const parser = new DOMParser();
|
|
409
|
+
const parsedDoc = parser.parseFromString(content, 'text/html');
|
|
410
|
+
const svg = parsedDoc.querySelector('svg');
|
|
411
|
+
if (svg) {
|
|
412
|
+
svgString = svg.outerHTML;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Sanitize the SVG
|
|
417
|
+
const sanitized = sanitizePastedHtml(svgString);
|
|
418
|
+
|
|
419
|
+
// Parse SVG
|
|
420
|
+
const parser = new DOMParser();
|
|
421
|
+
const svgDoc = parser.parseFromString(sanitized, 'image/svg+xml');
|
|
422
|
+
const svgElement = svgDoc.querySelector('svg');
|
|
423
|
+
|
|
424
|
+
if (!svgElement) {
|
|
425
|
+
console.warn('[paste-processors] Failed to parse SVG content');
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Ensure SVG has viewBox
|
|
430
|
+
if (!svgElement.getAttribute('viewBox')) {
|
|
431
|
+
const width = svgElement.getAttribute('width') || '100';
|
|
432
|
+
const height = svgElement.getAttribute('height') || '100';
|
|
433
|
+
svgElement.setAttribute('viewBox', `0 0 ${parseFloat(width)} ${parseFloat(height)}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Create wrapper div
|
|
437
|
+
const wrapper = doc.createElement('div');
|
|
438
|
+
wrapper.style.display = 'inline-block';
|
|
439
|
+
|
|
440
|
+
// Import SVG into document
|
|
441
|
+
const importedSvg = doc.importNode(svgElement, true);
|
|
442
|
+
wrapper.appendChild(importedSvg);
|
|
443
|
+
|
|
444
|
+
addEditorAttributes(wrapper, 'svg');
|
|
445
|
+
|
|
446
|
+
return [wrapper];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Process plain text content.
|
|
451
|
+
* Wraps text in appropriate elements, preserving line breaks.
|
|
452
|
+
*
|
|
453
|
+
* @param text - Plain text string
|
|
454
|
+
* @param doc - Document to create elements in
|
|
455
|
+
* @returns Array of processed HTMLElements
|
|
456
|
+
*/
|
|
457
|
+
export function processPlainText(text: string, doc: Document): HTMLElement[] {
|
|
458
|
+
const trimmed = text.trim();
|
|
459
|
+
if (!trimmed) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Split by line breaks and create paragraphs
|
|
464
|
+
const lines = trimmed.split(/\r?\n/);
|
|
465
|
+
const elements: HTMLElement[] = [];
|
|
466
|
+
|
|
467
|
+
// Group consecutive lines into paragraphs
|
|
468
|
+
let currentParagraph: string[] = [];
|
|
469
|
+
|
|
470
|
+
lines.forEach((line, index) => {
|
|
471
|
+
if (line.trim()) {
|
|
472
|
+
currentParagraph.push(line);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Create paragraph when we hit empty line or end
|
|
476
|
+
if ((!line.trim() || index === lines.length - 1) && currentParagraph.length > 0) {
|
|
477
|
+
const p = doc.createElement('p');
|
|
478
|
+
p.innerHTML = currentParagraph.join('<br>');
|
|
479
|
+
p.style.margin = '0 0 1em 0';
|
|
480
|
+
addEditorAttributes(p, 'text');
|
|
481
|
+
elements.push(p);
|
|
482
|
+
currentParagraph = [];
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// If single line, don't wrap in p, use span
|
|
487
|
+
if (elements.length === 1 && !trimmed.includes('\n')) {
|
|
488
|
+
const span = doc.createElement('span');
|
|
489
|
+
span.textContent = trimmed;
|
|
490
|
+
addEditorAttributes(span, 'text');
|
|
491
|
+
return [span];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return elements;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Process clipboard content based on detected type.
|
|
499
|
+
*
|
|
500
|
+
* @param clipboardData - DataTransfer from paste event
|
|
501
|
+
* @param doc - Document to create elements in (iframe document)
|
|
502
|
+
* @param contentType - Pre-detected content type (optional, will detect if not provided)
|
|
503
|
+
* @returns Processing result with elements and metadata
|
|
504
|
+
*/
|
|
505
|
+
export function processClipboardContent(
|
|
506
|
+
clipboardData: DataTransfer,
|
|
507
|
+
doc: Document,
|
|
508
|
+
contentType?: ClipboardContentType
|
|
509
|
+
): PasteProcessResult {
|
|
510
|
+
const type = contentType || detectContentType(clipboardData);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
let elements: HTMLElement[] = [];
|
|
514
|
+
|
|
515
|
+
switch (type) {
|
|
516
|
+
case 'image':
|
|
517
|
+
// Images handled separately by existing useImageUpload hook
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
elements: [],
|
|
521
|
+
contentType: type,
|
|
522
|
+
error: 'Images should be handled by useImageUpload',
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
case 'html-figma': {
|
|
526
|
+
const html = clipboardData.getData('text/html');
|
|
527
|
+
const figmaResult = processFigmaPaste(html, doc);
|
|
528
|
+
|
|
529
|
+
// If Figma decode failed (schema version mismatch), signal to fallback to image
|
|
530
|
+
if (figmaResult.shouldFallbackToImage) {
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
elements: [],
|
|
534
|
+
contentType: type,
|
|
535
|
+
error: 'Figma decode failed, fallback to image',
|
|
536
|
+
shouldFallbackToImage: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Return with Figma-specific data for image fetching
|
|
541
|
+
return {
|
|
542
|
+
success: figmaResult.elements.length > 0,
|
|
543
|
+
elements: figmaResult.elements,
|
|
544
|
+
contentType: type,
|
|
545
|
+
figmaFileKey: figmaResult.fileKey,
|
|
546
|
+
figmaImageHashes: figmaResult.imageHashes,
|
|
547
|
+
figmaImageNodeIds: figmaResult.imageNodeIds,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
case 'html-excel': {
|
|
552
|
+
const html = clipboardData.getData('text/html');
|
|
553
|
+
elements = processExcelContent(html, doc);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
case 'html-word': {
|
|
558
|
+
const html = clipboardData.getData('text/html');
|
|
559
|
+
elements = processWordContent(html, doc);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case 'html-browser': {
|
|
564
|
+
const html = clipboardData.getData('text/html');
|
|
565
|
+
elements = processBrowserHtml(html, doc);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case 'svg': {
|
|
570
|
+
// Try HTML first, then plain text
|
|
571
|
+
let content = clipboardData.getData('text/html');
|
|
572
|
+
if (!content || !content.includes('<svg')) {
|
|
573
|
+
content = clipboardData.getData('text/plain');
|
|
574
|
+
}
|
|
575
|
+
elements = processSvgContent(content, doc);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
case 'plain-text': {
|
|
580
|
+
const text = clipboardData.getData('text/plain');
|
|
581
|
+
elements = processPlainText(text, doc);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'unknown':
|
|
586
|
+
default:
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
elements: [],
|
|
590
|
+
contentType: type,
|
|
591
|
+
error: 'Unknown clipboard content type',
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
success: elements.length > 0,
|
|
597
|
+
elements,
|
|
598
|
+
contentType: type,
|
|
599
|
+
};
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error('[paste-processors] Error processing clipboard content:', error);
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
elements: [],
|
|
605
|
+
contentType: type,
|
|
606
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|