EGroupware /
egroupware
| 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
||
| 2 | /* Copyright 2012 Mozilla Foundation |
||
| 3 | * |
||
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||
| 5 | * you may not use this file except in compliance with the License. |
||
| 6 | * You may obtain a copy of the License at |
||
| 7 | * |
||
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
| 9 | * |
||
| 10 | * Unless required by applicable law or agreed to in writing, software |
||
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
||
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
| 13 | * See the License for the specific language governing permissions and |
||
| 14 | * limitations under the License. |
||
| 15 | */ |
||
| 16 | |||
| 17 | 'use strict'; |
||
| 18 | |||
| 19 | var CSS_UNITS = 96.0 / 72.0; |
||
| 20 | var DEFAULT_SCALE = 'auto'; |
||
| 21 | var UNKNOWN_SCALE = 0; |
||
| 22 | var MAX_AUTO_SCALE = 1.25; |
||
| 23 | var SCROLLBAR_PADDING = 40; |
||
| 24 | var VERTICAL_PADDING = 5; |
||
| 25 | |||
| 26 | // optimised CSS custom property getter/setter |
||
| 27 | var CustomStyle = (function CustomStyleClosure() { |
||
| 28 | |||
| 29 | // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ |
||
| 30 | // animate-css-transforms-firefox-webkit.html |
||
| 31 | // in some versions of IE9 it is critical that ms appear in this list |
||
| 32 | // before Moz |
||
| 33 | var prefixes = ['ms', 'Moz', 'Webkit', 'O']; |
||
| 34 | var _cache = {}; |
||
| 35 | |||
| 36 | function CustomStyle() {} |
||
| 37 | |||
| 38 | CustomStyle.getProp = function get(propName, element) { |
||
| 39 | // check cache only when no element is given |
||
| 40 | if (arguments.length === 1 && typeof _cache[propName] === 'string') { |
||
| 41 | return _cache[propName]; |
||
| 42 | } |
||
| 43 | |||
| 44 | element = element || document.documentElement; |
||
| 45 | var style = element.style, prefixed, uPropName; |
||
| 46 | |||
| 47 | // test standard property first |
||
| 48 | if (typeof style[propName] === 'string') { |
||
| 49 | return (_cache[propName] = propName); |
||
| 50 | } |
||
| 51 | |||
| 52 | // capitalize |
||
| 53 | uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); |
||
| 54 | |||
| 55 | // test vendor specific properties |
||
| 56 | for (var i = 0, l = prefixes.length; i < l; i++) { |
||
| 57 | prefixed = prefixes[i] + uPropName; |
||
| 58 | if (typeof style[prefixed] === 'string') { |
||
| 59 | return (_cache[propName] = prefixed); |
||
| 60 | } |
||
| 61 | } |
||
| 62 | |||
| 63 | //if all fails then set to undefined |
||
| 64 | return (_cache[propName] = 'undefined'); |
||
| 65 | }; |
||
| 66 | |||
| 67 | CustomStyle.setProp = function set(propName, element, str) { |
||
| 68 | var prop = this.getProp(propName); |
||
| 69 | if (prop !== 'undefined') { |
||
| 70 | element.style[prop] = str; |
||
| 71 | } |
||
| 72 | }; |
||
| 73 | |||
| 74 | return CustomStyle; |
||
| 75 | })(); |
||
| 76 | |||
| 77 | function getFileName(url) { |
||
| 78 | var anchor = url.indexOf('#'); |
||
| 79 | var query = url.indexOf('?'); |
||
| 80 | var end = Math.min( |
||
| 81 | anchor > 0 ? anchor : url.length, |
||
| 82 | query > 0 ? query : url.length); |
||
| 83 | return url.substring(url.lastIndexOf('/', end) + 1, end); |
||
| 84 | } |
||
| 85 | |||
| 86 | /** |
||
| 87 | * Returns scale factor for the canvas. It makes sense for the HiDPI displays. |
||
| 88 | * @return {Object} The object with horizontal (sx) and vertical (sy) |
||
| 89 | scales. The scaled property is set to false if scaling is |
||
| 90 | not required, true otherwise. |
||
| 91 | */ |
||
| 92 | function getOutputScale(ctx) { |
||
| 93 | var devicePixelRatio = window.devicePixelRatio || 1; |
||
| 94 | var backingStoreRatio = ctx.webkitBackingStorePixelRatio || |
||
| 95 | ctx.mozBackingStorePixelRatio || |
||
| 96 | ctx.msBackingStorePixelRatio || |
||
| 97 | ctx.oBackingStorePixelRatio || |
||
| 98 | ctx.backingStorePixelRatio || 1; |
||
| 99 | var pixelRatio = devicePixelRatio / backingStoreRatio; |
||
| 100 | return { |
||
| 101 | sx: pixelRatio, |
||
| 102 | sy: pixelRatio, |
||
| 103 | scaled: pixelRatio !== 1 |
||
| 104 | }; |
||
| 105 | } |
||
| 106 | |||
| 107 | /** |
||
| 108 | * Scrolls specified element into view of its parent. |
||
| 109 | * element {Object} The element to be visible. |
||
| 110 | * spot {Object} An object with optional top and left properties, |
||
| 111 | * specifying the offset from the top left edge. |
||
| 112 | */ |
||
| 113 | function scrollIntoView(element, spot) { |
||
| 114 | // Assuming offsetParent is available (it's not available when viewer is in |
||
| 115 | // hidden iframe or object). We have to scroll: if the offsetParent is not set |
||
| 116 | // producing the error. See also animationStartedClosure. |
||
| 117 | var parent = element.offsetParent; |
||
| 118 | var offsetY = element.offsetTop + element.clientTop; |
||
| 119 | var offsetX = element.offsetLeft + element.clientLeft; |
||
| 120 | if (!parent) { |
||
| 121 | console.error('offsetParent is not set -- cannot scroll'); |
||
| 122 | return; |
||
| 123 | } |
||
| 124 | while (parent.clientHeight === parent.scrollHeight) { |
||
| 125 | if (parent.dataset._scaleY) { |
||
| 126 | offsetY /= parent.dataset._scaleY; |
||
| 127 | offsetX /= parent.dataset._scaleX; |
||
| 128 | } |
||
| 129 | offsetY += parent.offsetTop; |
||
| 130 | offsetX += parent.offsetLeft; |
||
| 131 | parent = parent.offsetParent; |
||
| 132 | if (!parent) { |
||
| 133 | return; // no need to scroll |
||
| 134 | } |
||
| 135 | } |
||
| 136 | if (spot) { |
||
| 137 | if (spot.top !== undefined) { |
||
| 138 | offsetY += spot.top; |
||
| 139 | } |
||
| 140 | if (spot.left !== undefined) { |
||
| 141 | offsetX += spot.left; |
||
| 142 | parent.scrollLeft = offsetX; |
||
| 143 | } |
||
| 144 | } |
||
| 145 | parent.scrollTop = offsetY; |
||
| 146 | } |
||
| 147 | |||
| 148 | /** |
||
| 149 | * Helper function to start monitoring the scroll event and converting them into |
||
| 150 | * PDF.js friendly one: with scroll debounce and scroll direction. |
||
| 151 | */ |
||
| 152 | function watchScroll(viewAreaElement, callback) { |
||
| 153 | var debounceScroll = function debounceScroll(evt) { |
||
| 154 | if (rAF) { |
||
| 155 | return; |
||
| 156 | } |
||
| 157 | // schedule an invocation of scroll for next animation frame. |
||
| 158 | rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { |
||
| 159 | rAF = null; |
||
| 160 | |||
| 161 | var currentY = viewAreaElement.scrollTop; |
||
| 162 | var lastY = state.lastY; |
||
| 163 | if (currentY !== lastY) { |
||
| 164 | state.down = currentY > lastY; |
||
| 165 | } |
||
| 166 | state.lastY = currentY; |
||
| 167 | callback(state); |
||
| 168 | }); |
||
| 169 | }; |
||
| 170 | |||
| 171 | var state = { |
||
| 172 | down: true, |
||
| 173 | lastY: viewAreaElement.scrollTop, |
||
| 174 | _eventHandler: debounceScroll |
||
| 175 | }; |
||
| 176 | |||
| 177 | var rAF = null; |
||
| 178 | viewAreaElement.addEventListener('scroll', debounceScroll, true); |
||
| 179 | return state; |
||
| 180 | } |
||
| 181 | |||
| 182 | /** |
||
| 183 | * Use binary search to find the index of the first item in a given array which |
||
| 184 | * passes a given condition. The items are expected to be sorted in the sense |
||
| 185 | * that if the condition is true for one item in the array, then it is also true |
||
| 186 | * for all following items. |
||
| 187 | * |
||
| 188 | * @returns {Number} Index of the first array element to pass the test, |
||
| 189 | * or |items.length| if no such element exists. |
||
| 190 | */ |
||
| 191 | function binarySearchFirstItem(items, condition) { |
||
| 192 | var minIndex = 0; |
||
| 193 | var maxIndex = items.length - 1; |
||
| 194 | |||
| 195 | if (items.length === 0 || !condition(items[maxIndex])) { |
||
| 196 | return items.length; |
||
| 197 | } |
||
| 198 | if (condition(items[minIndex])) { |
||
| 199 | return minIndex; |
||
| 200 | } |
||
| 201 | |||
| 202 | while (minIndex < maxIndex) { |
||
| 203 | var currentIndex = (minIndex + maxIndex) >> 1; |
||
| 204 | var currentItem = items[currentIndex]; |
||
| 205 | if (condition(currentItem)) { |
||
| 206 | maxIndex = currentIndex; |
||
| 207 | } else { |
||
| 208 | minIndex = currentIndex + 1; |
||
| 209 | } |
||
| 210 | } |
||
| 211 | return minIndex; /* === maxIndex */ |
||
| 212 | } |
||
| 213 | |||
| 214 | /** |
||
| 215 | * Generic helper to find out what elements are visible within a scroll pane. |
||
| 216 | */ |
||
| 217 | function getVisibleElements(scrollEl, views, sortByVisibility) { |
||
| 218 | var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; |
||
| 219 | var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; |
||
| 220 | |||
| 221 | function isElementBottomBelowViewTop(view) { |
||
| 222 | var element = view.div; |
||
| 223 | var elementBottom = |
||
| 224 | element.offsetTop + element.clientTop + element.clientHeight; |
||
| 225 | return elementBottom > top; |
||
| 226 | } |
||
| 227 | |||
| 228 | var visible = [], view, element; |
||
| 229 | var currentHeight, viewHeight, hiddenHeight, percentHeight; |
||
| 230 | var currentWidth, viewWidth; |
||
| 231 | var firstVisibleElementInd = (views.length === 0) ? 0 : |
||
| 232 | binarySearchFirstItem(views, isElementBottomBelowViewTop); |
||
| 233 | |||
| 234 | for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) { |
||
| 235 | view = views[i]; |
||
| 236 | element = view.div; |
||
| 237 | currentHeight = element.offsetTop + element.clientTop; |
||
| 238 | viewHeight = element.clientHeight; |
||
| 239 | |||
| 240 | if (currentHeight > bottom) { |
||
| 241 | break; |
||
| 242 | } |
||
| 243 | |||
| 244 | currentWidth = element.offsetLeft + element.clientLeft; |
||
| 245 | viewWidth = element.clientWidth; |
||
| 246 | if (currentWidth + viewWidth < left || currentWidth > right) { |
||
| 247 | continue; |
||
| 248 | } |
||
| 249 | hiddenHeight = Math.max(0, top - currentHeight) + |
||
| 250 | Math.max(0, currentHeight + viewHeight - bottom); |
||
| 251 | percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; |
||
| 252 | |||
| 253 | visible.push({ |
||
| 254 | id: view.id, |
||
| 255 | x: currentWidth, |
||
| 256 | y: currentHeight, |
||
| 257 | view: view, |
||
| 258 | percent: percentHeight |
||
| 259 | }); |
||
| 260 | } |
||
| 261 | |||
| 262 | var first = visible[0]; |
||
| 263 | var last = visible[visible.length - 1]; |
||
| 264 | |||
| 265 | if (sortByVisibility) { |
||
| 266 | visible.sort(function(a, b) { |
||
| 267 | var pc = a.percent - b.percent; |
||
| 268 | if (Math.abs(pc) > 0.001) { |
||
| 269 | return -pc; |
||
| 270 | } |
||
| 271 | return a.id - b.id; // ensure stability |
||
| 272 | }); |
||
| 273 | } |
||
| 274 | return {first: first, last: last, views: visible}; |
||
| 275 | } |
||
| 276 | |||
| 277 | /** |
||
| 278 | * Event handler to suppress context menu. |
||
| 279 | */ |
||
| 280 | function noContextMenuHandler(e) { |
||
| 281 | e.preventDefault(); |
||
| 282 | } |
||
| 283 | |||
| 284 | /** |
||
| 285 | * Returns the filename or guessed filename from the url (see issue 3455). |
||
| 286 | * url {String} The original PDF location. |
||
| 287 | * @return {String} Guessed PDF file name. |
||
| 288 | */ |
||
| 289 | function getPDFFileNameFromURL(url) { |
||
| 290 | var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; |
||
| 291 | // SCHEME HOST 1.PATH 2.QUERY 3.REF |
||
| 292 | // Pattern to get last matching NAME.pdf |
||
| 293 | var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; |
||
| 294 | var splitURI = reURI.exec(url); |
||
| 295 | var suggestedFilename = reFilename.exec(splitURI[1]) || |
||
| 296 | reFilename.exec(splitURI[2]) || |
||
| 297 | reFilename.exec(splitURI[3]); |
||
| 298 | if (suggestedFilename) { |
||
| 299 | suggestedFilename = suggestedFilename[0]; |
||
| 300 | if (suggestedFilename.indexOf('%') !== -1) { |
||
| 301 | // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf |
||
| 302 | try { |
||
| 303 | suggestedFilename = |
||
| 304 | reFilename.exec(decodeURIComponent(suggestedFilename))[0]; |
||
| 305 | } catch(e) { // Possible (extremely rare) errors: |
||
|
0 ignored issues
–
show
Coding Style
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
| 306 | // URIError "Malformed URI", e.g. for "%AA.pdf" |
||
| 307 | // TypeError "null has no properties", e.g. for "%2F.pdf" |
||
| 308 | } |
||
| 309 | } |
||
| 310 | } |
||
| 311 | return suggestedFilename || 'document.pdf'; |
||
| 312 | } |
||
| 313 | |||
| 314 | var ProgressBar = (function ProgressBarClosure() { |
||
| 315 | |||
| 316 | function clamp(v, min, max) { |
||
| 317 | return Math.min(Math.max(v, min), max); |
||
| 318 | } |
||
| 319 | |||
| 320 | function ProgressBar(id, opts) { |
||
| 321 | this.visible = true; |
||
| 322 | |||
| 323 | // Fetch the sub-elements for later. |
||
| 324 | this.div = document.querySelector(id + ' .progress'); |
||
| 325 | |||
| 326 | // Get the loading bar element, so it can be resized to fit the viewer. |
||
| 327 | this.bar = this.div.parentNode; |
||
| 328 | |||
| 329 | // Get options, with sensible defaults. |
||
| 330 | this.height = opts.height || 100; |
||
| 331 | this.width = opts.width || 100; |
||
| 332 | this.units = opts.units || '%'; |
||
| 333 | |||
| 334 | // Initialize heights. |
||
| 335 | this.div.style.height = this.height + this.units; |
||
| 336 | this.percent = 0; |
||
| 337 | } |
||
| 338 | |||
| 339 | ProgressBar.prototype = { |
||
| 340 | |||
| 341 | updateBar: function ProgressBar_updateBar() { |
||
| 342 | if (this._indeterminate) { |
||
| 343 | this.div.classList.add('indeterminate'); |
||
| 344 | this.div.style.width = this.width + this.units; |
||
| 345 | return; |
||
| 346 | } |
||
| 347 | |||
| 348 | this.div.classList.remove('indeterminate'); |
||
| 349 | var progressSize = this.width * this._percent / 100; |
||
| 350 | this.div.style.width = progressSize + this.units; |
||
| 351 | }, |
||
| 352 | |||
| 353 | get percent() { |
||
| 354 | return this._percent; |
||
| 355 | }, |
||
| 356 | |||
| 357 | set percent(val) { |
||
| 358 | this._indeterminate = isNaN(val); |
||
| 359 | this._percent = clamp(val, 0, 100); |
||
| 360 | this.updateBar(); |
||
| 361 | }, |
||
| 362 | |||
| 363 | setWidth: function ProgressBar_setWidth(viewer) { |
||
| 364 | if (viewer) { |
||
| 365 | var container = viewer.parentNode; |
||
| 366 | var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; |
||
| 367 | if (scrollbarWidth > 0) { |
||
| 368 | this.bar.setAttribute('style', 'width: calc(100% - ' + |
||
| 369 | scrollbarWidth + 'px);'); |
||
| 370 | } |
||
| 371 | } |
||
| 372 | }, |
||
| 373 | |||
| 374 | hide: function ProgressBar_hide() { |
||
| 375 | if (!this.visible) { |
||
| 376 | return; |
||
| 377 | } |
||
| 378 | this.visible = false; |
||
| 379 | this.bar.classList.add('hidden'); |
||
| 380 | document.body.classList.remove('loadingInProgress'); |
||
| 381 | }, |
||
| 382 | |||
| 383 | show: function ProgressBar_show() { |
||
| 384 | if (this.visible) { |
||
| 385 | return; |
||
| 386 | } |
||
| 387 | this.visible = true; |
||
| 388 | document.body.classList.add('loadingInProgress'); |
||
| 389 | this.bar.classList.remove('hidden'); |
||
| 390 | } |
||
| 391 | }; |
||
| 392 | |||
| 393 | return ProgressBar; |
||
| 394 | })(); |
||
| 395 |