ui_utils.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  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 CSS_UNITS = 96.0 / 72.0;
  16. const DEFAULT_SCALE_VALUE = 'auto';
  17. const DEFAULT_SCALE = 1.0;
  18. const MIN_SCALE = 0.10;
  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 PresentationModeState = {
  25. UNKNOWN: 0,
  26. NORMAL: 1,
  27. CHANGING: 2,
  28. FULLSCREEN: 3,
  29. };
  30. const RendererType = {
  31. CANVAS: 'canvas',
  32. SVG: 'svg',
  33. };
  34. const TextLayerMode = {
  35. DISABLE: 0,
  36. ENABLE: 1,
  37. ENABLE_ENHANCE: 2,
  38. };
  39. const ScrollMode = {
  40. UNKNOWN: -1,
  41. VERTICAL: 0, // Default value.
  42. HORIZONTAL: 1,
  43. WRAPPED: 2,
  44. };
  45. const SpreadMode = {
  46. UNKNOWN: -1,
  47. NONE: 0, // Default value.
  48. ODD: 1,
  49. EVEN: 2,
  50. };
  51. // Replaces {{arguments}} with their values.
  52. function formatL10nValue(text, args) {
  53. if (!args) {
  54. return text;
  55. }
  56. return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (all, name) => {
  57. return (name in args ? args[name] : '{{' + name + '}}');
  58. });
  59. }
  60. /**
  61. * No-op implementation of the localization service.
  62. * @implements {IL10n}
  63. */
  64. let NullL10n = {
  65. async getLanguage() {
  66. return 'en-us';
  67. },
  68. async getDirection() {
  69. return 'ltr';
  70. },
  71. async get(property, args, fallback) {
  72. return formatL10nValue(fallback, args);
  73. },
  74. async translate(element) { },
  75. };
  76. /**
  77. * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
  78. * @return {Object} The object with horizontal (sx) and vertical (sy)
  79. scales. The scaled property is set to false if scaling is
  80. not required, true otherwise.
  81. */
  82. function getOutputScale(ctx) {
  83. let devicePixelRatio = window.devicePixelRatio || 1;
  84. let backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
  85. ctx.mozBackingStorePixelRatio ||
  86. ctx.msBackingStorePixelRatio ||
  87. ctx.oBackingStorePixelRatio ||
  88. ctx.backingStorePixelRatio || 1;
  89. let pixelRatio = devicePixelRatio / backingStoreRatio;
  90. return {
  91. sx: pixelRatio,
  92. sy: pixelRatio,
  93. scaled: pixelRatio !== 1,
  94. };
  95. }
  96. /**
  97. * Scrolls specified element into view of its parent.
  98. * @param {Object} element - The element to be visible.
  99. * @param {Object} spot - An object with optional top and left properties,
  100. * specifying the offset from the top left edge.
  101. * @param {boolean} skipOverflowHiddenElements - Ignore elements that have
  102. * the CSS rule `overflow: hidden;` set. The default is false.
  103. */
  104. function scrollIntoView(element, spot, skipOverflowHiddenElements = false) {
  105. // Assuming offsetParent is available (it's not available when viewer is in
  106. // hidden iframe or object). We have to scroll: if the offsetParent is not set
  107. // producing the error. See also animationStarted.
  108. let parent = element.offsetParent;
  109. if (!parent) {
  110. console.error('offsetParent is not set -- cannot scroll');
  111. return;
  112. }
  113. let offsetY = element.offsetTop + element.clientTop;
  114. let offsetX = element.offsetLeft + element.clientLeft;
  115. while ((parent.clientHeight === parent.scrollHeight &&
  116. parent.clientWidth === parent.scrollWidth) ||
  117. (skipOverflowHiddenElements &&
  118. getComputedStyle(parent).overflow === 'hidden')) {
  119. if (parent.dataset._scaleY) {
  120. offsetY /= parent.dataset._scaleY;
  121. offsetX /= parent.dataset._scaleX;
  122. }
  123. offsetY += parent.offsetTop;
  124. offsetX += parent.offsetLeft;
  125. parent = parent.offsetParent;
  126. if (!parent) {
  127. return; // no need to scroll
  128. }
  129. }
  130. if (spot) {
  131. if (spot.top !== undefined) {
  132. offsetY += spot.top;
  133. }
  134. if (spot.left !== undefined) {
  135. offsetX += spot.left;
  136. parent.scrollLeft = offsetX;
  137. }
  138. }
  139. parent.scrollTop = offsetY;
  140. }
  141. /**
  142. * Helper function to start monitoring the scroll event and converting them into
  143. * PDF.js friendly one: with scroll debounce and scroll direction.
  144. */
  145. function watchScroll(viewAreaElement, callback) {
  146. let debounceScroll = function(evt) {
  147. if (rAF) {
  148. return;
  149. }
  150. // schedule an invocation of scroll for next animation frame.
  151. rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
  152. rAF = null;
  153. let currentX = viewAreaElement.scrollLeft;
  154. let lastX = state.lastX;
  155. if (currentX !== lastX) {
  156. state.right = currentX > lastX;
  157. }
  158. state.lastX = currentX;
  159. let currentY = viewAreaElement.scrollTop;
  160. let lastY = state.lastY;
  161. if (currentY !== lastY) {
  162. state.down = currentY > lastY;
  163. }
  164. state.lastY = currentY;
  165. callback(state);
  166. });
  167. };
  168. let state = {
  169. right: true,
  170. down: true,
  171. lastX: viewAreaElement.scrollLeft,
  172. lastY: viewAreaElement.scrollTop,
  173. _eventHandler: debounceScroll,
  174. };
  175. let rAF = null;
  176. viewAreaElement.addEventListener('scroll', debounceScroll, true);
  177. return state;
  178. }
  179. /**
  180. * Helper function to parse query string (e.g. ?param1=value&parm2=...).
  181. */
  182. function parseQueryString(query) {
  183. let parts = query.split('&');
  184. let params = Object.create(null);
  185. for (let i = 0, ii = parts.length; i < ii; ++i) {
  186. let param = parts[i].split('=');
  187. let key = param[0].toLowerCase();
  188. let value = param.length > 1 ? param[1] : null;
  189. params[decodeURIComponent(key)] = decodeURIComponent(value);
  190. }
  191. return params;
  192. }
  193. /**
  194. * Use binary search to find the index of the first item in a given array which
  195. * passes a given condition. The items are expected to be sorted in the sense
  196. * that if the condition is true for one item in the array, then it is also true
  197. * for all following items.
  198. *
  199. * @returns {Number} Index of the first array element to pass the test,
  200. * or |items.length| if no such element exists.
  201. */
  202. function binarySearchFirstItem(items, condition) {
  203. let minIndex = 0;
  204. let maxIndex = items.length - 1;
  205. if (items.length === 0 || !condition(items[maxIndex])) {
  206. return items.length;
  207. }
  208. if (condition(items[minIndex])) {
  209. return minIndex;
  210. }
  211. while (minIndex < maxIndex) {
  212. let currentIndex = (minIndex + maxIndex) >> 1;
  213. let currentItem = items[currentIndex];
  214. if (condition(currentItem)) {
  215. maxIndex = currentIndex;
  216. } else {
  217. minIndex = currentIndex + 1;
  218. }
  219. }
  220. return minIndex; /* === maxIndex */
  221. }
  222. /**
  223. * Approximates float number as a fraction using Farey sequence (max order
  224. * of 8).
  225. * @param {number} x - Positive float number.
  226. * @returns {Array} Estimated fraction: the first array item is a numerator,
  227. * the second one is a denominator.
  228. */
  229. function approximateFraction(x) {
  230. // Fast paths for int numbers or their inversions.
  231. if (Math.floor(x) === x) {
  232. return [x, 1];
  233. }
  234. let xinv = 1 / x;
  235. let limit = 8;
  236. if (xinv > limit) {
  237. return [1, limit];
  238. } else if (Math.floor(xinv) === xinv) {
  239. return [1, xinv];
  240. }
  241. let x_ = x > 1 ? xinv : x;
  242. // a/b and c/d are neighbours in Farey sequence.
  243. let a = 0, b = 1, c = 1, d = 1;
  244. // Limiting search to order 8.
  245. while (true) {
  246. // Generating next term in sequence (order of q).
  247. let p = a + c, q = b + d;
  248. if (q > limit) {
  249. break;
  250. }
  251. if (x_ <= p / q) {
  252. c = p; d = q;
  253. } else {
  254. a = p; b = q;
  255. }
  256. }
  257. let result;
  258. // Select closest of the neighbours to x.
  259. if (x_ - a / b < c / d - x_) {
  260. result = x_ === x ? [a, b] : [b, a];
  261. } else {
  262. result = x_ === x ? [c, d] : [d, c];
  263. }
  264. return result;
  265. }
  266. function roundToDivide(x, div) {
  267. let r = x % div;
  268. return r === 0 ? x : Math.round(x - r + div);
  269. }
  270. /**
  271. * Gets the size of the specified page, converted from PDF units to inches.
  272. * @param {Object} An Object containing the properties: {Array} `view`,
  273. * {number} `userUnit`, and {number} `rotate`.
  274. * @return {Object} An Object containing the properties: {number} `width`
  275. * and {number} `height`, given in inches.
  276. */
  277. function getPageSizeInches({ view, userUnit, rotate, }) {
  278. const [x1, y1, x2, y2] = view;
  279. // We need to take the page rotation into account as well.
  280. const changeOrientation = rotate % 180 !== 0;
  281. const width = (x2 - x1) / 72 * userUnit;
  282. const height = (y2 - y1) / 72 * userUnit;
  283. return {
  284. width: (changeOrientation ? height : width),
  285. height: (changeOrientation ? width : height),
  286. };
  287. }
  288. /**
  289. * Helper function for getVisibleElements.
  290. *
  291. * @param {number} index - initial guess at the first visible element
  292. * @param {Array} views - array of pages, into which `index` is an index
  293. * @param {number} top - the top of the scroll pane
  294. * @returns {number} less than or equal to `index` that is definitely at or
  295. * before the first visible element in `views`, but not by too much. (Usually,
  296. * this will be the first element in the first partially visible row in
  297. * `views`, although sometimes it goes back one row further.)
  298. */
  299. function backtrackBeforeAllVisibleElements(index, views, top) {
  300. // binarySearchFirstItem's assumption is that the input is ordered, with only
  301. // one index where the conditions flips from false to true: [false ...,
  302. // true...]. With vertical scrolling and spreads, it is possible to have
  303. // [false ..., true, false, true ...]. With wrapped scrolling we can have a
  304. // similar sequence, with many more mixed true and false in the middle.
  305. //
  306. // So there is no guarantee that the binary search yields the index of the
  307. // first visible element. It could have been any of the other visible elements
  308. // that were preceded by a hidden element.
  309. // Of course, if either this element or the previous (hidden) element is also
  310. // the first element, there's nothing to worry about.
  311. if (index < 2) {
  312. return index;
  313. }
  314. // That aside, the possible cases are represented below.
  315. //
  316. // **** = fully hidden
  317. // A*B* = mix of partially visible and/or hidden pages
  318. // CDEF = fully visible
  319. //
  320. // (1) Binary search could have returned A, in which case we can stop.
  321. // (2) Binary search could also have returned B, in which case we need to
  322. // check the whole row.
  323. // (3) Binary search could also have returned C, in which case we need to
  324. // check the whole previous row.
  325. //
  326. // There's one other possibility:
  327. //
  328. // **** = fully hidden
  329. // ABCD = mix of fully and/or partially visible pages
  330. //
  331. // (4) Binary search could only have returned A.
  332. // Initially assume that we need to find the beginning of the current row
  333. // (case 1, 2, or 4), which means finding a page that is above the current
  334. // page's top. If the found page is partially visible, we're definitely not in
  335. // case 3, and this assumption is correct.
  336. let elt = views[index].div;
  337. let pageTop = elt.offsetTop + elt.clientTop;
  338. if (pageTop >= top) {
  339. // The found page is fully visible, so we're actually either in case 3 or 4,
  340. // and unfortunately we can't tell the difference between them without
  341. // scanning the entire previous row, so we just conservatively assume that
  342. // we do need to backtrack to that row. In both cases, the previous page is
  343. // in the previous row, so use its top instead.
  344. elt = views[index - 1].div;
  345. pageTop = elt.offsetTop + elt.clientTop;
  346. }
  347. // Now we backtrack to the first page that still has its bottom below
  348. // `pageTop`, which is the top of a page in the first visible row (unless
  349. // we're in case 4, in which case it's the row before that).
  350. // `index` is found by binary search, so the page at `index - 1` is
  351. // invisible and we can start looking for potentially visible pages from
  352. // `index - 2`. (However, if this loop terminates on its first iteration,
  353. // which is the case when pages are stacked vertically, `index` should remain
  354. // unchanged, so we use a distinct loop variable.)
  355. for (let i = index - 2; i >= 0; --i) {
  356. elt = views[i].div;
  357. if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
  358. // We have reached the previous row, so stop now.
  359. // This loop is expected to terminate relatively quickly because the
  360. // number of pages per row is expected to be small.
  361. break;
  362. }
  363. index = i;
  364. }
  365. return index;
  366. }
  367. /**
  368. * Generic helper to find out what elements are visible within a scroll pane.
  369. *
  370. * Well, pretty generic. There are some assumptions placed on the elements
  371. * referenced by `views`:
  372. * - If `horizontal`, no left of any earlier element is to the right of the
  373. * left of any later element.
  374. * - Otherwise, `views` can be split into contiguous rows where, within a row,
  375. * no top of any element is below the bottom of any other element, and
  376. * between rows, no bottom of any element in an earlier row is below the
  377. * top of any element in a later row.
  378. *
  379. * (Here, top, left, etc. all refer to the padding edge of the element in
  380. * question. For pages, that ends up being equivalent to the bounding box of the
  381. * rendering canvas. Earlier and later refer to index in `views`, not page
  382. * layout.)
  383. *
  384. * @param scrollEl {HTMLElement} - a container that can possibly scroll
  385. * @param views {Array} - objects with a `div` property that contains an
  386. * HTMLElement, which should all be descendents of `scrollEl` satisfying the
  387. * above layout assumptions
  388. * @param sortByVisibility {boolean} - if true, the returned elements are sorted
  389. * in descending order of the percent of their padding box that is visible
  390. * @param horizontal {boolean} - if true, the elements are assumed to be laid
  391. * out horizontally instead of vertically
  392. * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
  393. */
  394. function getVisibleElements(scrollEl, views, sortByVisibility = false,
  395. horizontal = false) {
  396. const top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
  397. const left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
  398. // Throughout this "generic" function, comments will assume we're working with
  399. // PDF document pages, which is the most important and complex case. In this
  400. // case, the visible elements we're actually interested is the page canvas,
  401. // which is contained in a wrapper which adds no padding/border/margin, which
  402. // is itself contained in `view.div` which adds no padding (but does add a
  403. // border). So, as specified in this function's doc comment, this function
  404. // does all of its work on the padding edge of the provided views, starting at
  405. // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is
  406. // the border). Adding clientWidth/Height gets us the bottom-right corner of
  407. // the padding edge.
  408. function isElementBottomAfterViewTop(view) {
  409. const element = view.div;
  410. const elementBottom =
  411. element.offsetTop + element.clientTop + element.clientHeight;
  412. return elementBottom > top;
  413. }
  414. function isElementRightAfterViewLeft(view) {
  415. const element = view.div;
  416. const elementRight =
  417. element.offsetLeft + element.clientLeft + element.clientWidth;
  418. return elementRight > left;
  419. }
  420. const visible = [], numViews = views.length;
  421. let firstVisibleElementInd = numViews === 0 ? 0 :
  422. binarySearchFirstItem(views, horizontal ? isElementRightAfterViewLeft :
  423. isElementBottomAfterViewTop);
  424. // Please note the return value of the `binarySearchFirstItem` function when
  425. // no valid element is found (hence the `firstVisibleElementInd` check below).
  426. if (firstVisibleElementInd > 0 && firstVisibleElementInd < numViews &&
  427. !horizontal) {
  428. // In wrapped scrolling (or vertical scrolling with spreads), with some page
  429. // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search
  430. // condition: there can be pages with bottoms above the view top between
  431. // pages with bottoms below. This function detects and corrects that error;
  432. // see it for more comments.
  433. firstVisibleElementInd =
  434. backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top);
  435. }
  436. // lastEdge acts as a cutoff for us to stop looping, because we know all
  437. // subsequent pages will be hidden.
  438. //
  439. // When using wrapped scrolling or vertical scrolling with spreads, we can't
  440. // simply stop the first time we reach a page below the bottom of the view;
  441. // the tops of subsequent pages on the same row could still be visible. In
  442. // horizontal scrolling, we don't have that issue, so we can stop as soon as
  443. // we pass `right`, without needing the code below that handles the -1 case.
  444. let lastEdge = horizontal ? right : -1;
  445. for (let i = firstVisibleElementInd; i < numViews; i++) {
  446. const view = views[i], element = view.div;
  447. const currentWidth = element.offsetLeft + element.clientLeft;
  448. const currentHeight = element.offsetTop + element.clientTop;
  449. const viewWidth = element.clientWidth, viewHeight = element.clientHeight;
  450. const viewRight = currentWidth + viewWidth;
  451. const viewBottom = currentHeight + viewHeight;
  452. if (lastEdge === -1) {
  453. // As commented above, this is only needed in non-horizontal cases.
  454. // Setting lastEdge to the bottom of the first page that is partially
  455. // visible ensures that the next page fully below lastEdge is on the
  456. // next row, which has to be fully hidden along with all subsequent rows.
  457. if (viewBottom >= bottom) {
  458. lastEdge = viewBottom;
  459. }
  460. } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
  461. break;
  462. }
  463. if (viewBottom <= top || currentHeight >= bottom ||
  464. viewRight <= left || currentWidth >= right) {
  465. continue;
  466. }
  467. const hiddenHeight = Math.max(0, top - currentHeight) +
  468. Math.max(0, viewBottom - bottom);
  469. const hiddenWidth = Math.max(0, left - currentWidth) +
  470. Math.max(0, viewRight - right);
  471. const percent = ((viewHeight - hiddenHeight) * (viewWidth - hiddenWidth) *
  472. 100 / viewHeight / viewWidth) | 0;
  473. visible.push({
  474. id: view.id,
  475. x: currentWidth,
  476. y: currentHeight,
  477. view,
  478. percent,
  479. });
  480. }
  481. const first = visible[0], last = visible[visible.length - 1];
  482. if (sortByVisibility) {
  483. visible.sort(function(a, b) {
  484. let pc = a.percent - b.percent;
  485. if (Math.abs(pc) > 0.001) {
  486. return -pc;
  487. }
  488. return a.id - b.id; // ensure stability
  489. });
  490. }
  491. return { first, last, views: visible, };
  492. }
  493. /**
  494. * Event handler to suppress context menu.
  495. */
  496. function noContextMenuHandler(evt) {
  497. evt.preventDefault();
  498. }
  499. function isDataSchema(url) {
  500. let i = 0, ii = url.length;
  501. while (i < ii && url[i].trim() === '') {
  502. i++;
  503. }
  504. return url.substring(i, i + 5).toLowerCase() === 'data:';
  505. }
  506. /**
  507. * Returns the filename or guessed filename from the url (see issue 3455).
  508. * @param {string} url - The original PDF location.
  509. * @param {string} defaultFilename - The value returned if the filename is
  510. * unknown, or the protocol is unsupported.
  511. * @returns {string} Guessed PDF filename.
  512. */
  513. function getPDFFileNameFromURL(url, defaultFilename = 'document.pdf') {
  514. if (typeof url !== 'string') {
  515. return defaultFilename;
  516. }
  517. if (isDataSchema(url)) {
  518. console.warn('getPDFFileNameFromURL: ' +
  519. 'ignoring "data:" URL for performance reasons.');
  520. return defaultFilename;
  521. }
  522. const reURI = /^(?:(?:[^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
  523. // SCHEME HOST 1.PATH 2.QUERY 3.REF
  524. // Pattern to get last matching NAME.pdf
  525. const reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
  526. let splitURI = reURI.exec(url);
  527. let suggestedFilename = reFilename.exec(splitURI[1]) ||
  528. reFilename.exec(splitURI[2]) ||
  529. reFilename.exec(splitURI[3]);
  530. if (suggestedFilename) {
  531. suggestedFilename = suggestedFilename[0];
  532. if (suggestedFilename.includes('%')) {
  533. // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
  534. try {
  535. suggestedFilename =
  536. reFilename.exec(decodeURIComponent(suggestedFilename))[0];
  537. } catch (ex) { // Possible (extremely rare) errors:
  538. // URIError "Malformed URI", e.g. for "%AA.pdf"
  539. // TypeError "null has no properties", e.g. for "%2F.pdf"
  540. }
  541. }
  542. }
  543. return suggestedFilename || defaultFilename;
  544. }
  545. function normalizeWheelEventDelta(evt) {
  546. let delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
  547. let angle = Math.atan2(evt.deltaY, evt.deltaX);
  548. if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
  549. // All that is left-up oriented has to change the sign.
  550. delta = -delta;
  551. }
  552. const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
  553. const MOUSE_DOM_DELTA_LINE_MODE = 1;
  554. const MOUSE_PIXELS_PER_LINE = 30;
  555. const MOUSE_LINES_PER_PAGE = 30;
  556. // Converts delta to per-page units
  557. if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
  558. delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
  559. } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
  560. delta /= MOUSE_LINES_PER_PAGE;
  561. }
  562. return delta;
  563. }
  564. function isValidRotation(angle) {
  565. return Number.isInteger(angle) && angle % 90 === 0;
  566. }
  567. function isValidScrollMode(mode) {
  568. return (Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) &&
  569. mode !== ScrollMode.UNKNOWN);
  570. }
  571. function isValidSpreadMode(mode) {
  572. return (Number.isInteger(mode) && Object.values(SpreadMode).includes(mode) &&
  573. mode !== SpreadMode.UNKNOWN);
  574. }
  575. function isPortraitOrientation(size) {
  576. return size.width <= size.height;
  577. }
  578. const WaitOnType = {
  579. EVENT: 'event',
  580. TIMEOUT: 'timeout',
  581. };
  582. /**
  583. * @typedef {Object} WaitOnEventOrTimeoutParameters
  584. * @property {Object} target - The event target, can for example be:
  585. * `window`, `document`, a DOM element, or an {EventBus} instance.
  586. * @property {string} name - The name of the event.
  587. * @property {number} delay - The delay, in milliseconds, after which the
  588. * timeout occurs (if the event wasn't already dispatched).
  589. */
  590. /**
  591. * Allows waiting for an event or a timeout, whichever occurs first.
  592. * Can be used to ensure that an action always occurs, even when an event
  593. * arrives late or not at all.
  594. *
  595. * @param {WaitOnEventOrTimeoutParameters}
  596. * @returns {Promise} A promise that is resolved with a {WaitOnType} value.
  597. */
  598. function waitOnEventOrTimeout({ target, name, delay = 0, }) {
  599. return new Promise(function(resolve, reject) {
  600. if (typeof target !== 'object' || !(name && typeof name === 'string') ||
  601. !(Number.isInteger(delay) && delay >= 0)) {
  602. throw new Error('waitOnEventOrTimeout - invalid parameters.');
  603. }
  604. function handler(type) {
  605. if (target instanceof EventBus) {
  606. target.off(name, eventHandler);
  607. } else {
  608. target.removeEventListener(name, eventHandler);
  609. }
  610. if (timeout) {
  611. clearTimeout(timeout);
  612. }
  613. resolve(type);
  614. }
  615. const eventHandler = handler.bind(null, WaitOnType.EVENT);
  616. if (target instanceof EventBus) {
  617. target.on(name, eventHandler);
  618. } else {
  619. target.addEventListener(name, eventHandler);
  620. }
  621. const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
  622. let timeout = setTimeout(timeoutHandler, delay);
  623. });
  624. }
  625. /**
  626. * Promise that is resolved when DOM window becomes visible.
  627. */
  628. let animationStarted = new Promise(function (resolve) {
  629. if ((typeof PDFJSDev !== 'undefined' && PDFJSDev.test('LIB')) &&
  630. typeof window === 'undefined') {
  631. // Prevent "ReferenceError: window is not defined" errors when running the
  632. // unit-tests in Node.js/Travis.
  633. setTimeout(resolve, 20);
  634. return;
  635. }
  636. window.requestAnimationFrame(resolve);
  637. });
  638. /**
  639. * Simple event bus for an application. Listeners are attached using the
  640. * `on` and `off` methods. To raise an event, the `dispatch` method shall be
  641. * used.
  642. */
  643. class EventBus {
  644. constructor({ dispatchToDOM = false, } = {}) {
  645. this._listeners = Object.create(null);
  646. this._dispatchToDOM = dispatchToDOM === true;
  647. }
  648. on(eventName, listener) {
  649. let eventListeners = this._listeners[eventName];
  650. if (!eventListeners) {
  651. eventListeners = [];
  652. this._listeners[eventName] = eventListeners;
  653. }
  654. eventListeners.push(listener);
  655. }
  656. off(eventName, listener) {
  657. let eventListeners = this._listeners[eventName];
  658. let i;
  659. if (!eventListeners || ((i = eventListeners.indexOf(listener)) < 0)) {
  660. return;
  661. }
  662. eventListeners.splice(i, 1);
  663. }
  664. dispatch(eventName) {
  665. let eventListeners = this._listeners[eventName];
  666. if (!eventListeners || eventListeners.length === 0) {
  667. if (this._dispatchToDOM) {
  668. const args = Array.prototype.slice.call(arguments, 1);
  669. this._dispatchDOMEvent(eventName, args);
  670. }
  671. return;
  672. }
  673. // Passing all arguments after the eventName to the listeners.
  674. const args = Array.prototype.slice.call(arguments, 1);
  675. // Making copy of the listeners array in case if it will be modified
  676. // during dispatch.
  677. eventListeners.slice(0).forEach(function (listener) {
  678. listener.apply(null, args);
  679. });
  680. if (this._dispatchToDOM) {
  681. this._dispatchDOMEvent(eventName, args);
  682. }
  683. }
  684. /**
  685. * @private
  686. */
  687. _dispatchDOMEvent(eventName, args = null) {
  688. const details = Object.create(null);
  689. if (args && args.length > 0) {
  690. const obj = args[0];
  691. for (let key in obj) {
  692. const value = obj[key];
  693. if (key === 'source') {
  694. if (value === window || value === document) {
  695. return; // No need to re-dispatch (already) global events.
  696. }
  697. continue; // Ignore the `source` property.
  698. }
  699. details[key] = value;
  700. }
  701. }
  702. const event = document.createEvent('CustomEvent');
  703. event.initCustomEvent(eventName, true, true, details);
  704. document.dispatchEvent(event);
  705. }
  706. }
  707. let globalEventBus = null;
  708. function getGlobalEventBus(dispatchToDOM = false) {
  709. if (!globalEventBus) {
  710. globalEventBus = new EventBus({ dispatchToDOM, });
  711. }
  712. return globalEventBus;
  713. }
  714. function clamp(v, min, max) {
  715. return Math.min(Math.max(v, min), max);
  716. }
  717. class ProgressBar {
  718. constructor(id, { height, width, units, } = {}) {
  719. this.visible = true;
  720. // Fetch the sub-elements for later.
  721. this.div = document.querySelector(id + ' .progress');
  722. // Get the loading bar element, so it can be resized to fit the viewer.
  723. this.bar = this.div.parentNode;
  724. // Get options, with sensible defaults.
  725. this.height = height || 100;
  726. this.width = width || 100;
  727. this.units = units || '%';
  728. // Initialize heights.
  729. this.div.style.height = this.height + this.units;
  730. this.percent = 0;
  731. }
  732. _updateBar() {
  733. if (this._indeterminate) {
  734. this.div.classList.add('indeterminate');
  735. this.div.style.width = this.width + this.units;
  736. return;
  737. }
  738. this.div.classList.remove('indeterminate');
  739. let progressSize = this.width * this._percent / 100;
  740. this.div.style.width = progressSize + this.units;
  741. }
  742. get percent() {
  743. return this._percent;
  744. }
  745. set percent(val) {
  746. this._indeterminate = isNaN(val);
  747. this._percent = clamp(val, 0, 100);
  748. this._updateBar();
  749. }
  750. setWidth(viewer) {
  751. if (!viewer) {
  752. return;
  753. }
  754. let container = viewer.parentNode;
  755. let scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
  756. if (scrollbarWidth > 0) {
  757. this.bar.setAttribute('style', 'width: calc(100% - ' +
  758. scrollbarWidth + 'px);');
  759. }
  760. }
  761. hide() {
  762. if (!this.visible) {
  763. return;
  764. }
  765. this.visible = false;
  766. this.bar.classList.add('hidden');
  767. document.body.classList.remove('loadingInProgress');
  768. }
  769. show() {
  770. if (this.visible) {
  771. return;
  772. }
  773. this.visible = true;
  774. document.body.classList.add('loadingInProgress');
  775. this.bar.classList.remove('hidden');
  776. }
  777. }
  778. /**
  779. * Moves all elements of an array that satisfy condition to the end of the
  780. * array, preserving the order of the rest.
  781. */
  782. function moveToEndOfArray(arr, condition) {
  783. const moved = [], len = arr.length;
  784. let write = 0;
  785. for (let read = 0; read < len; ++read) {
  786. if (condition(arr[read])) {
  787. moved.push(arr[read]);
  788. } else {
  789. arr[write] = arr[read];
  790. ++write;
  791. }
  792. }
  793. for (let read = 0; write < len; ++read, ++write) {
  794. arr[write] = moved[read];
  795. }
  796. }
  797. export {
  798. CSS_UNITS,
  799. DEFAULT_SCALE_VALUE,
  800. DEFAULT_SCALE,
  801. MIN_SCALE,
  802. MAX_SCALE,
  803. UNKNOWN_SCALE,
  804. MAX_AUTO_SCALE,
  805. SCROLLBAR_PADDING,
  806. VERTICAL_PADDING,
  807. isValidRotation,
  808. isValidScrollMode,
  809. isValidSpreadMode,
  810. isPortraitOrientation,
  811. PresentationModeState,
  812. RendererType,
  813. TextLayerMode,
  814. ScrollMode,
  815. SpreadMode,
  816. NullL10n,
  817. EventBus,
  818. getGlobalEventBus,
  819. ProgressBar,
  820. getPDFFileNameFromURL,
  821. noContextMenuHandler,
  822. parseQueryString,
  823. backtrackBeforeAllVisibleElements, // only exported for testing
  824. getVisibleElements,
  825. roundToDivide,
  826. getPageSizeInches,
  827. approximateFraction,
  828. getOutputScale,
  829. scrollIntoView,
  830. watchScroll,
  831. binarySearchFirstItem,
  832. normalizeWheelEventDelta,
  833. animationStarted,
  834. WaitOnType,
  835. waitOnEventOrTimeout,
  836. moveToEndOfArray,
  837. };