"添加前端模板和运行代码模块"
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDesignMode } from './DesignModeContext';
|
||||
import { useUpdateManager } from './UpdateManager';
|
||||
import { AttributeNames } from '@xagi/design-mode-shared/attributeNames';
|
||||
|
||||
export const DesignModeManager: React.FC = () => {
|
||||
const { isDesignMode, selectElement, selectedElement, updateElementContent } = useDesignMode();
|
||||
|
||||
// Initialize UpdateManager to enable context menu and direct editing features
|
||||
useUpdateManager();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesignMode) {
|
||||
document.querySelectorAll('[data-design-selected]').forEach(el => {
|
||||
el.removeAttribute('data-design-selected');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// Don't select the overlay itself or controls (assuming they are in shadow DOM or have specific attribute)
|
||||
// Since this manager runs in the main document context (event listeners), we need to be careful.
|
||||
// The UI will be in a separate root, likely in Shadow DOM, so events might not bubble up the same way
|
||||
// OR we need to check if target is part of our UI.
|
||||
if ((e.target as HTMLElement).closest('#__vite_plugin_design_mode__')) return;
|
||||
|
||||
// Don't handle clicks on context menu
|
||||
if ((e.target as HTMLElement).closest(`[${AttributeNames.contextMenu}="true"]`)) return;
|
||||
if (
|
||||
(e.target as HTMLElement).closest(`[${AttributeNames.staticContent}="true"]`)
|
||||
|| (e.target as HTMLElement).closest(`[${AttributeNames.staticClass}="true"]`)
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
selectElement(target);;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
// Double-click handling is now managed by UpdateManager → EditManager
|
||||
// This prevents conflicts and ensures data-ignore-mutation is properly set
|
||||
/*
|
||||
const handleDoubleClick = (e: MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('#__vite_plugin_design_mode__')) return;
|
||||
if ((e.target as HTMLElement).closest(`[${AttributeNames.contextMenu}="true"]`)) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if element is marked as static content
|
||||
if (!target.hasAttribute(AttributeNames.staticContent)) {
|
||||
// alert('Not editable: only plain static text (no expressions).');
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANT: Save original content BEFORE enabling editing
|
||||
const originalContent = target.innerText;
|
||||
|
||||
// Enable content editing
|
||||
target.contentEditable = 'true';
|
||||
target.focus();
|
||||
|
||||
const cleanup = () => {
|
||||
target.contentEditable = 'false';
|
||||
target.removeEventListener('blur', handleBlur);
|
||||
target.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
cleanup();
|
||||
// Use the saved original content, not current innerText
|
||||
const newContent = target.innerText;
|
||||
if (newContent !== originalContent) {
|
||||
// Create a custom version of updateElementContent that uses our saved original
|
||||
const sourceInfoStr = target.getAttribute(AttributeNames.info);
|
||||
let filePath: string | null = null;
|
||||
let line: number | null = null;
|
||||
let column: number | null = null;
|
||||
|
||||
if (sourceInfoStr) {
|
||||
try {
|
||||
const sourceInfo = JSON.parse(sourceInfoStr);
|
||||
filePath = sourceInfo.fileName;
|
||||
line = sourceInfo.lineNumber;
|
||||
column = sourceInfo.columnNumber;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to parse ${AttributeNames.info}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (filePath && line !== null && column !== null) {
|
||||
fetch('/__appdev_design_mode/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath,
|
||||
line,
|
||||
column,
|
||||
newValue: newContent,
|
||||
type: 'content',
|
||||
originalValue: originalContent,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
console.log('[DesignMode] Content updated successfully');
|
||||
} else {
|
||||
console.error('[DesignMode] Failed to update content');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('[DesignMode] Error updating content:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
target.blur(); // Will trigger handleBlur
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
target.innerText = originalContent;
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener('blur', handleBlur);
|
||||
target.addEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
*/
|
||||
|
||||
const handleMouseOver = (e: MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('#__vite_plugin_design_mode__')) return;
|
||||
if ((e.target as HTMLElement).closest(`[${AttributeNames.contextMenu}="true"]`)) return;
|
||||
if (
|
||||
(e.target as HTMLElement).hasAttribute(AttributeNames.staticContent)
|
||||
|| (e.target as HTMLElement).hasAttribute(AttributeNames.staticClass)
|
||||
) {
|
||||
const target = e.target as HTMLElement;
|
||||
target.setAttribute('data-design-hover', 'true');
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseOut = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Keep hover while context-menu hover flag is set
|
||||
if (!target.hasAttribute(AttributeNames.contextMenuHover)) {
|
||||
target.removeAttribute('data-design-hover');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, true);
|
||||
// document.addEventListener('dblclick', handleDoubleClick, true);
|
||||
document.addEventListener('mouseover', handleMouseOver);
|
||||
document.addEventListener('mouseout', handleMouseOut);
|
||||
|
||||
// Inject global styles for design mode into the MAIN document head
|
||||
const styleId = 'appdev-design-mode-styles';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
[data-design-hover="true"] {
|
||||
outline: 2px dashed #60a5fa !important; /* blue-400 */
|
||||
outline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-design-selected="true"] {
|
||||
outline: 2px solid #2563eb !important; /* blue-600 */
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[contenteditable="true"] {
|
||||
outline: 2px solid #22c55e !important; /* green-500 */
|
||||
cursor: text;
|
||||
background-color: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Optional: remove styles when unmounting?
|
||||
// For now, let's keep them or we can remove them if we want to be clean.
|
||||
// Since this component might be mounted/unmounted, maybe better to keep them
|
||||
// or manage them carefully.
|
||||
// Let's remove them to be clean.
|
||||
const style = document.getElementById(styleId);
|
||||
if (style) {
|
||||
style.remove();
|
||||
}
|
||||
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
// document.removeEventListener('dblclick', handleDoubleClick, true);
|
||||
document.removeEventListener('mouseover', handleMouseOver);
|
||||
document.removeEventListener('mouseout', handleMouseOut);
|
||||
|
||||
document.querySelectorAll('[data-design-hover]').forEach(el => {
|
||||
el.removeAttribute('data-design-hover');
|
||||
});
|
||||
};
|
||||
}, [isDesignMode, selectElement, updateElementContent]);
|
||||
|
||||
useEffect(() => {
|
||||
document.querySelectorAll('[data-design-selected]').forEach(el => {
|
||||
el.removeAttribute('data-design-selected');
|
||||
});
|
||||
|
||||
if (selectedElement) {
|
||||
selectedElement.setAttribute('data-design-selected', 'true');
|
||||
}
|
||||
}, [selectedElement]);
|
||||
|
||||
return null; // No UI to render, just logic and global side effects
|
||||
};
|
||||
Reference in New Issue
Block a user