@@ -21,6 +21,7 @@ let pollTimer: ReturnType<typeof setInterval> | null = null;
2121let windowBlurListener : ( ( ) => void ) | null = null ;
2222let windowFocusListener : ( ( ) => void ) | null = null ;
2323
24+ // Poll often enough to catch slide changes in both Online and Desktop
2425const POLL_MS = 500 ;
2526
2627export function isOfficeAvailable ( ) : boolean {
@@ -34,6 +35,18 @@ function isPowerPointApiAvailable(): boolean {
3435 ) ;
3536}
3637
38+ /** True when running inside PowerPoint Online (browser-based). */
39+ function isOnline ( ) : boolean {
40+ try {
41+ return (
42+ isOfficeAvailable ( ) &&
43+ Office . context . platform === Office . PlatformType . OfficeOnline
44+ ) ;
45+ } catch {
46+ return false ;
47+ }
48+ }
49+
3750export function initOfficeBridge (
3851 callbacks : OfficeBridgeCallbacks
3952) : Promise < SyncCapability > {
@@ -56,6 +69,11 @@ export function initOfficeBridge(
5669
5770 void syncCurrentSlide ( ) ;
5871 startPolling ( ) ;
72+
73+ // Window blur/focus helps on Desktop: when the user clicks into the
74+ // slide panel the taskpane iframe loses focus — good moment to read.
75+ // (In Online everything is same-window, so these fire less usefully
76+ // but do no harm.)
5977 setupWindowListeners ( ) ;
6078
6179 try {
@@ -88,7 +106,7 @@ export function initOfficeBridge(
88106 } ) ;
89107}
90108
91- // No debounce: call immediately so getSelectedDataAsync still has focus on slide
109+ // No debounce — read immediately while the slide is still the active selection
92110function handleSelectionChanged ( ) {
93111 void syncCurrentSlide ( ) ;
94112}
@@ -113,15 +131,8 @@ function startPolling() {
113131
114132function setupWindowListeners ( ) {
115133 if ( typeof window === "undefined" ) return ;
116-
117- // Taskpane lost focus → user moved to presentation.
118- // Wait briefly for focus transfer to complete, then try reading.
119134 windowBlurListener = ( ) => setTimeout ( ( ) => void syncCurrentSlide ( ) , 80 ) ;
120-
121- // Taskpane gained focus → user just came from the slides.
122- // Read immediately before focus fully transfers to taskpane.
123135 windowFocusListener = ( ) => void syncCurrentSlide ( ) ;
124-
125136 window . addEventListener ( "blur" , windowBlurListener , { passive : true } ) ;
126137 window . addEventListener ( "focus" , windowFocusListener , { passive : true } ) ;
127138}
@@ -139,14 +150,15 @@ function teardownWindowListeners() {
139150}
140151
141152/**
142- * Get current slide info using two strategies :
153+ * Strategy :
143154 *
144- * 1. PowerPoint JS API + getSelectedSlides() (PowerPointApi 1.5)
145- * Focus-independent — works regardless of where keyboard focus is.
155+ * 1. getSelectedDataAsync(SlideRange) — PRIMARY for both Online and Desktop.
156+ * In PowerPoint Online this always works.
157+ * In Desktop it works when focus is on the slide (event handler, no debounce).
146158 *
147- * 2. Legacy getSelectedDataAsync(SlideRange)
148- * Only works when focus is on the presentation (not the taskpane).
149- * Called immediately on selection-change events so it often succeeds .
159+ * 2. PowerPoint.run + getSelectedSlides() — FALLBACK for Desktop only.
160+ * In Online, getSelectedSlides() may return wrong data (e.g. always slide 1)
161+ * so we skip it when running in Online mode .
150162 */
151163export function getCurrentSlideInfo ( ) : Promise < OfficeSlideInfo | null > {
152164 return new Promise ( ( resolve ) => {
@@ -160,95 +172,97 @@ export function getCurrentSlideInfo(): Promise<OfficeSlideInfo | null> {
160172 rawTitle . split ( / [ / \\ ] / ) . pop ( ) ?. replace ( / \. p p t x ? $ / i, "" ) ??
161173 "Praesentation" ;
162174
163- if ( isPowerPointApiAvailable ( ) ) {
164- ( globalThis as any ) . PowerPoint . run ( async ( context : any ) => {
165- try {
166- const allSlides = context . presentation . slides ;
167- allSlides . load ( "items/id" ) ;
168-
169- let selectedSlides : any = null ;
170- try {
171- selectedSlides = context . presentation . getSelectedSlides ( ) ;
172- selectedSlides . load ( "items/id" ) ;
173- } catch {
174- // getSelectedSlides requires PowerPointApi 1.5
175- }
176-
177- await context . sync ( ) ;
178-
179- const totalSlides : number = allSlides . items . length ;
180-
181- if ( selectedSlides && selectedSlides . items . length > 0 ) {
182- const selectedId = String ( selectedSlides . items [ 0 ] . id ) ;
183- const allIds = allSlides . items . map ( ( s : any ) => String ( s . id ) ) ;
184- const idx = allIds . indexOf ( selectedId ) ;
185- if ( idx >= 0 ) {
186- resolve ( { slideNumber : idx + 1 , totalSlides, presentationTitle } ) ;
175+ // ── Primary: getSelectedDataAsync ────────────────────────────────────────
176+ try {
177+ Office . context . document . getSelectedDataAsync (
178+ Office . CoercionType . SlideRange ,
179+ ( slideResult : {
180+ status : string ;
181+ value ?: { slides ?: Array < { index : number } > } ;
182+ } ) => {
183+ if ( slideResult . status !== Office . AsyncResultStatus . Failed ) {
184+ const slides = slideResult . value ?. slides ;
185+ if ( slides && slides . length > 0 ) {
186+ const slideNumber = slides [ 0 ] . index + 1 ; // index is 0-based
187+ getSlideCount ( presentationTitle , slideNumber , resolve ) ;
187188 return ;
188189 }
189190 }
190191
191- // PowerPoint.run worked but getSelectedSlides not available — fall back
192- legacyGetSlideInfo ( presentationTitle , totalSlides , resolve ) ;
193- } catch {
194- legacyGetSlideInfo ( presentationTitle , 0 , resolve ) ;
192+ // ── Fallback: PowerPoint JS API (Desktop only) ───────────────────
193+ // Skip in Online because getSelectedSlides() unreliably returns
194+ // slide 1 there regardless of which slide is actually displayed.
195+ if ( ! isOnline ( ) && isPowerPointApiAvailable ( ) ) {
196+ powerPointRunGetSlide ( presentationTitle , resolve ) ;
197+ } else {
198+ resolve ( null ) ;
199+ }
195200 }
196- } ) . catch ( ( ) => legacyGetSlideInfo ( presentationTitle , 0 , resolve ) ) ;
197- return ;
201+ ) ;
202+ } catch {
203+ resolve ( null ) ;
198204 }
199-
200- legacyGetSlideInfo ( presentationTitle , 0 , resolve ) ;
201205 } ) ;
202206}
203207
204- function legacyGetSlideInfo (
208+ /** Get total slide count, then resolve. */
209+ function getSlideCount (
205210 presentationTitle : string ,
206- knownTotalSlides : number ,
207- resolve : ( value : OfficeSlideInfo | null ) => void
211+ slideNumber : number ,
212+ resolve : ( v : OfficeSlideInfo | null ) => void
208213) : void {
209214 try {
210- Office . context . document . getSelectedDataAsync (
211- Office . CoercionType . SlideRange ,
212- ( slideResult : {
213- status : string ;
214- value ?: { slides ?: Array < { index : number } > } ;
215- } ) => {
216- if ( slideResult . status === Office . AsyncResultStatus . Failed ) {
217- resolve ( null ) ;
218- return ;
219- }
215+ ( Office . context . document as any ) . getSlideCountAsync (
216+ ( countResult : { status : string ; value : number } ) => {
217+ const totalSlides =
218+ countResult . status === Office . AsyncResultStatus . Succeeded
219+ ? countResult . value
220+ : slideNumber ;
221+ resolve ( { slideNumber, totalSlides, presentationTitle } ) ;
222+ }
223+ ) ;
224+ } catch {
225+ resolve ( { slideNumber, totalSlides : slideNumber , presentationTitle } ) ;
226+ }
227+ }
220228
221- const slides = slideResult . value ?. slides ;
222- if ( ! slides || slides . length === 0 ) {
223- resolve ( null ) ;
224- return ;
225- }
229+ /** Desktop fallback: PowerPoint.run + getSelectedSlides (PowerPointApi 1.5). */
230+ function powerPointRunGetSlide (
231+ presentationTitle : string ,
232+ resolve : ( v : OfficeSlideInfo | null ) => void
233+ ) : void {
234+ ( globalThis as any ) . PowerPoint . run ( async ( context : any ) => {
235+ try {
236+ const allSlides = context . presentation . slides ;
237+ allSlides . load ( "items/id" ) ;
226238
227- const slideNumber = slides [ 0 ] . index + 1 ;
239+ let selectedSlides : any = null ;
240+ try {
241+ selectedSlides = context . presentation . getSelectedSlides ( ) ;
242+ selectedSlides . load ( "items/id" ) ;
243+ } catch {
244+ // PowerPointApi 1.5 not available
245+ }
228246
229- if ( knownTotalSlides > 0 ) {
230- resolve ( { slideNumber, totalSlides : knownTotalSlides , presentationTitle } ) ;
231- return ;
232- }
247+ await context . sync ( ) ;
233248
234- try {
235- ( Office . context . document as any ) . getSlideCountAsync (
236- ( countResult : { status : string ; value : number } ) => {
237- const totalSlides =
238- countResult . status === Office . AsyncResultStatus . Succeeded
239- ? countResult . value
240- : slideNumber ;
241- resolve ( { slideNumber, totalSlides, presentationTitle } ) ;
242- }
243- ) ;
244- } catch {
245- resolve ( { slideNumber, totalSlides : slideNumber , presentationTitle } ) ;
249+ const totalSlides : number = allSlides . items . length ;
250+
251+ if ( selectedSlides && selectedSlides . items . length > 0 ) {
252+ const selectedId = String ( selectedSlides . items [ 0 ] . id ) ;
253+ const allIds = allSlides . items . map ( ( s : any ) => String ( s . id ) ) ;
254+ const idx = allIds . indexOf ( selectedId ) ;
255+ if ( idx >= 0 ) {
256+ resolve ( { slideNumber : idx + 1 , totalSlides, presentationTitle } ) ;
257+ return ;
246258 }
247259 }
248- ) ;
249- } catch {
250- resolve ( null ) ;
251- }
260+
261+ resolve ( null ) ;
262+ } catch {
263+ resolve ( null ) ;
264+ }
265+ } ) . catch ( ( ) => resolve ( null ) ) ;
252266}
253267
254268export function destroyOfficeBridge ( ) {
0 commit comments