@@ -78,13 +78,38 @@ function openBlobInNewTab(blob: Blob, pendingWindow: Window | null): void {
7878 document . body . appendChild ( anchor ) ;
7979 anchor . click ( ) ;
8080 anchor . remove ( ) ;
81+ // Best-effort — anchor click may also silently fail in WebView.
82+ // We can't verify whether it worked, so no error toast here.
8183 }
8284
8385 setTimeout ( ( ) => {
8486 URL . revokeObjectURL ( url ) ;
8587 } , 60_000 ) ;
8688}
8789
90+ /**
91+ * Copy text content to clipboard — reliable in Office WebView where
92+ * window.open / blob URLs silently fail.
93+ */
94+ async function copyTextToClipboard ( text : string , fileName : string ) : Promise < void > {
95+ await navigator . clipboard . writeText ( text ) ;
96+ showToast ( `Copied ${ fileName } to clipboard.` ) ;
97+ }
98+
99+ /**
100+ * Download via data URI — works in WebView where blob URLs may not.
101+ */
102+ function downloadViaDataUri ( text : string , fileName : string , mimeType : string ) : void {
103+ const dataUri = `data:${ mimeType } ;charset=utf-8,${ encodeURIComponent ( text ) } ` ;
104+ const anchor = document . createElement ( "a" ) ;
105+ anchor . href = dataUri ;
106+ anchor . download = fileName ;
107+ anchor . style . display = "none" ;
108+ document . body . appendChild ( anchor ) ;
109+ anchor . click ( ) ;
110+ anchor . remove ( ) ;
111+ }
112+
88113async function openFileInBrowser ( options : {
89114 file : WorkspaceFileEntry ;
90115 fileRef : FilesDialogDetailActionFileRef ;
@@ -141,36 +166,91 @@ export function createFilesDialogDetailActions(options: CreateFilesDialogDetailA
141166 const actions = document . createElement ( "div" ) ;
142167 actions . className = "pi-files-detail-actions" ;
143168
144- const openButton = document . createElement ( "button" ) ;
145- openButton . type = "button" ;
146- openButton . className = options . file . kind === "text"
147- ? "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact"
148- : "pi-overlay-btn pi-overlay-btn--primary pi-overlay-btn--compact" ;
149- openButton . textContent = "Open ↗" ;
150- openButton . addEventListener ( "click" , ( ) => {
151- void openFileInBrowser ( {
152- file : options . file ,
153- fileRef : options . fileRef ,
154- workspace : options . workspace ,
155- auditContext : options . auditContext ,
156- } ) . catch ( ( error : unknown ) => {
157- showToast ( `Open failed: ${ getErrorMessage ( error ) } ` ) ;
169+ const isBuiltIn = isFilesDialogBuiltInDoc ( options . file ) ;
170+
171+ if ( isBuiltIn && options . file . kind === "text" ) {
172+ // Built-in docs: use clipboard + data-URI download instead of
173+ // window.open / blob URLs which silently fail in the Office WebView.
174+ // This add-in always runs inside the Office WebView (loaded via
175+ // manifest.xml into Excel's sidebar), so this path covers all
176+ // production usage. Dev-server testing in a browser is unaffected
177+ // because built-in docs are only available via the workspace.
178+ const copyButton = document . createElement ( "button" ) ;
179+ copyButton . type = "button" ;
180+ copyButton . className = "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact" ;
181+ copyButton . textContent = "Copy content" ;
182+ copyButton . addEventListener ( "click" , ( ) => {
183+ void ( async ( ) => {
184+ const result = await options . workspace . readFile ( options . file . path , {
185+ mode : "text" ,
186+ maxChars : 16_000_000 ,
187+ audit : options . auditContext ,
188+ locationKind : options . fileRef . locationKind ,
189+ } ) ;
190+ if ( result . text === undefined ) throw new Error ( "Could not read file." ) ;
191+ await copyTextToClipboard ( result . text , options . file . name ) ;
192+ } ) ( ) . catch ( ( error : unknown ) => {
193+ showToast ( `Copy failed: ${ getErrorMessage ( error ) } ` ) ;
194+ } ) ;
158195 } ) ;
159- } ) ;
160196
161- const downloadButton = document . createElement ( "button" ) ;
162- downloadButton . type = "button" ;
163- downloadButton . className = "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact" ;
164- downloadButton . textContent = "Download" ;
165- downloadButton . addEventListener ( "click" , ( ) => {
166- void options . workspace . downloadFile ( options . file . path , {
167- locationKind : options . fileRef . locationKind ,
168- } ) . catch ( ( error : unknown ) => {
169- showToast ( `Download failed: ${ getErrorMessage ( error ) } ` ) ;
197+ const downloadButton = document . createElement ( "button" ) ;
198+ downloadButton . type = "button" ;
199+ downloadButton . className = "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact" ;
200+ downloadButton . textContent = "Download" ;
201+ downloadButton . addEventListener ( "click" , ( ) => {
202+ void ( async ( ) => {
203+ const result = await options . workspace . readFile ( options . file . path , {
204+ mode : "text" ,
205+ maxChars : 16_000_000 ,
206+ audit : options . auditContext ,
207+ locationKind : options . fileRef . locationKind ,
208+ } ) ;
209+ if ( result . text === undefined ) throw new Error ( "Could not read file." ) ;
210+ downloadViaDataUri (
211+ result . text ,
212+ options . file . name ,
213+ resolveSafeBlobUrlMimeType ( options . file . mimeType || "text/plain" ) ,
214+ ) ;
215+ } ) ( ) . catch ( ( error : unknown ) => {
216+ showToast ( `Download failed: ${ getErrorMessage ( error ) } ` ) ;
217+ } ) ;
170218 } ) ;
171- } ) ;
172219
173- actions . append ( openButton , downloadButton ) ;
220+ actions . append ( copyButton , downloadButton ) ;
221+ } else {
222+ // Non-built-in files: use the standard blob URL approach.
223+ const openButton = document . createElement ( "button" ) ;
224+ openButton . type = "button" ;
225+ openButton . className = options . file . kind === "text"
226+ ? "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact"
227+ : "pi-overlay-btn pi-overlay-btn--primary pi-overlay-btn--compact" ;
228+ openButton . textContent = "Open ↗" ;
229+ openButton . addEventListener ( "click" , ( ) => {
230+ void openFileInBrowser ( {
231+ file : options . file ,
232+ fileRef : options . fileRef ,
233+ workspace : options . workspace ,
234+ auditContext : options . auditContext ,
235+ } ) . catch ( ( error : unknown ) => {
236+ showToast ( `Open failed: ${ getErrorMessage ( error ) } ` ) ;
237+ } ) ;
238+ } ) ;
239+
240+ const downloadButton = document . createElement ( "button" ) ;
241+ downloadButton . type = "button" ;
242+ downloadButton . className = "pi-overlay-btn pi-overlay-btn--ghost pi-overlay-btn--compact" ;
243+ downloadButton . textContent = "Download" ;
244+ downloadButton . addEventListener ( "click" , ( ) => {
245+ void options . workspace . downloadFile ( options . file . path , {
246+ locationKind : options . fileRef . locationKind ,
247+ } ) . catch ( ( error : unknown ) => {
248+ showToast ( `Download failed: ${ getErrorMessage ( error ) } ` ) ;
249+ } ) ;
250+ } ) ;
251+
252+ actions . append ( openButton , downloadButton ) ;
253+ }
174254
175255 const isReadOnly = options . file . readOnly || isFilesDialogBuiltInDoc ( options . file ) ;
176256 if ( isReadOnly ) {
0 commit comments