1/* Copyright 2012 Mozilla Foundation 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { 17 normalizeWheelEventDelta, 18 PresentationModeState, 19 ScrollMode, 20 SpreadMode, 21} from "./ui_utils.js"; 22import { AnnotationEditorType } from "pdfjs-lib"; 23 24const DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms 25const ACTIVE_SELECTOR = "pdfPresentationMode"; 26const CONTROLS_SELECTOR = "pdfPresentationModeControls"; 27const MOUSE_SCROLL_COOLDOWN_TIME = 50; // in ms 28const PAGE_SWITCH_THRESHOLD = 0.1; 29 30// Number of CSS pixels for a movement to count as a swipe. 31const SWIPE_MIN_DISTANCE_THRESHOLD = 50; 32 33// Swipe angle deviation from the x or y axis before it is not 34// considered a swipe in that direction any more. 35const SWIPE_ANGLE_THRESHOLD = Math.PI / 6; 36 37/** 38 * @typedef {Object} PDFPresentationModeOptions 39 * @property {HTMLDivElement} container - The container for the viewer element. 40 * @property {PDFViewer} pdfViewer - The document viewer. 41 * @property {EventBus} eventBus - The application event bus. 42 */ 43 44class PDFPresentationMode { 45 #state = PresentationModeState.UNKNOWN; 46 47 #args = null; 48 49 /** 50 * @param {PDFPresentationModeOptions} options 51 */ 52 constructor({ container, pdfViewer, eventBus }) { 53 this.container = container; 54 this.pdfViewer = pdfViewer; 55 this.eventBus = eventBus; 56 57 this.contextMenuOpen = false; 58 this.mouseScrollTimeStamp = 0; 59 this.mouseScrollDelta = 0; 60 this.touchSwipeState = null; 61 } 62 63 /** 64 * Request the browser to enter fullscreen mode. 65 * @returns {Promise<boolean>} Indicating if the request was successful. 66 */ 67 async request() { 68 const { container, pdfViewer } = this; 69 70 if (this.active || !pdfViewer.pagesCount || !container.requestFullscreen) { 71 return false; 72 } 73 this.#addFullscreenChangeListeners(); 74 this.#notifyStateChange(PresentationModeState.CHANGING); 75 76 const promise = container.requestFullscreen(); 77 78 this.#args = { 79 pageNumber: pdfViewer.currentPageNumber, 80 scaleValue: pdfViewer.currentScaleValue, 81 scrollMode: pdfViewer.scrollMode, 82 spreadMode: null, 83 annotationEditorMode: null, 84 }; 85 86 if ( 87 pdfViewer.spreadMode !== SpreadMode.NONE && 88 !(pdfViewer.pageViewsReady && pdfViewer.hasEqualPageSizes) 89 ) { 90 console.warn( 91 "Ignoring Spread modes when entering PresentationMode, " + 92 "since the document may contain varying page sizes." 93 ); 94 this.#args.spreadMode = pdfViewer.spreadMode; 95 } 96 if (pdfViewer.annotationEditorMode !== AnnotationEditorType.DISABLE) { 97 this.#args.annotationEditorMode = pdfViewer.annotationEditorMode; 98 } 99 100 try { 101 await promise; 102 return true; 103 } catch (reason) { 104 this.#removeFullscreenChangeListeners(); 105 this.#notifyStateChange(PresentationModeState.NORMAL); 106 } 107 return false; 108 } 109 110 get active() { 111 return ( 112 this.#state === PresentationModeState.CHANGING || 113 this.#state === PresentationModeState.FULLSCREEN 114 ); 115 } 116 117 #mouseWheel(evt) { 118 if (!this.active) { 119 return; 120 } 121 evt.preventDefault(); 122 123 const delta = normalizeWheelEventDelta(evt); 124 const currentTime = Date.now(); 125 const storedTime = this.mouseScrollTimeStamp; 126 127 // If we've already switched page, avoid accidentally switching again. 128 if ( 129 currentTime > storedTime && 130 currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME 131 ) { 132 return; 133 } 134 // If the scroll direction changed, reset the accumulated scroll delta. 135 if ( 136 (this.mouseScrollDelta > 0 && delta < 0) || 137 (this.mouseScrollDelta < 0 && delta > 0) 138 ) { 139 this.#resetMouseScrollState(); 140 } 141 this.mouseScrollDelta += delta; 142 143 if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) { 144 const totalDelta = this.mouseScrollDelta; 145 this.#resetMouseScrollState(); 146 const success = 147 totalDelta > 0 148 ? this.pdfViewer.previousPage() 149 : this.pdfViewer.nextPage(); 150 if (success) { 151 this.mouseScrollTimeStamp = currentTime; 152 } 153 } 154 } 155 156 #notifyStateChange(state) { 157 this.#state = state; 158 159 this.eventBus.dispatch("presentationmodechanged", { source: this, state }); 160 } 161 162 #enter() { 163 this.#notifyStateChange(PresentationModeState.FULLSCREEN); 164 this.container.classList.add(ACTIVE_SELECTOR); 165 166 // Ensure that the correct page is scrolled into view when entering 167 // Presentation Mode, by waiting until fullscreen mode in enabled. 168 setTimeout(() => { 169 this.pdfViewer.scrollMode = ScrollMode.PAGE; 170 if (this.#args.spreadMode !== null) { 171 this.pdfViewer.spreadMode = SpreadMode.NONE; 172 } 173 this.pdfViewer.currentPageNumber = this.#args.pageNumber; 174 this.pdfViewer.currentScaleValue = "page-fit"; 175 176 if (this.#args.annotationEditorMode !== null) { 177 this.pdfViewer.annotationEditorMode = AnnotationEditorType.NONE; 178 } 179 }, 0); 180 181 this.#addWindowListeners(); 182 this.#showControls(); 183 this.contextMenuOpen = false; 184 185 // Text selection is disabled in Presentation Mode, thus it's not possible 186 // for the user to deselect text that is selected (e.g. with "Select all") 187 // when entering Presentation Mode, hence we remove any active selection. 188 window.getSelection().removeAllRanges(); 189 } 190 191 #exit() { 192 const pageNumber = this.pdfViewer.currentPageNumber; 193 this.container.classList.remove(ACTIVE_SELECTOR); 194 195 // Ensure that the correct page is scrolled into view when exiting 196 // Presentation Mode, by waiting until fullscreen mode is disabled. 197 setTimeout(() => { 198 this.#removeFullscreenChangeListeners(); 199 this.#notifyStateChange(PresentationModeState.NORMAL); 200 201 this.pdfViewer.scrollMode = this.#args.scrollMode; 202 if (this.#args.spreadMode !== null) { 203 this.pdfViewer.spreadMode = this.#args.spreadMode; 204 } 205 this.pdfViewer.currentScaleValue = this.#args.scaleValue; 206 this.pdfViewer.currentPageNumber = pageNumber; 207 208 if (this.#args.annotationEditorMode !== null) { 209 this.pdfViewer.annotationEditorMode = this.#args.annotationEditorMode; 210 } 211 this.#args = null; 212 }, 0); 213 214 this.#removeWindowListeners(); 215 this.#hideControls(); 216 this.#resetMouseScrollState(); 217 this.contextMenuOpen = false; 218 } 219 220 #mouseDown(evt) { 221 if (this.contextMenuOpen) { 222 this.contextMenuOpen = false; 223 evt.preventDefault(); 224 return; 225 } 226 if (evt.button === 0) { 227 // Enable clicking of links in presentation mode. Note: only links
228 // pointing to destinations in the current PDF document work. 229 const isInternalLink = 230 evt.target.href && evt.target.classList.contains("internalLink"); 231 if (!isInternalLink) { 232 // Unless an internal link was clicked, advance one page. 233 evt.preventDefault(); 234 235 if (evt.shiftKey) { 236 this.pdfViewer.previousPage(); 237 } else { 238 this.pdfViewer.nextPage(); 239 } 240 } 241 } 242 } 243 244 #contextMenu() { 245 this.contextMenuOpen = true; 246 } 247 248 #showControls() { 249 if (this.controlsTimeout) { 250 clearTimeout(this.controlsTimeout); 251 } else { 252 this.container.classList.add(CONTROLS_SELECTOR); 253 } 254 this.controlsTimeout = setTimeout(() => { 255 this.container.classList.remove(CONTROLS_SELECTOR); 256 delete this.controlsTimeout; 257 }, DELAY_BEFORE_HIDING_CONTROLS); 258 } 259 260 #hideControls() { 261 if (!this.controlsTimeout) { 262 return; 263 } 264 clearTimeout(this.controlsTimeout); 265 this.container.classList.remove(CONTROLS_SELECTOR); 266 delete this.controlsTimeout; 267 } 268 269 /** 270 * Resets the properties used for tracking mouse scrolling events. 271 */ 272 #resetMouseScrollState() { 273 this.mouseScrollTimeStamp = 0; 274 this.mouseScrollDelta = 0; 275 } 276 277 #touchSwipe(evt) { 278 if (!this.active) { 279 return; 280 } 281 if (evt.touches.length > 1) { 282 // Multiple touch points detected; cancel the swipe. 283 this.touchSwipeState = null; 284 return; 285 } 286 287 switch (evt.type) { 288 case "touchstart": 289 this.touchSwipeState = { 290 startX: evt.touches[0].pageX, 291 startY: evt.touches[0].pageY, 292 endX: evt.touches[0].pageX, 293 endY: evt.touches[0].pageY, 294 }; 295 break; 296 case "touchmove": 297 if (this.touchSwipeState === null) { 298 return; 299 } 300 this.touchSwipeState.endX = evt.touches[0].pageX; 301 this.touchSwipeState.endY = evt.touches[0].pageY; 302 // Avoid the swipe from triggering browser gestures (Chrome in 303 // particular has some sort of swipe gesture in fullscreen mode). 304 evt.preventDefault(); 305 break; 306 case "touchend": 307 if (this.touchSwipeState === null) { 308 return; 309 } 310 let delta = 0; 311 const dx = this.touchSwipeState.endX - this.touchSwipeState.startX; 312 const dy = this.touchSwipeState.endY - this.touchSwipeState.startY; 313 const absAngle = Math.abs(Math.atan2(dy, dx)); 314 if ( 315 Math.abs(dx) > SWIPE_MIN_DISTANCE_THRESHOLD && 316 (absAngle <= SWIPE_ANGLE_THRESHOLD || 317 absAngle >= Math.PI - SWIPE_ANGLE_THRESHOLD) 318 ) { 319 // Horizontal swipe. 320 delta = dx; 321 } else if ( 322 Math.abs(dy) > SWIPE_MIN_DISTANCE_THRESHOLD && 323 Math.abs(absAngle - Math.PI / 2) <= SWIPE_ANGLE_THRESHOLD 324 ) { 325 // Vertical swipe. 326 delta = dy; 327 } 328 if (delta > 0) { 329 this.pdfViewer.previousPage(); 330 } else if (delta < 0) { 331 this.pdfViewer.nextPage(); 332 } 333 break; 334 } 335 } 336 337 #addWindowListeners() { 338 this.showControlsBind = this.#showControls.bind(this); 339 this.mouseDownBind = this.#mouseDown.bind(this); 340 this.mouseWheelBind = this.#mouseWheel.bind(this); 341 this.resetMouseScrollStateBind = this.#resetMouseScrollState.bind(this); 342 this.contextMenuBind = this.#contextMenu.bind(this); 343 this.touchSwipeBind = this.#touchSwipe.bind(this); 344 345 window.addEventListener("mousemove", this.showControlsBind); 346 window.addEventListener("mousedown", this.mouseDownBind); 347 window.addEventListener("wheel", this.mouseWheelBind, { passive: false }); 348 window.addEventListener("keydown", this.resetMouseScrollStateBind); 349 window.addEventListener("contextmenu", this.contextMenuBind); 350 window.addEventListener("touchstart", this.touchSwipeBind); 351 window.addEventListener("touchmove", this.touchSwipeBind); 352 window.addEventListener("touchend", this.touchSwipeBind); 353 } 354 355 #removeWindowListeners() { 356 window.removeEventListener("mousemove", this.showControlsBind); 357 window.removeEventListener("mousedown", this.mouseDownBind); 358 window.removeEventListener("wheel", this.mouseWheelBind, { 359 passive: false, 360 });
361 window.removeEventListener("keydown", this.resetMouseScrollStateBind); 362 window.removeEventListener("contextmenu", this.contextMenuBind); 363 window.removeEventListener("touchstart", this.touchSwipeBind); 364 window.removeEventListener("touchmove", this.touchSwipeBind); 365 window.removeEventListener("touchend", this.touchSwipeBind); 366 367 delete this.showControlsBind; 368 delete this.mouseDownBind; 369 delete this.mouseWheelBind; 370 delete this.resetMouseScrollStateBind; 371 delete this.contextMenuBind; 372 delete this.touchSwipeBind; 373 } 374 375 #fullscreenChange() { 376 if (/* isFullscreen = */ document.fullscreenElement) { 377 this.#enter(); 378 } else { 379 this.#exit(); 380 } 381 } 382 383 #addFullscreenChangeListeners() { 384 this.fullscreenChangeBind = this.#fullscreenChange.bind(this); 385 window.addEventListener("fullscreenchange", this.fullscreenChangeBind); 386 } 387 388 #removeFullscreenChangeListeners() { 389 window.removeEventListener("fullscreenchange", this.fullscreenChangeBind); 390 delete this.fullscreenChangeBind; 391 } 392} 393 394export { PDFPresentationMode };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.