Path: blob/master/source/_static/ViewerJS/ui_utils.js
1237 views
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */1/* Copyright 2012 Mozilla Foundation2*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 at6*7* http://www.apache.org/licenses/LICENSE-2.08*9* Unless required by applicable law or agreed to in writing, software10* 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 and13* limitations under the License.14*/1516'use strict';1718var CSS_UNITS = 96.0 / 72.0;19var DEFAULT_SCALE = 'auto';20var UNKNOWN_SCALE = 0;21var MAX_AUTO_SCALE = 1.25;22var SCROLLBAR_PADDING = 40;23var VERTICAL_PADDING = 5;2425// optimised CSS custom property getter/setter26var CustomStyle = (function CustomStyleClosure() {2728// As noted on: http://www.zachstronaut.com/posts/2009/02/17/29// animate-css-transforms-firefox-webkit.html30// in some versions of IE9 it is critical that ms appear in this list31// before Moz32var prefixes = ['ms', 'Moz', 'Webkit', 'O'];33var _cache = {};3435function CustomStyle() {}3637CustomStyle.getProp = function get(propName, element) {38// check cache only when no element is given39if (arguments.length === 1 && typeof _cache[propName] === 'string') {40return _cache[propName];41}4243element = element || document.documentElement;44var style = element.style, prefixed, uPropName;4546// test standard property first47if (typeof style[propName] === 'string') {48return (_cache[propName] = propName);49}5051// capitalize52uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);5354// test vendor specific properties55for (var i = 0, l = prefixes.length; i < l; i++) {56prefixed = prefixes[i] + uPropName;57if (typeof style[prefixed] === 'string') {58return (_cache[propName] = prefixed);59}60}6162//if all fails then set to undefined63return (_cache[propName] = 'undefined');64};6566CustomStyle.setProp = function set(propName, element, str) {67var prop = this.getProp(propName);68if (prop !== 'undefined') {69element.style[prop] = str;70}71};7273return CustomStyle;74})();7576function getFileName(url) {77var anchor = url.indexOf('#');78var query = url.indexOf('?');79var end = Math.min(80anchor > 0 ? anchor : url.length,81query > 0 ? query : url.length);82return url.substring(url.lastIndexOf('/', end) + 1, end);83}8485/**86* Returns scale factor for the canvas. It makes sense for the HiDPI displays.87* @return {Object} The object with horizontal (sx) and vertical (sy)88scales. The scaled property is set to false if scaling is89not required, true otherwise.90*/91function getOutputScale(ctx) {92var devicePixelRatio = window.devicePixelRatio || 1;93var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||94ctx.mozBackingStorePixelRatio ||95ctx.msBackingStorePixelRatio ||96ctx.oBackingStorePixelRatio ||97ctx.backingStorePixelRatio || 1;98var pixelRatio = devicePixelRatio / backingStoreRatio;99return {100sx: pixelRatio,101sy: pixelRatio,102scaled: pixelRatio !== 1103};104}105106/**107* Scrolls specified element into view of its parent.108* element {Object} The element to be visible.109* spot {Object} An object with optional top and left properties,110* specifying the offset from the top left edge.111*/112function scrollIntoView(element, spot) {113// Assuming offsetParent is available (it's not available when viewer is in114// hidden iframe or object). We have to scroll: if the offsetParent is not set115// producing the error. See also animationStartedClosure.116var parent = element.offsetParent;117var offsetY = element.offsetTop + element.clientTop;118var offsetX = element.offsetLeft + element.clientLeft;119if (!parent) {120console.error('offsetParent is not set -- cannot scroll');121return;122}123while (parent.clientHeight === parent.scrollHeight) {124if (parent.dataset._scaleY) {125offsetY /= parent.dataset._scaleY;126offsetX /= parent.dataset._scaleX;127}128offsetY += parent.offsetTop;129offsetX += parent.offsetLeft;130parent = parent.offsetParent;131if (!parent) {132return; // no need to scroll133}134}135if (spot) {136if (spot.top !== undefined) {137offsetY += spot.top;138}139if (spot.left !== undefined) {140offsetX += spot.left;141parent.scrollLeft = offsetX;142}143}144parent.scrollTop = offsetY;145}146147/**148* Helper function to start monitoring the scroll event and converting them into149* PDF.js friendly one: with scroll debounce and scroll direction.150*/151function watchScroll(viewAreaElement, callback) {152var debounceScroll = function debounceScroll(evt) {153if (rAF) {154return;155}156// schedule an invocation of scroll for next animation frame.157rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {158rAF = null;159160var currentY = viewAreaElement.scrollTop;161var lastY = state.lastY;162if (currentY !== lastY) {163state.down = currentY > lastY;164}165state.lastY = currentY;166callback(state);167});168};169170var state = {171down: true,172lastY: viewAreaElement.scrollTop,173_eventHandler: debounceScroll174};175176var rAF = null;177viewAreaElement.addEventListener('scroll', debounceScroll, true);178return state;179}180181/**182* Use binary search to find the index of the first item in a given array which183* passes a given condition. The items are expected to be sorted in the sense184* that if the condition is true for one item in the array, then it is also true185* for all following items.186*187* @returns {Number} Index of the first array element to pass the test,188* or |items.length| if no such element exists.189*/190function binarySearchFirstItem(items, condition) {191var minIndex = 0;192var maxIndex = items.length - 1;193194if (items.length === 0 || !condition(items[maxIndex])) {195return items.length;196}197if (condition(items[minIndex])) {198return minIndex;199}200201while (minIndex < maxIndex) {202var currentIndex = (minIndex + maxIndex) >> 1;203var currentItem = items[currentIndex];204if (condition(currentItem)) {205maxIndex = currentIndex;206} else {207minIndex = currentIndex + 1;208}209}210return minIndex; /* === maxIndex */211}212213/**214* Generic helper to find out what elements are visible within a scroll pane.215*/216function getVisibleElements(scrollEl, views, sortByVisibility) {217var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;218var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;219220function isElementBottomBelowViewTop(view) {221var element = view.div;222var elementBottom =223element.offsetTop + element.clientTop + element.clientHeight;224return elementBottom > top;225}226227var visible = [], view, element;228var currentHeight, viewHeight, hiddenHeight, percentHeight;229var currentWidth, viewWidth;230var firstVisibleElementInd = (views.length === 0) ? 0 :231binarySearchFirstItem(views, isElementBottomBelowViewTop);232233for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) {234view = views[i];235element = view.div;236currentHeight = element.offsetTop + element.clientTop;237viewHeight = element.clientHeight;238239if (currentHeight > bottom) {240break;241}242243currentWidth = element.offsetLeft + element.clientLeft;244viewWidth = element.clientWidth;245if (currentWidth + viewWidth < left || currentWidth > right) {246continue;247}248hiddenHeight = Math.max(0, top - currentHeight) +249Math.max(0, currentHeight + viewHeight - bottom);250percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;251252visible.push({253id: view.id,254x: currentWidth,255y: currentHeight,256view: view,257percent: percentHeight258});259}260261var first = visible[0];262var last = visible[visible.length - 1];263264if (sortByVisibility) {265visible.sort(function(a, b) {266var pc = a.percent - b.percent;267if (Math.abs(pc) > 0.001) {268return -pc;269}270return a.id - b.id; // ensure stability271});272}273return {first: first, last: last, views: visible};274}275276/**277* Event handler to suppress context menu.278*/279function noContextMenuHandler(e) {280e.preventDefault();281}282283/**284* Returns the filename or guessed filename from the url (see issue 3455).285* url {String} The original PDF location.286* @return {String} Guessed PDF file name.287*/288function getPDFFileNameFromURL(url) {289var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;290// SCHEME HOST 1.PATH 2.QUERY 3.REF291// Pattern to get last matching NAME.pdf292var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;293var splitURI = reURI.exec(url);294var suggestedFilename = reFilename.exec(splitURI[1]) ||295reFilename.exec(splitURI[2]) ||296reFilename.exec(splitURI[3]);297if (suggestedFilename) {298suggestedFilename = suggestedFilename[0];299if (suggestedFilename.indexOf('%') !== -1) {300// URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf301try {302suggestedFilename =303reFilename.exec(decodeURIComponent(suggestedFilename))[0];304} catch(e) { // Possible (extremely rare) errors:305// URIError "Malformed URI", e.g. for "%AA.pdf"306// TypeError "null has no properties", e.g. for "%2F.pdf"307}308}309}310return suggestedFilename || 'document.pdf';311}312313var ProgressBar = (function ProgressBarClosure() {314315function clamp(v, min, max) {316return Math.min(Math.max(v, min), max);317}318319function ProgressBar(id, opts) {320this.visible = true;321322// Fetch the sub-elements for later.323this.div = document.querySelector(id + ' .progress');324325// Get the loading bar element, so it can be resized to fit the viewer.326this.bar = this.div.parentNode;327328// Get options, with sensible defaults.329this.height = opts.height || 100;330this.width = opts.width || 100;331this.units = opts.units || '%';332333// Initialize heights.334this.div.style.height = this.height + this.units;335this.percent = 0;336}337338ProgressBar.prototype = {339340updateBar: function ProgressBar_updateBar() {341if (this._indeterminate) {342this.div.classList.add('indeterminate');343this.div.style.width = this.width + this.units;344return;345}346347this.div.classList.remove('indeterminate');348var progressSize = this.width * this._percent / 100;349this.div.style.width = progressSize + this.units;350},351352get percent() {353return this._percent;354},355356set percent(val) {357this._indeterminate = isNaN(val);358this._percent = clamp(val, 0, 100);359this.updateBar();360},361362setWidth: function ProgressBar_setWidth(viewer) {363if (viewer) {364var container = viewer.parentNode;365var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;366if (scrollbarWidth > 0) {367this.bar.setAttribute('style', 'width: calc(100% - ' +368scrollbarWidth + 'px);');369}370}371},372373hide: function ProgressBar_hide() {374if (!this.visible) {375return;376}377this.visible = false;378this.bar.classList.add('hidden');379document.body.classList.remove('loadingInProgress');380},381382show: function ProgressBar_show() {383if (this.visible) {384return;385}386this.visible = true;387document.body.classList.add('loadingInProgress');388this.bar.classList.remove('hidden');389}390};391392return ProgressBar;393})();394395396