Skip to content

Commit d09302d

Browse files
committed
feat: implement document loading from remote URLs via URL parameters, enhancing user experience and multi-format support in README
1 parent 78658f2 commit d09302d

4 files changed

Lines changed: 276 additions & 94 deletions

File tree

‎index.ts‎

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const events: Record<string, MessageHandler<any, unknown>> = {
3535
// Hide the control panel when rendering office
3636
hideControlPanel();
3737
fileChunks.push(data);
38-
if (fileChunks.length >= data.totalChunks) {
38+
if (fileChunks.length >= data.totalChunks) {
3939
const { removeLoading } = showLoading();
4040
try {
4141
const file = await MessageCodec.decodeFileChunked(fileChunks);
@@ -70,7 +70,7 @@ const events: Record<string, MessageHandler<any, unknown>> = {
7070

7171
Platform.init(events);
7272

73-
const { file } = getAllQueryString();
73+
const { file, src } = getAllQueryString();
7474

7575
const onCreateNew = async (ext: string) => {
7676
// Note: Loading is now shown in the menu button click handler
@@ -704,12 +704,96 @@ createControlPanel();
704704
window.hideControlPanel = hideControlPanel;
705705
window.showControlPanel = showControlPanel;
706706

707-
if (!file) {
707+
// Function to open document from URL
708+
const openDocumentFromUrl = async (url: string, fileName?: string) => {
709+
const { removeLoading } = showLoading();
710+
try {
711+
hideControlPanel();
712+
713+
// Fetch the file from URL
714+
console.log('Fetching document from URL:', url);
715+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
716+
const response = await fetch(url);
717+
718+
if (!response.ok) {
719+
throw new Error(`Failed to fetch document: ${response.status} ${response.statusText}`);
720+
}
721+
722+
// Get file name from URL or Content-Disposition header, or use provided name
723+
let finalFileName = fileName;
724+
if (!finalFileName) {
725+
// Try to get filename from Content-Disposition header
726+
const contentDisposition = response.headers.get('Content-Disposition');
727+
if (contentDisposition) {
728+
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
729+
if (filenameMatch && filenameMatch[1]) {
730+
finalFileName = filenameMatch[1].replace(/['"]/g, '');
731+
}
732+
}
733+
734+
// If still no filename, extract from URL
735+
if (!finalFileName) {
736+
try {
737+
const urlObj = new URL(url);
738+
const pathname = urlObj.pathname;
739+
finalFileName = pathname.split('/').pop() || 'document';
740+
// Remove query parameters if any
741+
finalFileName = finalFileName.split('?')[0];
742+
} catch {
743+
finalFileName = 'document';
744+
}
745+
}
746+
}
747+
748+
// Get file blob
749+
const blob = await response.blob();
750+
const file = new File([blob], finalFileName, { type: blob.type });
751+
752+
// Set document object
753+
setDocmentObj({
754+
fileName: finalFileName,
755+
file: file,
756+
url: window.URL.createObjectURL(file),
757+
});
758+
759+
// Initialize and open document
760+
await initX2T();
761+
const { fileName: docFileName, file: fileBlob } = getDocmentObj();
762+
await handleDocumentOperation({ file: fileBlob, fileName: docFileName, isNew: !fileBlob });
763+
764+
// Show menu guide after document is loaded
765+
setTimeout(() => {
766+
showMenuGuide();
767+
}, 1000);
768+
} catch (error) {
769+
console.error('Error opening document from URL:', error);
770+
alert(`Failed to open document: ${error instanceof Error ? error.message : 'Unknown error'}`);
771+
showControlPanel();
772+
} finally {
773+
removeLoading();
774+
}
775+
};
776+
777+
// Check for file or src parameter in URL
778+
// Both parameters support opening document from URL
779+
// Priority: file > src (for backward compatibility)
780+
// Examples:
781+
// ?file=https://example.com/doc.docx
782+
// ?src=https://example.com/doc.docx
783+
// ?file=doc1.docx&src=doc2.xlsx (will use file: doc1.docx)
784+
const documentUrl = file || src;
785+
if (documentUrl) {
786+
// Decode URL if it's encoded
787+
try {
788+
const decodedUrl = decodeURIComponent(documentUrl);
789+
// Open document from URL
790+
openDocumentFromUrl(decodedUrl);
791+
} catch (error) {
792+
// If decoding fails, try using original URL
793+
console.warn('Failed to decode URL, using original:', error);
794+
openDocumentFromUrl(documentUrl);
795+
}
796+
} else {
708797
// Don't automatically open document dialog, let user choose
709798
// onOpenDocument();
710-
} else {
711-
setDocmentObj({
712-
fileName: Math.random().toString(36).substring(2, 15),
713-
url: file,
714-
});
715799
}

‎lib/x2t.ts‎

Lines changed: 66 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -542,11 +542,7 @@ class X2TConverter {
542542
const xlsxFileName = `${sanitizedBase}.xlsx`;
543543
this.x2tModule!.FS.writeFile(`/working/${binFileName}`, bin);
544544

545-
const params = this.createConversionParams(
546-
`/working/${binFileName}`,
547-
`/working/${xlsxFileName}`,
548-
'',
549-
);
545+
const params = this.createConversionParams(`/working/${binFileName}`, `/working/${xlsxFileName}`, '');
550546

551547
this.x2tModule!.FS.writeFile('/working/params.xml', params);
552548
this.executeConversion('/working/params.xml');
@@ -880,10 +876,10 @@ async function handleSaveDocument(event: SaveEvent) {
880876
if (event.data && event.data.data) {
881877
const { data, option } = event.data;
882878
const { fileName } = getDocmentObj() || {};
883-
879+
884880
// Determine target format from editor's output format
885881
let targetFormat = c_oAscFileType2[option.outputformat];
886-
882+
887883
// Only force CSV format if the original file is CSV
888884
// This check ensures XLSX and other file types are not affected
889885
// CSV files are converted to XLSX internally, so editor may return XLSX format
@@ -895,7 +891,7 @@ async function handleSaveDocument(event: SaveEvent) {
895891
// This ensures XLSX files are saved as XLSX, not CSV
896892
console.log(`Saving as ${targetFormat} format (original file: ${fileName})`);
897893
}
898-
894+
899895
// Create download
900896
await convertBinToDocumentAndDownload(data.data, fileName, targetFormat);
901897
}
@@ -956,9 +952,7 @@ async function queueEditorOperation<T>(operation: () => Promise<T>): Promise<T>
956952
try {
957953
await Promise.race([
958954
editorOperationQueue,
959-
new Promise((_, reject) =>
960-
setTimeout(() => reject(new Error('Editor operation queue timeout')), 30000)
961-
)
955+
new Promise((_, reject) => setTimeout(() => reject(new Error('Editor operation queue timeout')), 30000)),
962956
]);
963957
} catch (error) {
964958
// If timeout, log warning but continue (previous operation may have failed)
@@ -969,18 +963,18 @@ async function queueEditorOperation<T>(operation: () => Promise<T>): Promise<T>
969963
throw error;
970964
}
971965
}
972-
966+
973967
// Create a new promise for this operation
974968
let resolveOperation: () => void;
975969
let rejectOperation: (error: any) => void;
976970
const operationPromise = new Promise<void>((resolve, reject) => {
977971
resolveOperation = resolve;
978972
rejectOperation = reject;
979973
});
980-
974+
981975
// Update the queue
982976
editorOperationQueue = operationPromise;
983-
977+
984978
try {
985979
const result = await operation();
986980
resolveOperation!();
@@ -1079,22 +1073,22 @@ function createEditorInstance(config: {
10791073
}) {
10801074
return queueEditorOperation(async () => {
10811075
const { fileName, fileType, binData, media } = config;
1082-
1076+
10831077
// Check if there's an existing editor that needs cleanup
10841078
const hasExistingEditor = !!window.editor;
1085-
1079+
10861080
// Clean up old editor instance properly
10871081
if (window.editor) {
10881082
try {
10891083
console.log('Destroying previous editor instance...');
10901084
window.editor.destroyEditor();
1091-
1085+
10921086
// When switching between document types, especially from/to PPT,
10931087
// we need more time for cleanup. PPT editors are particularly resource-intensive.
10941088
// Use longer delay when switching editors or when dealing with presentations
10951089
const isPresentation = fileType === 'pptx' || fileType === 'ppt';
1096-
const destroyDelay = (hasExistingEditor && isPresentation) ? 400 : hasExistingEditor ? 250 : 150;
1097-
1090+
const destroyDelay = hasExistingEditor && isPresentation ? 400 : hasExistingEditor ? 250 : 150;
1091+
10981092
// Wait a bit for destroy to complete
10991093
await new Promise((resolve) => setTimeout(resolve, destroyDelay));
11001094
} catch (error) {
@@ -1116,69 +1110,69 @@ function createEditorInstance(config: {
11161110
// This is especially important when switching between different document types
11171111
// When switching editors, especially involving PPT, we need more time
11181112
const isPresentation = fileType === 'pptx' || fileType === 'ppt';
1119-
const cleanupDelay = (hasExistingEditor && isPresentation) ? 400 : hasExistingEditor ? 250 : 150;
1113+
const cleanupDelay = hasExistingEditor && isPresentation ? 400 : hasExistingEditor ? 250 : 150;
11201114
await new Promise((resolve) => setTimeout(resolve, cleanupDelay));
11211115

11221116
const editorLang = getOnlyOfficeLang();
11231117
console.log('Creating new editor instance for:', fileName, 'type:', fileType);
11241118

11251119
try {
11261120
window.editor = new window.DocsAPI.DocEditor('iframe', {
1127-
document: {
1128-
title: fileName,
1129-
url: fileName, // Use file name as identifier
1130-
fileType: fileType,
1131-
permissions: {
1132-
edit: true,
1133-
chat: false,
1134-
protect: false,
1135-
},
1136-
},
1137-
editorConfig: {
1138-
lang: editorLang,
1139-
customization: {
1140-
help: false,
1141-
about: false,
1142-
hideRightMenu: true,
1143-
features: {
1144-
spellcheck: {
1145-
change: false,
1121+
document: {
1122+
title: fileName,
1123+
url: fileName, // Use file name as identifier
1124+
fileType: fileType,
1125+
permissions: {
1126+
edit: true,
1127+
chat: false,
1128+
protect: false,
11461129
},
11471130
},
1148-
anonymous: {
1149-
request: false,
1150-
label: 'Guest',
1131+
editorConfig: {
1132+
lang: editorLang,
1133+
customization: {
1134+
help: false,
1135+
about: false,
1136+
hideRightMenu: true,
1137+
features: {
1138+
spellcheck: {
1139+
change: false,
1140+
},
1141+
},
1142+
anonymous: {
1143+
request: false,
1144+
label: 'Guest',
1145+
},
1146+
},
11511147
},
1152-
},
1153-
},
1154-
events: {
1155-
onAppReady: () => {
1156-
// Set media resources
1157-
if (media) {
1158-
window.editor?.sendCommand({
1159-
command: 'asc_setImageUrls',
1160-
data: { urls: media },
1161-
});
1162-
}
1163-
1164-
// Load document content
1165-
window.editor?.sendCommand({
1166-
command: 'asc_openDocument',
1167-
// @ts-expect-error binData type is handled by the editor
1168-
data: { buf: binData },
1169-
});
1170-
},
1171-
onDocumentReady: () => {
1172-
console.log(`${t('documentLoaded')}${fileName}`);
1173-
// Note: For CSV files, the save dialog may show XLSX format,
1174-
// but the actual save will be forced to CSV format in handleSaveDocument
1175-
},
1176-
onSave: handleSaveDocument,
1177-
// writeFile
1178-
// TODO: writeFile - handle when pasting images from external sources
1179-
writeFile: handleWriteFile,
1180-
},
1181-
});
1148+
events: {
1149+
onAppReady: () => {
1150+
// Set media resources
1151+
if (media) {
1152+
window.editor?.sendCommand({
1153+
command: 'asc_setImageUrls',
1154+
data: { urls: media },
1155+
});
1156+
}
1157+
1158+
// Load document content
1159+
window.editor?.sendCommand({
1160+
command: 'asc_openDocument',
1161+
// @ts-expect-error binData type is handled by the editor
1162+
data: { buf: binData },
1163+
});
1164+
},
1165+
onDocumentReady: () => {
1166+
console.log(`${t('documentLoaded')}${fileName}`);
1167+
// Note: For CSV files, the save dialog may show XLSX format,
1168+
// but the actual save will be forced to CSV format in handleSaveDocument
1169+
},
1170+
onSave: handleSaveDocument,
1171+
// writeFile
1172+
// TODO: writeFile - handle when pasting images from external sources
1173+
writeFile: handleWriteFile,
1174+
},
1175+
});
11821176
} catch (error) {
11831177
console.error('Error creating editor instance:', error);
11841178
throw error;

0 commit comments

Comments
 (0)