dpu-onlyoffice-react
Version:
React component for OnlyOffice Document Server integration with version history support
1,028 lines (1,021 loc) • 41.9 kB
JavaScript
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import { useState, forwardRef, useRef, useCallback, useImperativeHandle, useEffect } from 'react';
export { default as React } from 'react';
import { Dialog, DialogTitle, Typography, Button, DialogContent, FormControl, RadioGroup, FormControlLabel, Box, Radio, DialogActions } from '@mui/material';
let scriptLoaded = false;
let scriptLoading = false;
let scriptLoadPromise = null;
/**
* Default script URLs
*/
const DEFAULT_SCRIPT_URL = 'https://documentserver.dpunity.com/web-apps/apps/api/documents/api.js';
const DEFAULT_TIMEOUT = 30000; // 30 seconds
/**
* Load OnlyOffice API script dynamically
* @param options Configuration options for script loading
* @returns Promise that resolves when script is loaded
*/
const loadOnlyOfficeScript = (options = {}) => {
const { scriptUrl = DEFAULT_SCRIPT_URL, timeout = DEFAULT_TIMEOUT } = options;
// If script is already loaded
if (scriptLoaded) {
return Promise.resolve();
}
// If currently loading, return existing promise
if (scriptLoading && scriptLoadPromise) {
return scriptLoadPromise;
}
// Start loading script
scriptLoading = true;
scriptLoadPromise = new Promise((resolve, reject) => {
// Check if script is already available
if (window.DocsAPI && window.DocsAPI.DocEditor) {
scriptLoaded = true;
scriptLoading = false;
resolve();
return;
}
// Create script element
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptUrl;
script.async = true;
// Set up timeout
const timeoutId = setTimeout(() => {
scriptLoading = false;
reject(new Error(`OnlyOffice script loading timeout after ${timeout}ms`));
}, timeout);
script.onload = () => {
clearTimeout(timeoutId);
console.log('OnlyOffice API script loaded successfully');
scriptLoaded = true;
scriptLoading = false;
resolve();
};
script.onerror = () => {
clearTimeout(timeoutId);
console.error(`Failed to load OnlyOffice API script from ${scriptUrl}`);
scriptLoading = false;
reject(new Error(`Failed to load OnlyOffice API script from ${scriptUrl}`));
};
// Add script to document
document.head.appendChild(script);
});
return scriptLoadPromise;
};
/**
* Check if OnlyOffice script is loaded and available
* @returns boolean indicating if script is loaded
*/
const isOnlyOfficeScriptLoaded = () => {
return scriptLoaded && !!(window.DocsAPI && window.DocsAPI.DocEditor);
};
/**
* Reset script loading state (useful for testing)
*/
const resetScriptState = () => {
scriptLoaded = false;
scriptLoading = false;
scriptLoadPromise = null;
};
/**
* Get current script loading state
*/
const getScriptState = () => ({
loaded: scriptLoaded,
loading: scriptLoading,
hasPromise: !!scriptLoadPromise
});
/**
* Utility functions for interacting with OnlyOffice Document Editor
* These functions help users easily communicate with the editor instance
*/
/**
* Get editor instance by key
* Nếu không có key, sẽ trả về instance đầu tiên có sẵn
*/
const getEditorInstance = (editorKey) => {
if (editorKey !== undefined) {
return window.DocEditor?.instances?.[`editor-${editorKey}`] || null;
}
// Nếu không có key, trả về instance đầu tiên có sẵn
const instances = window.DocEditor?.instances;
if (instances) {
const firstKey = Object.keys(instances)[0];
return firstKey ? instances[firstKey] : null;
}
return null;
};
/**
* Get editor instance by key
* Nếu không có key, sẽ trả về instance đầu tiên có sẵn
*/
const getApiInstance = (editorKey) => {
if (editorKey !== undefined) {
return window.Api;
}
return null;
};
/**
* Refresh version history in editor
* Supports both history data and error handling
*/
const refreshHistory = (data, message = 'History updated', editorKey) => {
const editor = getEditorInstance(editorKey);
console.log('editor', editor);
if (editor && typeof editor.refreshHistory === 'function') {
if (data && typeof data === 'object' && 'error' in data) {
// Handle error case
editor.refreshHistory({ error: data.error }, message);
}
else {
// Handle success case
editor.refreshHistory(data, message);
}
}
else {
console.warn('Editor instance not found or refreshHistory method not available');
}
};
/**
* Set history data in editor
* Supports both history data and error handling
*/
const setHistoryData = (data, editorKey) => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.setHistoryData === 'function') {
if (data && typeof data === 'object' && 'error' in data) {
// Handle error case
editor.setHistoryData({ error: data.error });
}
else {
// Handle success case
editor.setHistoryData(data);
}
}
else {
console.warn('Editor instance not found or setHistoryData method not available');
}
};
/**
* Map user document data to editor format
*/
const mapDocumentDataToEditor = (userData) => {
return {
c: "compare", // Default value for compare mode
fileType: userData.fileType,
url: userData.url,
token: userData.token
};
};
/**
* Set requested document in editor
* Supports both document data and error handling
*/
const setRequestedDocument = (data, editorKey) => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.setRequestedDocument === 'function') {
if (data && typeof data === 'object' && 'error' in data) {
// Handle error case
editor.setRequestedDocument({ error: data.error });
}
else if (data.changesUrl && data.fileType && data.key && data.token && data.url && data.version) {
// Handle success case with user data
const mappedData = mapDocumentDataToEditor(data);
editor.setRequestedDocument(mappedData);
}
else {
console.warn('Invalid data format for setRequestedDocument');
}
}
else {
console.warn('Editor instance not found or setRequestedDocument method not available');
}
};
/**
* Set data for version selection dialog
* This will trigger the dialog to show version selection
* @param historyData - Data from OnlyOffice history response
* @param onVersionSelect - Callback when version is selected
* @param editorKey - Editor key for identification
*/
const setDataDialog = (historyData, onVersionSelect, editorKey) => {
// Store versions and callback in global state for dialog access
if (!window.DocEditor) {
window.DocEditor = {};
}
if (!window.DocEditor.dialogs) {
window.DocEditor.dialogs = {};
}
const dialogKey = `dialog-${editorKey || 'default'}`;
window.DocEditor.dialogs[dialogKey] = {
versions: historyData.history,
onVersionSelect,
open: true
};
// Trigger dialog open event
const event = new CustomEvent('openVersionDialog', {
detail: { dialogKey, versions: historyData.history }
});
window.dispatchEvent(event);
};
/**
* Request editor to close
*/
const requestClose = (editorKey) => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.requestClose === 'function') {
editor.requestClose();
}
else {
console.warn('Editor instance not found or requestClose method not available');
}
};
/**
* Destroy editor instance
*/
const destroyEditor = (editorKey) => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.destroyEditor === 'function') {
editor.destroyEditor();
}
// Remove from global instances
if (editorKey !== undefined && window.DocEditor?.instances?.[`editor-${editorKey}`]) {
delete window.DocEditor.instances[`editor-${editorKey}`];
}
else if (editorKey === undefined) {
// Nếu không có key cụ thể, xóa tất cả instances
if (window.DocEditor?.instances) {
Object.keys(window.DocEditor.instances).forEach(key => {
if (window.DocEditor?.instances) {
delete window.DocEditor.instances[key];
}
});
}
}
};
/**
* Check if editor is ready
*/
const isEditorReady = (editorKey) => {
const editor = getEditorInstance(editorKey);
return !!(editor && typeof editor.refreshHistory === 'function');
};
/**
* Get editor status
*/
const getEditorStatus = (editorKey) => {
const editor = getEditorInstance(editorKey);
return {
exists: !!editor,
ready: isEditorReady(editorKey),
methods: editor ? {
refreshHistory: typeof editor.refreshHistory === 'function',
setHistoryData: typeof editor.setHistoryData === 'function',
setRequestedDocument: typeof editor.setRequestedDocument === 'function',
requestClose: typeof editor.requestClose === 'function',
destroyEditor: typeof editor.destroyEditor === 'function'
} : null
};
};
/**
* Dialog component for selecting document version
* Styled exactly like OnlyOffice Comparison settings dialog
*/
const VersionSelectDialog = ({ open, onClose, onSelectVersion, versions, title = "Select document version" }) => {
const [selectedVersion, setSelectedVersion] = useState(null);
const handleVersionSelect = (version) => {
setSelectedVersion(version);
};
const handleConfirm = () => {
if (selectedVersion) {
onSelectVersion(selectedVersion);
setSelectedVersion(null);
onClose();
}
};
const handleCancel = () => {
setSelectedVersion(null);
onClose();
};
const formatDateTime = (timeString) => {
try {
const date = new Date(timeString);
return date.toLocaleString('vi-VN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
catch {
return timeString;
}
};
const getUserDisplayName = (user) => {
return user.name || user.id;
};
return (jsxs(Dialog, { open: open, onClose: handleCancel, maxWidth: "sm", fullWidth: false, PaperProps: {
sx: {
minWidth: 400,
maxWidth: 500,
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
backgroundColor: '#ffffff',
border: '1px solid #d0d0d0',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif'
}
}, children: [jsxs(DialogTitle, { sx: {
padding: '12px 16px',
borderBottom: '1px solid #e0e0e0',
backgroundColor: '#f8f8f8',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
minHeight: '20px'
}, children: [jsx(Typography, { variant: "h6", sx: {
fontSize: '14px',
fontWeight: 500,
color: '#333333',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
margin: 0,
lineHeight: 1.2
}, children: title }), jsx(Button, { onClick: handleCancel, sx: {
minWidth: 'auto',
width: '24px',
height: '24px',
padding: 0,
borderRadius: '50%',
backgroundColor: '#f0f0f0',
color: '#666666',
fontSize: '12px',
fontWeight: 'bold',
'&:hover': {
backgroundColor: '#e0e0e0',
color: '#333333'
}
}, children: "\u00D7" })] }), jsx(DialogContent, { sx: {
padding: '20px 16px',
backgroundColor: '#ffffff'
}, children: jsxs(FormControl, { component: "fieldset", sx: { width: '100%' }, children: [jsx(Typography, { variant: "body1", sx: {
fontSize: '13px',
fontWeight: 400,
color: '#333333',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
marginBottom: '12px',
lineHeight: 1.4
}, children: "Select document version" }), jsx(RadioGroup, { value: selectedVersion?.version || '', onChange: (e) => {
const version = versions.find(v => v.version === e.target.value);
if (version) {
handleVersionSelect(version);
}
}, sx: { gap: '8px' }, children: versions.length === 0 ? (jsx(Typography, { variant: "body2", sx: {
fontSize: '12px',
color: '#666666',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
textAlign: 'center',
py: 2
}, children: "No versions available" })) : (versions.map((version, index) => (jsx(FormControlLabel, { value: version.version, control: jsx(Radio, { sx: {
color: '#666666',
'&.Mui-checked': {
color: '#1976d2'
},
padding: '4px',
'& .MuiSvgIcon-root': {
fontSize: '16px'
}
} }), label: jsxs(Box, { sx: { ml: 1 }, children: [jsxs(Typography, { variant: "body2", sx: {
fontSize: '13px',
fontWeight: 400,
color: '#333333',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
lineHeight: 1.3
}, children: ["Version ", version.version] }), jsxs(Typography, { variant: "caption", sx: {
fontSize: '11px',
color: '#666666',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
display: 'block',
lineHeight: 1.2
}, children: [formatDateTime(version.created), " \u2022 ", getUserDisplayName(version.user)] })] }), sx: {
margin: 0,
padding: '4px 0',
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
width: '100%'
}
} }, version.version)))) })] }) }), jsxs(DialogActions, { sx: {
padding: '12px 16px',
backgroundColor: '#f8f8f8',
borderTop: '1px solid #e0e0e0',
justifyContent: 'flex-end',
gap: '8px'
}, children: [jsx(Button, { onClick: handleCancel, sx: {
fontSize: '13px',
fontWeight: 400,
color: '#333333',
backgroundColor: '#ffffff',
border: '1px solid #d0d0d0',
borderRadius: '3px',
padding: '6px 12px',
minWidth: '60px',
height: '28px',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
textTransform: 'none',
'&:hover': {
backgroundColor: '#f5f5f5',
borderColor: '#b0b0b0'
}
}, children: "Cancel" }), jsx(Button, { onClick: handleConfirm, disabled: !selectedVersion, sx: {
fontSize: '13px',
fontWeight: 400,
color: '#ffffff',
backgroundColor: selectedVersion ? '#4a4a4a' : '#cccccc',
border: 'none',
borderRadius: '3px',
padding: '6px 16px',
minWidth: '60px',
height: '28px',
fontFamily: 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
textTransform: 'none',
'&:hover': {
backgroundColor: selectedVersion ? '#333333' : '#cccccc'
},
'&:disabled': {
backgroundColor: '#cccccc',
color: '#999999'
}
}, children: "OK" })] })] }));
};
/**
* OnlyOffice Document Editor React Component
*
* A React wrapper for OnlyOffice Document Server that provides:
* - Document viewing and editing capabilities
* - Version history support
* - Event handling
* - Proper cleanup and memory management
*/
const DocumentEditor = forwardRef(({ config, editorKey, className, style, onError, eventHandlers, defaultKeys }, ref) => {
let messageId = 0;
const containerRef = useRef(null);
const editorInstanceRef = useRef(null);
const [documentLoaded, setDocumentLoaded] = useState(false);
// State để quản lý editorKey có thể thay đổi
const [currentEditorKey, setCurrentEditorKey] = useState(() => {
if (editorKey !== undefined) {
return editorKey;
}
// Tạo key duy nhất dựa trên timestamp và random
return `editor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
});
// State cho version selection dialog
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogVersions, setDialogVersions] = useState([]);
const [dialogCallback, setDialogCallback] = useState(null);
const [pluginAddKeySource, setPluginAddKeySource] = useState(null);
const prevEditorKeyRef = useRef(currentEditorKey);
const regenerateEditorRef = useRef(null);
/**
* Tạo editorKey mới
*/
const generateNewEditorKey = useCallback(() => {
const newKey = `editor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return newKey;
}, []);
/**
* Create event handlers - user provides handlers, library handles OnlyOffice integration
*/
const createEventHandlers = useCallback(() => {
const handlers = {};
function sendMessageToOnlyOffice(event, message) {
try {
// Add message ID for tracking
message.id = ++messageId;
// Send message to iframe
event.source.postMessage(message, '*');
}
catch (error) {
console.error('Error sending message:', error);
}
}
function loadSampleData(event, keys) {
// Send to OnlyOffice
sendMessageToOnlyOffice(event, {
type: 'SET_TEXT_DATA',
payload: keys
});
}
// Default handlers with OnlyOffice integration
handlers.onDocumentReady = (event) => {
setDocumentLoaded(true);
// Listen for messages from OnlyOffice iframe
if (defaultKeys && defaultKeys.length > 0) {
window.addEventListener('message', function (event) {
try {
const data = event.data;
if (data && data.type === 'PLUGIN_READY') {
setPluginAddKeySource(event.source);
loadSampleData(event, defaultKeys);
}
else if (data && data.type === 'TEXT_DATA_SET_SUCCESS') {
}
else if (data && data.type === 'TEXT_DATA_RESPONSE') {
setDocumentLoaded(true);
}
}
catch (error) {
console.error('Error processing message from OnlyOffice:', error);
}
});
}
if (eventHandlers?.onDocumentReady) {
eventHandlers.onDocumentReady(event);
}
};
handlers.onDocumentStateChange = (event) => {
if (eventHandlers?.onDocumentStateChange) {
eventHandlers.onDocumentStateChange(event);
}
};
handlers.onLoadComponentError = (errorCode, errorDescription) => {
console.error(`OnlyOffice Error ${errorCode}: ${errorDescription}`);
if (eventHandlers?.onLoadComponentError) {
eventHandlers.onLoadComponentError(errorCode, errorDescription);
}
if (onError) {
onError(new Error(`OnlyOffice Error ${errorCode}: ${errorDescription}`));
}
};
handlers.onRequestClose = () => {
const docEditor = getEditorInstance(currentEditorKey);
if (docEditor) {
docEditor.requestClose();
docEditor.destroyEditor();
}
if (eventHandlers?.onRequestClose) {
eventHandlers.onRequestClose();
}
};
handlers.onSubmit = (event) => {
if (eventHandlers?.onSubmit) {
eventHandlers.onSubmit(event);
}
};
// Version history handlers - user provides data, library handles OnlyOffice
handlers.onRequestHistory = (event) => {
if (eventHandlers?.onRequestHistory) {
eventHandlers.onRequestHistory(event);
}
};
handlers.onRequestHistoryData = (event) => {
if (eventHandlers?.onRequestHistoryData) {
eventHandlers.onRequestHistoryData(event);
}
};
handlers.onRequestHistoryClose = (event) => {
// Tự động tạo lại editorKey mới khi đóng history
if (regenerateEditorRef.current) {
regenerateEditorRef.current();
}
if (eventHandlers?.onRequestHistoryClose) {
eventHandlers.onRequestHistoryClose(event);
}
};
handlers.onRequestRestore = (event) => {
if (eventHandlers?.onRequestRestore) {
eventHandlers.onRequestRestore(event);
}
};
handlers.onRequestSelectDocument = (event) => {
// Gọi custom handler để user có thể set data cho dialog
if (eventHandlers?.onRequestSelectDocument) {
eventHandlers.onRequestSelectDocument(event);
}
else {
// Default behavior: hiển thị dialog với dữ liệu mẫu
const sampleHistoryData = {
history: [
{
version: "1",
created: new Date().toISOString(),
user: {
id: "sample-user-id",
name: "Người dùng mẫu"
},
key: "sample-key",
serverVersion: "",
changes: null
}
]
};
setDataDialog(sampleHistoryData, (selectedVersion) => {
// Có thể thêm logic xử lý version được chọn ở đây
}, currentEditorKey);
}
};
handlers.onRequestSaveAs = (event) => {
if (eventHandlers?.onRequestSaveAs) {
eventHandlers.onRequestSaveAs(event);
}
};
return handlers;
}, [eventHandlers, onError, currentEditorKey]);
/**
* Initialize OnlyOffice editor
*/
const initializeEditor = useCallback(async () => {
if (!containerRef.current || !config) {
console.warn("Cannot initialize editor: missing required props");
return;
}
try {
// Load OnlyOffice script first
await loadOnlyOfficeScript();
// Clear container
containerRef.current.innerHTML = '';
// Create placeholder div
const placeholder = document.createElement('div');
placeholder.id = `editor-placeholder-${currentEditorKey}`;
placeholder.style.width = '100%';
placeholder.style.height = '100%';
containerRef.current.appendChild(placeholder);
// Create editor configuration with event handlers
const handlers = createEventHandlers();
const editorConfig = {
...config,
events: {
...handlers,
// Allow config events to override default handlers
...config.events
}
};
// Initialize editor instance
if (window.DocsAPI && window.DocsAPI.DocEditor) {
editorInstanceRef.current = new window.DocsAPI.DocEditor(placeholder.id, editorConfig);
// Store instance in global window for external access
if (!window.DocEditor) {
window.DocEditor = {};
}
if (!window.DocEditor.instances) {
window.DocEditor.instances = {};
}
window.DocEditor.instances[`editor-${currentEditorKey}`] = editorInstanceRef.current;
}
else {
const error = new Error("DocsAPI is not defined");
console.error(error.message);
if (onError) {
onError(error);
}
if (config.events?.onLoadComponentError) {
config.events.onLoadComponentError(-3, error.message);
}
}
}
catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
console.error("Error initializing editor:", errorObj);
if (onError) {
onError(errorObj);
}
if (config.events?.onLoadComponentError) {
config.events.onLoadComponentError(-1, errorObj.message);
}
}
}, [config, currentEditorKey, onError, createEventHandlers]);
/**
* Cleanup editor instance
*/
const cleanupEditor = useCallback(() => {
try {
if (editorInstanceRef.current) {
// Call destroyEditor if available
if (typeof editorInstanceRef.current.destroyEditor === 'function') {
editorInstanceRef.current.destroyEditor();
}
editorInstanceRef.current = null;
}
// Remove instance from global window
if (window.DocEditor?.instances?.[`editor-${currentEditorKey}`]) {
delete window.DocEditor.instances[`editor-${currentEditorKey}`];
}
// Clear container
if (containerRef.current) {
containerRef.current.innerHTML = '';
}
}
catch (error) {
console.warn("Error during editor cleanup:", error);
}
}, [currentEditorKey]);
/**
* Tạo lại editor với key mới
*/
const regenerateEditor = useCallback(async () => {
// Cleanup editor hiện tại
cleanupEditor();
// Tạo key mới
const newKey = generateNewEditorKey();
setCurrentEditorKey(newKey);
// Đợi một chút để cleanup hoàn tất
setTimeout(() => {
initializeEditor().catch((error) => {
console.error("Failed to regenerate editor:", error);
if (onError) {
onError(error instanceof Error ? error : new Error(String(error)));
}
});
}, 100);
}, [generateNewEditorKey, cleanupEditor, initializeEditor, onError]);
// Lưu regenerateEditor vào ref để sử dụng trong event handlers
regenerateEditorRef.current = regenerateEditor;
useImperativeHandle(ref, () => ({
isDocumentLoaded: () => documentLoaded,
getAllKeys: () => {
return new Promise((resolve, reject) => {
if (!documentLoaded) {
reject(new Error(`document_not_found`));
return;
}
if (!pluginAddKeySource) {
reject(new Error(`plugin_add_key_source_not_found`));
return;
}
// Tạo unique message ID để track response
const messageId = `get_all_keys_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Tạo event listener để chờ response
const handleResponse = (event) => {
if (event.data && event.data.type === 'ALL_KEYS_RESPONSE' && event.data.messageId === messageId) {
window.removeEventListener('message', handleResponse);
resolve(event.data.payload.keys);
}
};
// Thêm timeout để tránh chờ vô hạn
const timeout = setTimeout(() => {
window.removeEventListener('message', handleResponse);
reject(new Error('timeout'));
}, 10000000);
// Listen for response
window.addEventListener('message', handleResponse);
// Send request
pluginAddKeySource.postMessage({
type: 'GET_ALL_KEYS',
messageId: messageId
}, { targetOrigin: '*' });
// Clear timeout when resolved
const originalResolve = resolve;
resolve = (value) => {
clearTimeout(timeout);
originalResolve(value);
};
});
}
}));
// Listen for dialog open events
useEffect(() => {
const handleOpenDialog = (event) => {
const { dialogKey, versions } = event.detail;
const dialogData = window.DocEditor?.dialogs?.[dialogKey];
if (dialogData && dialogData.open) {
setDialogVersions(dialogData.versions);
setDialogCallback(() => dialogData.onVersionSelect);
setDialogOpen(true);
}
};
window.addEventListener('openVersionDialog', handleOpenDialog);
return () => {
window.removeEventListener('openVersionDialog', handleOpenDialog);
};
}, []);
// Initialize editor when component mounts
useEffect(() => {
initializeEditor().catch((error) => {
console.error("Failed to initialize editor:", error);
if (onError) {
onError(error instanceof Error ? error : new Error(String(error)));
}
});
return () => {
cleanupEditor();
};
}, []); // Only run once on mount
// Handle editorKey changes
useEffect(() => {
const prevKey = prevEditorKeyRef.current;
// Skip first mount
if (prevKey === currentEditorKey) {
return;
}
// Cleanup old editor
cleanupEditor();
// Initialize new editor
setTimeout(() => {
initializeEditor().catch((error) => {
console.error("Failed to reinitialize editor:", error);
if (onError) {
onError(error instanceof Error ? error : new Error(String(error)));
}
});
}, 100); // Wait a bit to ensure cleanup is complete
prevEditorKeyRef.current = currentEditorKey;
}, [currentEditorKey, config, initializeEditor, cleanupEditor, onError, createEventHandlers]);
return (jsxs(Fragment, { children: [jsx("div", { ref: containerRef, className: className, id: "onlyoffice-iframe", style: {
width: '100%',
height: '100%',
position: 'relative',
...style
} }), jsx(VersionSelectDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelectVersion: (version) => {
if (dialogCallback) {
dialogCallback(version);
}
setDialogOpen(false);
}, versions: dialogVersions, title: "Ch\u1ECDn phi\u00EAn b\u1EA3n t\u00E0i li\u1EC7u" })] }));
});
/**
* Custom hook for managing OnlyOffice editor state and operations
*/
const useOnlyOfficeEditor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const editorInstanceRef = useRef(null);
/**
* Get editor instance from global window
*/
const getEditorInstance = useCallback((editorKey = 'default') => {
return window.DocEditor?.instances?.[`editor-${editorKey}`] || null;
}, []);
/**
* Set editor instance reference
*/
const setEditorInstance = useCallback((instance, editorKey = 'default') => {
editorInstanceRef.current = instance;
if (!window.DocEditor) {
window.DocEditor = { instances: {} };
}
if (window.DocEditor.instances) {
window.DocEditor.instances[`editor-${editorKey}`] = instance;
}
}, []);
/**
* Refresh version history in editor
*/
const refreshHistory = useCallback((history, message = 'History updated', editorKey = 'default') => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.refreshHistory === 'function') {
editor.refreshHistory(history, message);
}
else {
console.warn('Editor instance not found or refreshHistory method not available');
}
}, [getEditorInstance]);
/**
* Set history data in editor
*/
const setHistoryData = useCallback((data, editorKey = 'default') => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.setHistoryData === 'function') {
editor.setHistoryData(data);
}
else {
console.warn('Editor instance not found or setHistoryData method not available');
}
}, [getEditorInstance]);
/**
* Request editor to close
*/
const requestClose = useCallback((editorKey = 'default') => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.requestClose === 'function') {
editor.requestClose();
}
else {
console.warn('Editor instance not found or requestClose method not available');
}
}, [getEditorInstance]);
/**
* Destroy editor instance
*/
const destroyEditor = useCallback((editorKey = 'default') => {
const editor = getEditorInstance(editorKey);
if (editor && typeof editor.destroyEditor === 'function') {
editor.destroyEditor();
}
// Remove from global instances
if (window.DocEditor?.instances?.[`editor-${editorKey}`]) {
delete window.DocEditor.instances[`editor-${editorKey}`];
}
editorInstanceRef.current = null;
}, [getEditorInstance]);
/**
* Create default event handlers
*/
const createEventHandlers = useCallback((options = {}) => {
const { onHistoryRequest, onHistoryDataRequest, onHistoryClose, onClose, onRestore, fileId } = options;
return {
onDocumentReady: (event) => {
console.log("Document is loaded");
},
onRequestHistory: async (event) => {
if (!onHistoryRequest || !fileId) {
console.warn("History request handler or fileId not provided");
return;
}
try {
setIsLoading(true);
const history = await onHistoryRequest(fileId);
refreshHistory(history, "History loaded successfully");
}
catch (error) {
console.error("Error fetching version history:", error);
setError(error instanceof Error ? error : new Error(String(error)));
refreshHistory({
currentVersion: "1",
history: []
}, "Failed to load history");
}
finally {
setIsLoading(false);
}
},
onRequestHistoryData: async (event) => {
if (!onHistoryDataRequest || !fileId) {
console.warn("History data request handler or fileId not provided");
return;
}
const version = event.data;
try {
setIsLoading(true);
const data = await onHistoryDataRequest(fileId, version);
setHistoryData(data);
}
catch (error) {
console.error("Error fetching history data:", error);
setError(error instanceof Error ? error : new Error(String(error)));
setHistoryData({
error: error instanceof Error ? error.message : String(error),
version
});
}
finally {
setIsLoading(false);
}
},
onRequestHistoryClose: (event) => {
if (onHistoryClose) {
onHistoryClose();
}
},
onRequestClose: () => {
if (onClose) {
onClose();
}
else {
requestClose();
}
},
onRequestRestore: async (event) => {
if (!onRestore || !fileId) {
console.warn("Restore handler or fileId not provided");
return;
}
const version = event.data.version;
try {
setIsLoading(true);
const result = await onRestore(fileId, version);
refreshHistory(result, "Version restored successfully");
}
catch (error) {
console.error("Error restoring version:", error);
setError(error instanceof Error ? error : new Error(String(error)));
refreshHistory({
currentVersion: "1",
history: []
}, "Failed to restore version");
}
finally {
setIsLoading(false);
}
},
onLoadComponentError: (errorCode, errorDescription) => {
const error = new Error(`OnlyOffice Error ${errorCode}: ${errorDescription}`);
setError(error);
console.error(error.message);
}
};
}, [refreshHistory, setHistoryData, requestClose]);
return {
isLoading,
error,
setError,
getEditorInstance,
setEditorInstance,
refreshHistory,
setHistoryData,
requestClose,
destroyEditor,
createEventHandlers
};
};
export { DocumentEditor, destroyEditor, getApiInstance, getEditorInstance, getEditorStatus, getScriptState, isEditorReady, isOnlyOfficeScriptLoaded, loadOnlyOfficeScript, mapDocumentDataToEditor, refreshHistory, requestClose, resetScriptState, setDataDialog, setHistoryData, setRequestedDocument, useOnlyOfficeEditor };
//# sourceMappingURL=index.esm.js.map