ui_utils.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  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. const DEFAULT_SCALE_VALUE = "auto";
  16. const DEFAULT_SCALE = 1.0;
  17. const DEFAULT_SCALE_DELTA = 1.1;
  18. const MIN_SCALE = 0.1;
  19. const MAX_SCALE = 10.0;
  20. const UNKNOWN_SCALE = 0;
  21. const MAX_AUTO_SCALE = 1.25;
  22. const SCROLLBAR_PADDING = 40;
  23. const VERTICAL_PADDING = 5;
  24. const RenderingStates = {
  25. INITIAL: 0,
  26. RUNNING: 1,
  27. PAUSED: 2,
  28. FINISHED: 3,
  29. };
  30. const PresentationModeState = {
  31. UNKNOWN: 0,
  32. NORMAL: 1,
  33. CHANGING: 2,
  34. FULLSCREEN: 3,
  35. };
  36. const SidebarView = {
  37. UNKNOWN: -1,
  38. NONE: 0,
  39. THUMBS: 1, // Default value.
  40. OUTLINE: 2,
  41. ATTACHMENTS: 3,
  42. LAYERS: 4,
  43. };
  44. const RendererType =
  45. typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || GENERIC")
  46. ? {
  47. CANVAS: "canvas",
  48. SVG: "svg",
  49. }
  50. : null;
  51. const TextLayerMode = {
  52. DISABLE: 0,
  53. ENABLE: 1,
  54. };
  55. const ScrollMode = {
  56. UNKNOWN: -1,
  57. VERTICAL: 0, // Default value.
  58. HORIZONTAL: 1,
  59. WRAPPED: 2,
  60. PAGE: 3,
  61. };
  62. const SpreadMode = {
  63. UNKNOWN: -1,
  64. NONE: 0, // Default value.
  65. ODD: 1,
  66. EVEN: 2,
  67. };
  68. // Used by `PDFViewerApplication`, and by the API unit-tests.
  69. const AutoPrintRegExp = /\bprint\s*\(/;
  70. /**
  71. * Scale factors for the canvas, necessary with HiDPI displays.
  72. */
  73. class OutputScale {
  74. constructor() {
  75. const pixelRatio = window.devicePixelRatio || 1;
  76. /**
  77. * @type {number} Horizontal scale.
  78. */
  79. this.sx = pixelRatio;
  80. /**
  81. * @type {number} Vertical scale.
  82. */
  83. this.sy = pixelRatio;
  84. }
  85. /**
  86. * @type {boolean} Returns `true` when scaling is required, `false` otherwise.
  87. */
  88. get scaled() {
  89. return this.sx !== 1 || this.sy !== 1;
  90. }
  91. }
  92. /**
  93. * Scrolls specified element into view of its parent.
  94. * @param {Object} element - The element to be visible.
  95. * @param {Object} spot - An object with optional top and left properties,
  96. * specifying the offset from the top left edge.
  97. * @param {boolean} [scrollMatches] - When scrolling search results into view,
  98. * ignore elements that either: Contains marked content identifiers,
  99. * or have the CSS-rule `overflow: hidden;` set. The default value is `false`.
  100. */
  101. function scrollIntoView(element, spot, scrollMatches = false) {
  102. // Assuming offsetParent is available (it's not available when viewer is in
  103. // hidden iframe or object). We have to scroll: if the offsetParent is not set
  104. // producing the error. See also animationStarted.
  105. let parent = element.offsetParent;
  106. if (!parent) {
  107. console.error("offsetParent is not set -- cannot scroll");
  108. return;
  109. }
  110. let offsetY = element.offsetTop + element.clientTop;
  111. let offsetX = element.offsetLeft + element.clientLeft;
  112. while (
  113. (parent.clientHeight === parent.scrollHeight &&
  114. parent.clientWidth === parent.scrollWidth) ||
  115. (scrollMatches &&
  116. (parent.classList.contains("markedContent") ||
  117. getComputedStyle(parent).overflow === "hidden"))
  118. ) {
  119. offsetY += parent.offsetTop;
  120. offsetX += parent.offsetLeft;
  121. parent = parent.offsetParent;
  122. if (!parent) {
  123. return; // no need to scroll
  124. }
  125. }
  126. if (spot) {
  127. if (spot.top !== undefined) {
  128. offsetY += spot.top;
  129. }
  130. if (spot.left !== undefined) {
  131. offsetX += spot.left;
  132. parent.scrollLeft = offsetX;
  133. }
  134. }
  135. parent.scrollTop = offsetY;
  136. }
  137. /**
  138. * Helper function to start monitoring the scroll event and converting them into
  139. * PDF.js friendly one: with scroll debounce and scroll direction.
  140. */
  141. function watchScroll(viewAreaElement, callback) {
  142. const debounceScroll = function (evt) {
  143. if (rAF) {
  144. return;
  145. }
  146. // schedule an invocation of scroll for next animation frame.
  147. rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
  148. rAF = null;
  149. const currentX = viewAreaElement.scrollLeft;
  150. const lastX = state.lastX;
  151. if (currentX !== lastX) {
  152. state.right = currentX > lastX;
  153. }
  154. state.lastX = currentX;
  155. const currentY = viewAreaElement.scrollTop;
  156. const lastY = state.lastY;
  157. if (currentY !== lastY) {
  158. state.down = currentY > lastY;
  159. }
  160. state.lastY = currentY;
  161. callback(state);
  162. });
  163. };
  164. const state = {
  165. right: true,
  166. down: true,
  167. lastX: viewAreaElement.scrollLeft,
  168. lastY: viewAreaElement.scrollTop,
  169. _eventHandler: debounceScroll,
  170. };
  171. let rAF = null;
  172. viewAreaElement.addEventListener("scroll", debounceScroll, true);
  173. return state;
  174. }
  175. /**
  176. * Helper function to parse query string (e.g. ?param1=value&param2=...).
  177. * @param {string}
  178. * @returns {Map}
  179. */
  180. function parseQueryString(query) {
  181. const params = new Map();
  182. for (const [key, value] of new URLSearchParams(query)) {
  183. params.set(key.toLowerCase(), value);
  184. }
  185. return params;
  186. }
  187. const NullCharactersRegExp = /\x00/g;
  188. const InvisibleCharactersRegExp = /[\x01-\x1F]/g;
  189. /**
  190. * @param {string} str
  191. * @param {boolean} [replaceInvisible]
  192. */
  193. function removeNullCharacters(str, replaceInvisible = false) {
  194. if (typeof str !== "string") {
  195. console.error(`The argument must be a string.`);
  196. return str;
  197. }
  198. if (replaceInvisible) {
  199. str = str.replace(InvisibleCharactersRegExp, " ");
  200. }
  201. return str.replace(NullCharactersRegExp, "");
  202. }
  203. /**
  204. * Use binary search to find the index of the first item in a given array which
  205. * passes a given condition. The items are expected to be sorted in the sense
  206. * that if the condition is true for one item in the array, then it is also true
  207. * for all following items.
  208. *
  209. * @returns {number} Index of the first array element to pass the test,
  210. * or |items.length| if no such element exists.
  211. */
  212. function binarySearchFirstItem(items, condition, start = 0) {
  213. let minIndex = start;
  214. let maxIndex = items.length - 1;
  215. if (maxIndex < 0 || !condition(items[maxIndex])) {
  216. return items.length;
  217. }
  218. if (condition(items[minIndex])) {
  219. return minIndex;
  220. }
  221. while (minIndex < maxIndex) {
  222. const currentIndex = (minIndex + maxIndex) >> 1;
  223. const currentItem = items[currentIndex];
  224. if (condition(currentItem)) {
  225. maxIndex = currentIndex;
  226. } else {
  227. minIndex = currentIndex + 1;
  228. }
  229. }
  230. return minIndex; /* === maxIndex */
  231. }
  232. /**
  233. * Approximates float number as a fraction using Farey sequence (max order
  234. * of 8).
  235. * @param {number} x - Positive float number.
  236. * @returns {Array} Estimated fraction: the first array item is a numerator,
  237. * the second one is a denominator.
  238. */
  239. function approximateFraction(x) {
  240. // Fast paths for int numbers or their inversions.
  241. if (Math.floor(x) === x) {
  242. return [x, 1];
  243. }
  244. const xinv = 1 / x;
  245. const limit = 8;
  246. if (xinv > limit) {
  247. return [1, limit];
  248. } else if (Math.floor(xinv) === xinv) {
  249. return [1, xinv];
  250. }
  251. const x_ = x > 1 ? xinv : x;
  252. // a/b and c/d are neighbours in Farey sequence.
  253. let a = 0,
  254. b = 1,
  255. c = 1,
  256. d = 1;
  257. // Limiting search to order 8.
  258. while (true) {
  259. // Generating next term in sequence (order of q).
  260. const p = a + c,
  261. q = b + d;
  262. if (q > limit) {
  263. break;
  264. }
  265. if (x_ <= p / q) {
  266. c = p;
  267. d = q;
  268. } else {
  269. a = p;
  270. b = q;
  271. }
  272. }
  273. let result;
  274. // Select closest of the neighbours to x.
  275. if (x_ - a / b < c / d - x_) {
  276. result = x_ === x ? [a, b] : [b, a];
  277. } else {
  278. result = x_ === x ? [c, d] : [d, c];
  279. }
  280. return result;
  281. }
  282. function roundToDivide(x, div) {
  283. const r = x % div;
  284. return r === 0 ? x : Math.round(x - r + div);
  285. }
  286. /**
  287. * @typedef {Object} GetPageSizeInchesParameters
  288. * @property {number[]} view
  289. * @property {number} userUnit
  290. * @property {number} rotate
  291. */
  292. /**
  293. * @typedef {Object} PageSize
  294. * @property {number} width - In inches.
  295. * @property {number} height - In inches.
  296. */
  297. /**
  298. * Gets the size of the specified page, converted from PDF units to inches.
  299. * @param {GetPageSizeInchesParameters} params
  300. * @returns {PageSize}
  301. */
  302. function getPageSizeInches({ view, userUnit, rotate }) {
  303. const [x1, y1, x2, y2] = view;
  304. // We need to take the page rotation into account as well.
  305. const changeOrientation = rotate % 180 !== 0;
  306. const width = ((x2 - x1) / 72) * userUnit;
  307. const height = ((y2 - y1) / 72) * userUnit;
  308. return {
  309. width: changeOrientation ? height : width,
  310. height: changeOrientation ? width : height,
  311. };
  312. }
  313. /**
  314. * Helper function for getVisibleElements.
  315. *
  316. * @param {number} index - initial guess at the first visible element
  317. * @param {Array} views - array of pages, into which `index` is an index
  318. * @param {number} top - the top of the scroll pane
  319. * @returns {number} less than or equal to `index` that is definitely at or
  320. * before the first visible element in `views`, but not by too much. (Usually,
  321. * this will be the first element in the first partially visible row in
  322. * `views`, although sometimes it goes back one row further.)
  323. */
  324. function backtrackBeforeAllVisibleElements(index, views, top) {
  325. // binarySearchFirstItem's assumption is that the input is ordered, with only
  326. // one index where the conditions flips from false to true: [false ...,
  327. // true...]. With vertical scrolling and spreads, it is possible to have
  328. // [false ..., true, false, true ...]. With wrapped scrolling we can have a
  329. // similar sequence, with many more mixed true and false in the middle.
  330. //
  331. // So there is no guarantee that the binary search yields the index of the
  332. // first visible element. It could have been any of the other visible elements
  333. // that were preceded by a hidden element.
  334. // Of course, if either this element or the previous (hidden) element is also
  335. // the first element, there's nothing to worry about.
  336. if (index < 2) {
  337. return index;
  338. }
  339. // That aside, the possible cases are represented below.
  340. //
  341. // **** = fully hidden
  342. // A*B* = mix of partially visible and/or hidden pages
  343. // CDEF = fully visible
  344. //
  345. // (1) Binary search could have returned A, in which case we can stop.
  346. // (2) Binary search could also have returned B, in which case we need to
  347. // check the whole row.
  348. // (3) Binary search could also have returned C, in which case we need to
  349. // check the whole previous row.
  350. //
  351. // There's one other possibility:
  352. //
  353. // **** = fully hidden
  354. // ABCD = mix of fully and/or partially visible pages
  355. //
  356. // (4) Binary search could only have returned A.
  357. // Initially assume that we need to find the beginning of the current row
  358. // (case 1, 2, or 4), which means finding a page that is above the current
  359. // page's top. If the found page is partially visible, we're definitely not in
  360. // case 3, and this assumption is correct.
  361. let elt = views[index].div;
  362. let pageTop = elt.offsetTop + elt.clientTop;
  363. if (pageTop >= top) {
  364. // The found page is fully visible, so we're actually either in case 3 or 4,
  365. // and unfortunately we can't tell the difference between them without
  366. // scanning the entire previous row, so we just conservatively assume that
  367. // we do need to backtrack to that row. In both cases, the previous page is
  368. // in the previous row, so use its top instead.
  369. elt = views[index - 1].div;
  370. pageTop = elt.offsetTop + elt.clientTop;
  371. }
  372. // Now we backtrack to the first page that still has its bottom below
  373. // `pageTop`, which is the top of a page in the first visible row (unless
  374. // we're in case 4, in which case it's the row before that).
  375. // `index` is found by binary search, so the page at `index - 1` is
  376. // invisible and we can start looking for potentially visible pages from
  377. // `index - 2`. (However, if this loop terminates on its first iteration,
  378. // which is the case when pages are stacked vertically, `index` should remain
  379. // unchanged, so we use a distinct loop variable.)
  380. for (let i = index - 2; i >= 0; --i) {
  381. elt = views[i].div;
  382. if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
  383. // We have reached the previous row, so stop now.
  384. // This loop is expected to terminate relatively quickly because the
  385. // number of pages per row is expected to be small.
  386. break;
  387. }
  388. index = i;
  389. }
  390. return index;
  391. }
  392. /**
  393. * @typedef {Object} GetVisibleElementsParameters
  394. * @property {HTMLElement} scrollEl - A container that can possibly scroll.
  395. * @property {Array} views - Objects with a `div` property that contains an
  396. * HTMLElement, which should all be descendants of `scrollEl` satisfying the
  397. * relevant layout assumptions.
  398. * @property {boolean} sortByVisibility - If `true`, the returned elements are
  399. * sorted in descending order of the percent of their padding box that is
  400. * visible. The default value is `false`.
  401. * @property {boolean} horizontal - If `true`, the elements are assumed to be
  402. * laid out horizontally instead of vertically. The default value is `false`.
  403. * @property {boolean} rtl - If `true`, the `scrollEl` container is assumed to
  404. * be in right-to-left mode. The default value is `false`.
  405. */
  406. /**
  407. * Generic helper to find out what elements are visible within a scroll pane.
  408. *
  409. * Well, pretty generic. There are some assumptions placed on the elements
  410. * referenced by `views`:
  411. * - If `horizontal`, no left of any earlier element is to the right of the
  412. * left of any later element.
  413. * - Otherwise, `views` can be split into contiguous rows where, within a row,
  414. * no top of any element is below the bottom of any other element, and
  415. * between rows, no bottom of any element in an earlier row is below the
  416. * top of any element in a later row.
  417. *
  418. * (Here, top, left, etc. all refer to the padding edge of the element in
  419. * question. For pages, that ends up being equivalent to the bounding box of the
  420. * rendering canvas. Earlier and later refer to index in `views`, not page
  421. * layout.)
  422. *
  423. * @param {GetVisibleElementsParameters}
  424. * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
  425. */
  426. function getVisibleElements({
  427. scrollEl,
  428. views,
  429. sortByVisibility = false,
  430. horizontal = false,
  431. rtl = false,
  432. }) {
  433. const top = scrollEl.scrollTop,
  434. bottom = top + scrollEl.clientHeight;
  435. const left = scrollEl.scrollLeft,
  436. right = left + scrollEl.clientWidth;
  437. // Throughout this "generic" function, comments will assume we're working with
  438. // PDF document pages, which is the most important and complex case. In this
  439. // case, the visible elements we're actually interested is the page canvas,
  440. // which is contained in a wrapper which adds no padding/border/margin, which
  441. // is itself contained in `view.div` which adds no padding (but does add a
  442. // border). So, as specified in this function's doc comment, this function
  443. // does all of its work on the padding edge of the provided views, starting at
  444. // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is
  445. // the border). Adding clientWidth/Height gets us the bottom-right corner of
  446. // the padding edge.
  447. function isElementBottomAfterViewTop(view) {
  448. const element = view.div;
  449. const elementBottom =
  450. element.offsetTop + element.clientTop + element.clientHeight;
  451. return elementBottom > top;
  452. }
  453. function isElementNextAfterViewHorizontally(view) {
  454. const element = view.div;
  455. const elementLeft = element.offsetLeft + element.clientLeft;
  456. const elementRight = elementLeft + element.clientWidth;
  457. return rtl ? elementLeft < right : elementRight > left;
  458. }
  459. const visible = [],
  460. ids = new Set(),
  461. numViews = views.length;
  462. let firstVisibleElementInd = binarySearchFirstItem(
  463. views,
  464. horizontal
  465. ? isElementNextAfterViewHorizontally
  466. : isElementBottomAfterViewTop
  467. );
  468. // Please note the return value of the `binarySearchFirstItem` function when
  469. // no valid element is found (hence the `firstVisibleElementInd` check below).
  470. if (
  471. firstVisibleElementInd > 0 &&
  472. firstVisibleElementInd < numViews &&
  473. !horizontal
  474. ) {
  475. // In wrapped scrolling (or vertical scrolling with spreads), with some page
  476. // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search
  477. // condition: there can be pages with bottoms above the view top between
  478. // pages with bottoms below. This function detects and corrects that error;
  479. // see it for more comments.
  480. firstVisibleElementInd = backtrackBeforeAllVisibleElements(
  481. firstVisibleElementInd,
  482. views,
  483. top
  484. );
  485. }
  486. // lastEdge acts as a cutoff for us to stop looping, because we know all
  487. // subsequent pages will be hidden.
  488. //
  489. // When using wrapped scrolling or vertical scrolling with spreads, we can't
  490. // simply stop the first time we reach a page below the bottom of the view;
  491. // the tops of subsequent pages on the same row could still be visible. In
  492. // horizontal scrolling, we don't have that issue, so we can stop as soon as
  493. // we pass `right`, without needing the code below that handles the -1 case.
  494. let lastEdge = horizontal ? right : -1;
  495. for (let i = firstVisibleElementInd; i < numViews; i++) {
  496. const view = views[i],
  497. element = view.div;
  498. const currentWidth = element.offsetLeft + element.clientLeft;
  499. const currentHeight = element.offsetTop + element.clientTop;
  500. const viewWidth = element.clientWidth,
  501. viewHeight = element.clientHeight;
  502. const viewRight = currentWidth + viewWidth;
  503. const viewBottom = currentHeight + viewHeight;
  504. if (lastEdge === -1) {
  505. // As commented above, this is only needed in non-horizontal cases.
  506. // Setting lastEdge to the bottom of the first page that is partially
  507. // visible ensures that the next page fully below lastEdge is on the
  508. // next row, which has to be fully hidden along with all subsequent rows.
  509. if (viewBottom >= bottom) {
  510. lastEdge = viewBottom;
  511. }
  512. } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
  513. break;
  514. }
  515. if (
  516. viewBottom <= top ||
  517. currentHeight >= bottom ||
  518. viewRight <= left ||
  519. currentWidth >= right
  520. ) {
  521. continue;
  522. }
  523. const hiddenHeight =
  524. Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
  525. const hiddenWidth =
  526. Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
  527. const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
  528. fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
  529. const percent = (fractionHeight * fractionWidth * 100) | 0;
  530. visible.push({
  531. id: view.id,
  532. x: currentWidth,
  533. y: currentHeight,
  534. view,
  535. percent,
  536. widthPercent: (fractionWidth * 100) | 0,
  537. });
  538. ids.add(view.id);
  539. }
  540. const first = visible[0],
  541. last = visible.at(-1);
  542. if (sortByVisibility) {
  543. visible.sort(function (a, b) {
  544. const pc = a.percent - b.percent;
  545. if (Math.abs(pc) > 0.001) {
  546. return -pc;
  547. }
  548. return a.id - b.id; // ensure stability
  549. });
  550. }
  551. return { first, last, views: visible, ids };
  552. }
  553. /**
  554. * Event handler to suppress context menu.
  555. */
  556. function noContextMenuHandler(evt) {
  557. evt.preventDefault();
  558. }
  559. function normalizeWheelEventDirection(evt) {
  560. let delta = Math.hypot(evt.deltaX, evt.deltaY);
  561. const angle = Math.atan2(evt.deltaY, evt.deltaX);
  562. if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
  563. // All that is left-up oriented has to change the sign.
  564. delta = -delta;
  565. }
  566. return delta;
  567. }
  568. function normalizeWheelEventDelta(evt) {
  569. let delta = normalizeWheelEventDirection(evt);
  570. const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
  571. const MOUSE_DOM_DELTA_LINE_MODE = 1;
  572. const MOUSE_PIXELS_PER_LINE = 30;
  573. const MOUSE_LINES_PER_PAGE = 30;
  574. // Converts delta to per-page units
  575. if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
  576. delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
  577. } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
  578. delta /= MOUSE_LINES_PER_PAGE;
  579. }
  580. return delta;
  581. }
  582. function isValidRotation(angle) {
  583. return Number.isInteger(angle) && angle % 90 === 0;
  584. }
  585. function isValidScrollMode(mode) {
  586. return (
  587. Number.isInteger(mode) &&
  588. Object.values(ScrollMode).includes(mode) &&
  589. mode !== ScrollMode.UNKNOWN
  590. );
  591. }
  592. function isValidSpreadMode(mode) {
  593. return (
  594. Number.isInteger(mode) &&
  595. Object.values(SpreadMode).includes(mode) &&
  596. mode !== SpreadMode.UNKNOWN
  597. );
  598. }
  599. function isPortraitOrientation(size) {
  600. return size.width <= size.height;
  601. }
  602. /**
  603. * Promise that is resolved when DOM window becomes visible.
  604. */
  605. const animationStarted = new Promise(function (resolve) {
  606. if (
  607. typeof PDFJSDev !== "undefined" &&
  608. PDFJSDev.test("LIB") &&
  609. typeof window === "undefined"
  610. ) {
  611. // Prevent "ReferenceError: window is not defined" errors when running the
  612. // unit-tests in Node.js environments.
  613. setTimeout(resolve, 20);
  614. return;
  615. }
  616. window.requestAnimationFrame(resolve);
  617. });
  618. const docStyle =
  619. typeof PDFJSDev !== "undefined" &&
  620. PDFJSDev.test("LIB") &&
  621. typeof document === "undefined"
  622. ? null
  623. : document.documentElement.style;
  624. function clamp(v, min, max) {
  625. return Math.min(Math.max(v, min), max);
  626. }
  627. class ProgressBar {
  628. #classList = null;
  629. #percent = 0;
  630. #visible = true;
  631. constructor(bar) {
  632. this.#classList = bar.classList;
  633. }
  634. get percent() {
  635. return this.#percent;
  636. }
  637. set percent(val) {
  638. this.#percent = clamp(val, 0, 100);
  639. if (isNaN(val)) {
  640. this.#classList.add("indeterminate");
  641. return;
  642. }
  643. this.#classList.remove("indeterminate");
  644. docStyle.setProperty("--progressBar-percent", `${this.#percent}%`);
  645. }
  646. setWidth(viewer) {
  647. if (!viewer) {
  648. return;
  649. }
  650. const container = viewer.parentNode;
  651. const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
  652. if (scrollbarWidth > 0) {
  653. docStyle.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`);
  654. }
  655. }
  656. hide() {
  657. if (!this.#visible) {
  658. return;
  659. }
  660. this.#visible = false;
  661. this.#classList.add("hidden");
  662. }
  663. show() {
  664. if (this.#visible) {
  665. return;
  666. }
  667. this.#visible = true;
  668. this.#classList.remove("hidden");
  669. }
  670. }
  671. /**
  672. * Get the active or focused element in current DOM.
  673. *
  674. * Recursively search for the truly active or focused element in case there are
  675. * shadow DOMs.
  676. *
  677. * @returns {Element} the truly active or focused element.
  678. */
  679. function getActiveOrFocusedElement() {
  680. let curRoot = document;
  681. let curActiveOrFocused =
  682. curRoot.activeElement || curRoot.querySelector(":focus");
  683. while (curActiveOrFocused?.shadowRoot) {
  684. curRoot = curActiveOrFocused.shadowRoot;
  685. curActiveOrFocused =
  686. curRoot.activeElement || curRoot.querySelector(":focus");
  687. }
  688. return curActiveOrFocused;
  689. }
  690. /**
  691. * Converts API PageLayout values to the format used by `BaseViewer`.
  692. * @param {string} mode - The API PageLayout value.
  693. * @returns {Object}
  694. */
  695. function apiPageLayoutToViewerModes(layout) {
  696. let scrollMode = ScrollMode.VERTICAL,
  697. spreadMode = SpreadMode.NONE;
  698. switch (layout) {
  699. case "SinglePage":
  700. scrollMode = ScrollMode.PAGE;
  701. break;
  702. case "OneColumn":
  703. break;
  704. case "TwoPageLeft":
  705. scrollMode = ScrollMode.PAGE;
  706. /* falls through */
  707. case "TwoColumnLeft":
  708. spreadMode = SpreadMode.ODD;
  709. break;
  710. case "TwoPageRight":
  711. scrollMode = ScrollMode.PAGE;
  712. /* falls through */
  713. case "TwoColumnRight":
  714. spreadMode = SpreadMode.EVEN;
  715. break;
  716. }
  717. return { scrollMode, spreadMode };
  718. }
  719. /**
  720. * Converts API PageMode values to the format used by `PDFSidebar`.
  721. * NOTE: There's also a "FullScreen" parameter which is not possible to support,
  722. * since the Fullscreen API used in browsers requires that entering
  723. * fullscreen mode only occurs as a result of a user-initiated event.
  724. * @param {string} mode - The API PageMode value.
  725. * @returns {number} A value from {SidebarView}.
  726. */
  727. function apiPageModeToSidebarView(mode) {
  728. switch (mode) {
  729. case "UseNone":
  730. return SidebarView.NONE;
  731. case "UseThumbs":
  732. return SidebarView.THUMBS;
  733. case "UseOutlines":
  734. return SidebarView.OUTLINE;
  735. case "UseAttachments":
  736. return SidebarView.ATTACHMENTS;
  737. case "UseOC":
  738. return SidebarView.LAYERS;
  739. }
  740. return SidebarView.NONE; // Default value.
  741. }
  742. export {
  743. animationStarted,
  744. apiPageLayoutToViewerModes,
  745. apiPageModeToSidebarView,
  746. approximateFraction,
  747. AutoPrintRegExp,
  748. backtrackBeforeAllVisibleElements, // only exported for testing
  749. binarySearchFirstItem,
  750. DEFAULT_SCALE,
  751. DEFAULT_SCALE_DELTA,
  752. DEFAULT_SCALE_VALUE,
  753. docStyle,
  754. getActiveOrFocusedElement,
  755. getPageSizeInches,
  756. getVisibleElements,
  757. isPortraitOrientation,
  758. isValidRotation,
  759. isValidScrollMode,
  760. isValidSpreadMode,
  761. MAX_AUTO_SCALE,
  762. MAX_SCALE,
  763. MIN_SCALE,
  764. noContextMenuHandler,
  765. normalizeWheelEventDelta,
  766. normalizeWheelEventDirection,
  767. OutputScale,
  768. parseQueryString,
  769. PresentationModeState,
  770. ProgressBar,
  771. removeNullCharacters,
  772. RendererType,
  773. RenderingStates,
  774. roundToDivide,
  775. SCROLLBAR_PADDING,
  776. scrollIntoView,
  777. ScrollMode,
  778. SidebarView,
  779. SpreadMode,
  780. TextLayerMode,
  781. UNKNOWN_SCALE,
  782. VERTICAL_PADDING,
  783. watchScroll,
  784. };