Skip to content

Commit b7145cf

Browse files
authored
Merge pull request #27 from EinfachMxrc/fix/office-init-timing
fix: correct slide detection for PowerPoint Online
2 parents e6d855b + bff7ad0 commit b7145cf

1 file changed

Lines changed: 99 additions & 85 deletions

File tree

apps/web/src/lib/powerpoint/officeBridge.ts

Lines changed: 99 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let pollTimer: ReturnType<typeof setInterval> | null = null;
2121
let windowBlurListener: (() => void) | null = null;
2222
let windowFocusListener: (() => void) | null = null;
2323

24+
// Poll often enough to catch slide changes in both Online and Desktop
2425
const POLL_MS = 500;
2526

2627
export 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+
3750
export 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
92110
function handleSelectionChanged() {
93111
void syncCurrentSlide();
94112
}
@@ -113,15 +131,8 @@ function startPolling() {
113131

114132
function 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
*/
151163
export 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(/\.pptx?$/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

254268
export function destroyOfficeBridge() {

0 commit comments

Comments
 (0)