395 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			395 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | 
						|
/* Copyright 2012 Mozilla Foundation
 | 
						|
 *
 | 
						|
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
 * you may not use this file except in compliance with the License.
 | 
						|
 * You may obtain a copy of the License at
 | 
						|
 *
 | 
						|
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 *
 | 
						|
 * Unless required by applicable law or agreed to in writing, software
 | 
						|
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
 * See the License for the specific language governing permissions and
 | 
						|
 * limitations under the License.
 | 
						|
 */
 | 
						|
 | 
						|
'use strict';
 | 
						|
 | 
						|
var CSS_UNITS = 96.0 / 72.0;
 | 
						|
var DEFAULT_SCALE = 'auto';
 | 
						|
var UNKNOWN_SCALE = 0;
 | 
						|
var MAX_AUTO_SCALE = 1.25;
 | 
						|
var SCROLLBAR_PADDING = 40;
 | 
						|
var VERTICAL_PADDING = 5;
 | 
						|
 | 
						|
// optimised CSS custom property getter/setter
 | 
						|
var CustomStyle = (function CustomStyleClosure() {
 | 
						|
 | 
						|
  // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
 | 
						|
  //              animate-css-transforms-firefox-webkit.html
 | 
						|
  // in some versions of IE9 it is critical that ms appear in this list
 | 
						|
  // before Moz
 | 
						|
  var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
 | 
						|
  var _cache = {};
 | 
						|
 | 
						|
  function CustomStyle() {}
 | 
						|
 | 
						|
  CustomStyle.getProp = function get(propName, element) {
 | 
						|
    // check cache only when no element is given
 | 
						|
    if (arguments.length === 1 && typeof _cache[propName] === 'string') {
 | 
						|
      return _cache[propName];
 | 
						|
    }
 | 
						|
 | 
						|
    element = element || document.documentElement;
 | 
						|
    var style = element.style, prefixed, uPropName;
 | 
						|
 | 
						|
    // test standard property first
 | 
						|
    if (typeof style[propName] === 'string') {
 | 
						|
      return (_cache[propName] = propName);
 | 
						|
    }
 | 
						|
 | 
						|
    // capitalize
 | 
						|
    uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
 | 
						|
 | 
						|
    // test vendor specific properties
 | 
						|
    for (var i = 0, l = prefixes.length; i < l; i++) {
 | 
						|
      prefixed = prefixes[i] + uPropName;
 | 
						|
      if (typeof style[prefixed] === 'string') {
 | 
						|
        return (_cache[propName] = prefixed);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    //if all fails then set to undefined
 | 
						|
    return (_cache[propName] = 'undefined');
 | 
						|
  };
 | 
						|
 | 
						|
  CustomStyle.setProp = function set(propName, element, str) {
 | 
						|
    var prop = this.getProp(propName);
 | 
						|
    if (prop !== 'undefined') {
 | 
						|
      element.style[prop] = str;
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  return CustomStyle;
 | 
						|
})();
 | 
						|
 | 
						|
function getFileName(url) {
 | 
						|
  var anchor = url.indexOf('#');
 | 
						|
  var query = url.indexOf('?');
 | 
						|
  var end = Math.min(
 | 
						|
    anchor > 0 ? anchor : url.length,
 | 
						|
    query > 0 ? query : url.length);
 | 
						|
  return url.substring(url.lastIndexOf('/', end) + 1, end);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
 | 
						|
 * @return {Object} The object with horizontal (sx) and vertical (sy)
 | 
						|
                    scales. The scaled property is set to false if scaling is
 | 
						|
                    not required, true otherwise.
 | 
						|
 */
 | 
						|
function getOutputScale(ctx) {
 | 
						|
  var devicePixelRatio = window.devicePixelRatio || 1;
 | 
						|
  var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
 | 
						|
                          ctx.mozBackingStorePixelRatio ||
 | 
						|
                          ctx.msBackingStorePixelRatio ||
 | 
						|
                          ctx.oBackingStorePixelRatio ||
 | 
						|
                          ctx.backingStorePixelRatio || 1;
 | 
						|
  var pixelRatio = devicePixelRatio / backingStoreRatio;
 | 
						|
  return {
 | 
						|
    sx: pixelRatio,
 | 
						|
    sy: pixelRatio,
 | 
						|
    scaled: pixelRatio !== 1
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Scrolls specified element into view of its parent.
 | 
						|
 * element {Object} The element to be visible.
 | 
						|
 * spot {Object} An object with optional top and left properties,
 | 
						|
 *               specifying the offset from the top left edge.
 | 
						|
 */
 | 
						|
function scrollIntoView(element, spot) {
 | 
						|
  // Assuming offsetParent is available (it's not available when viewer is in
 | 
						|
  // hidden iframe or object). We have to scroll: if the offsetParent is not set
 | 
						|
  // producing the error. See also animationStartedClosure.
 | 
						|
  var parent = element.offsetParent;
 | 
						|
  var offsetY = element.offsetTop + element.clientTop;
 | 
						|
  var offsetX = element.offsetLeft + element.clientLeft;
 | 
						|
  if (!parent) {
 | 
						|
    console.error('offsetParent is not set -- cannot scroll');
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  while (parent.clientHeight === parent.scrollHeight) {
 | 
						|
    if (parent.dataset._scaleY) {
 | 
						|
      offsetY /= parent.dataset._scaleY;
 | 
						|
      offsetX /= parent.dataset._scaleX;
 | 
						|
    }
 | 
						|
    offsetY += parent.offsetTop;
 | 
						|
    offsetX += parent.offsetLeft;
 | 
						|
    parent = parent.offsetParent;
 | 
						|
    if (!parent) {
 | 
						|
      return; // no need to scroll
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (spot) {
 | 
						|
    if (spot.top !== undefined) {
 | 
						|
      offsetY += spot.top;
 | 
						|
    }
 | 
						|
    if (spot.left !== undefined) {
 | 
						|
      offsetX += spot.left;
 | 
						|
      parent.scrollLeft = offsetX;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  parent.scrollTop = offsetY;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Helper function to start monitoring the scroll event and converting them into
 | 
						|
 * PDF.js friendly one: with scroll debounce and scroll direction.
 | 
						|
 */
 | 
						|
function watchScroll(viewAreaElement, callback) {
 | 
						|
  var debounceScroll = function debounceScroll(evt) {
 | 
						|
    if (rAF) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // schedule an invocation of scroll for next animation frame.
 | 
						|
    rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
 | 
						|
      rAF = null;
 | 
						|
 | 
						|
      var currentY = viewAreaElement.scrollTop;
 | 
						|
      var lastY = state.lastY;
 | 
						|
      if (currentY !== lastY) {
 | 
						|
        state.down = currentY > lastY;
 | 
						|
      }
 | 
						|
      state.lastY = currentY;
 | 
						|
      callback(state);
 | 
						|
    });
 | 
						|
  };
 | 
						|
 | 
						|
  var state = {
 | 
						|
    down: true,
 | 
						|
    lastY: viewAreaElement.scrollTop,
 | 
						|
    _eventHandler: debounceScroll
 | 
						|
  };
 | 
						|
 | 
						|
  var rAF = null;
 | 
						|
  viewAreaElement.addEventListener('scroll', debounceScroll, true);
 | 
						|
  return state;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Use binary search to find the index of the first item in a given array which
 | 
						|
 * passes a given condition. The items are expected to be sorted in the sense
 | 
						|
 * that if the condition is true for one item in the array, then it is also true
 | 
						|
 * for all following items.
 | 
						|
 *
 | 
						|
 * @returns {Number} Index of the first array element to pass the test,
 | 
						|
 *                   or |items.length| if no such element exists.
 | 
						|
 */
 | 
						|
function binarySearchFirstItem(items, condition) {
 | 
						|
  var minIndex = 0;
 | 
						|
  var maxIndex = items.length - 1;
 | 
						|
 | 
						|
  if (items.length === 0 || !condition(items[maxIndex])) {
 | 
						|
    return items.length;
 | 
						|
  }
 | 
						|
  if (condition(items[minIndex])) {
 | 
						|
    return minIndex;
 | 
						|
  }
 | 
						|
 | 
						|
  while (minIndex < maxIndex) {
 | 
						|
    var currentIndex = (minIndex + maxIndex) >> 1;
 | 
						|
    var currentItem = items[currentIndex];
 | 
						|
    if (condition(currentItem)) {
 | 
						|
      maxIndex = currentIndex;
 | 
						|
    } else {
 | 
						|
      minIndex = currentIndex + 1;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return minIndex; /* === maxIndex */
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Generic helper to find out what elements are visible within a scroll pane.
 | 
						|
 */
 | 
						|
function getVisibleElements(scrollEl, views, sortByVisibility) {
 | 
						|
  var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
 | 
						|
  var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
 | 
						|
 | 
						|
  function isElementBottomBelowViewTop(view) {
 | 
						|
    var element = view.div;
 | 
						|
    var elementBottom =
 | 
						|
      element.offsetTop + element.clientTop + element.clientHeight;
 | 
						|
    return elementBottom > top;
 | 
						|
  }
 | 
						|
 | 
						|
  var visible = [], view, element;
 | 
						|
  var currentHeight, viewHeight, hiddenHeight, percentHeight;
 | 
						|
  var currentWidth, viewWidth;
 | 
						|
  var firstVisibleElementInd = (views.length === 0) ? 0 :
 | 
						|
    binarySearchFirstItem(views, isElementBottomBelowViewTop);
 | 
						|
 | 
						|
  for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) {
 | 
						|
    view = views[i];
 | 
						|
    element = view.div;
 | 
						|
    currentHeight = element.offsetTop + element.clientTop;
 | 
						|
    viewHeight = element.clientHeight;
 | 
						|
 | 
						|
    if (currentHeight > bottom) {
 | 
						|
      break;
 | 
						|
    }
 | 
						|
 | 
						|
    currentWidth = element.offsetLeft + element.clientLeft;
 | 
						|
    viewWidth = element.clientWidth;
 | 
						|
    if (currentWidth + viewWidth < left || currentWidth > right) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    hiddenHeight = Math.max(0, top - currentHeight) +
 | 
						|
      Math.max(0, currentHeight + viewHeight - bottom);
 | 
						|
    percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
 | 
						|
 | 
						|
    visible.push({
 | 
						|
      id: view.id,
 | 
						|
      x: currentWidth,
 | 
						|
      y: currentHeight,
 | 
						|
      view: view,
 | 
						|
      percent: percentHeight
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var first = visible[0];
 | 
						|
  var last = visible[visible.length - 1];
 | 
						|
 | 
						|
  if (sortByVisibility) {
 | 
						|
    visible.sort(function(a, b) {
 | 
						|
      var pc = a.percent - b.percent;
 | 
						|
      if (Math.abs(pc) > 0.001) {
 | 
						|
        return -pc;
 | 
						|
      }
 | 
						|
      return a.id - b.id; // ensure stability
 | 
						|
    });
 | 
						|
  }
 | 
						|
  return {first: first, last: last, views: visible};
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Event handler to suppress context menu.
 | 
						|
 */
 | 
						|
function noContextMenuHandler(e) {
 | 
						|
  e.preventDefault();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns the filename or guessed filename from the url (see issue 3455).
 | 
						|
 * url {String} The original PDF location.
 | 
						|
 * @return {String} Guessed PDF file name.
 | 
						|
 */
 | 
						|
function getPDFFileNameFromURL(url) {
 | 
						|
  var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
 | 
						|
  //            SCHEME      HOST         1.PATH  2.QUERY   3.REF
 | 
						|
  // Pattern to get last matching NAME.pdf
 | 
						|
  var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
 | 
						|
  var splitURI = reURI.exec(url);
 | 
						|
  var suggestedFilename = reFilename.exec(splitURI[1]) ||
 | 
						|
                           reFilename.exec(splitURI[2]) ||
 | 
						|
                           reFilename.exec(splitURI[3]);
 | 
						|
  if (suggestedFilename) {
 | 
						|
    suggestedFilename = suggestedFilename[0];
 | 
						|
    if (suggestedFilename.indexOf('%') !== -1) {
 | 
						|
      // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
 | 
						|
      try {
 | 
						|
        suggestedFilename =
 | 
						|
          reFilename.exec(decodeURIComponent(suggestedFilename))[0];
 | 
						|
      } catch(e) { // Possible (extremely rare) errors:
 | 
						|
        // URIError "Malformed URI", e.g. for "%AA.pdf"
 | 
						|
        // TypeError "null has no properties", e.g. for "%2F.pdf"
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return suggestedFilename || 'document.pdf';
 | 
						|
}
 | 
						|
 | 
						|
var ProgressBar = (function ProgressBarClosure() {
 | 
						|
 | 
						|
  function clamp(v, min, max) {
 | 
						|
    return Math.min(Math.max(v, min), max);
 | 
						|
  }
 | 
						|
 | 
						|
  function ProgressBar(id, opts) {
 | 
						|
    this.visible = true;
 | 
						|
 | 
						|
    // Fetch the sub-elements for later.
 | 
						|
    this.div = document.querySelector(id + ' .progress');
 | 
						|
 | 
						|
    // Get the loading bar element, so it can be resized to fit the viewer.
 | 
						|
    this.bar = this.div.parentNode;
 | 
						|
 | 
						|
    // Get options, with sensible defaults.
 | 
						|
    this.height = opts.height || 100;
 | 
						|
    this.width = opts.width || 100;
 | 
						|
    this.units = opts.units || '%';
 | 
						|
 | 
						|
    // Initialize heights.
 | 
						|
    this.div.style.height = this.height + this.units;
 | 
						|
    this.percent = 0;
 | 
						|
  }
 | 
						|
 | 
						|
  ProgressBar.prototype = {
 | 
						|
 | 
						|
    updateBar: function ProgressBar_updateBar() {
 | 
						|
      if (this._indeterminate) {
 | 
						|
        this.div.classList.add('indeterminate');
 | 
						|
        this.div.style.width = this.width + this.units;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      this.div.classList.remove('indeterminate');
 | 
						|
      var progressSize = this.width * this._percent / 100;
 | 
						|
      this.div.style.width = progressSize + this.units;
 | 
						|
    },
 | 
						|
 | 
						|
    get percent() {
 | 
						|
      return this._percent;
 | 
						|
    },
 | 
						|
 | 
						|
    set percent(val) {
 | 
						|
      this._indeterminate = isNaN(val);
 | 
						|
      this._percent = clamp(val, 0, 100);
 | 
						|
      this.updateBar();
 | 
						|
    },
 | 
						|
 | 
						|
    setWidth: function ProgressBar_setWidth(viewer) {
 | 
						|
      if (viewer) {
 | 
						|
        var container = viewer.parentNode;
 | 
						|
        var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
 | 
						|
        if (scrollbarWidth > 0) {
 | 
						|
          this.bar.setAttribute('style', 'width: calc(100% - ' +
 | 
						|
                                         scrollbarWidth + 'px);');
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    hide: function ProgressBar_hide() {
 | 
						|
      if (!this.visible) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      this.visible = false;
 | 
						|
      this.bar.classList.add('hidden');
 | 
						|
      document.body.classList.remove('loadingInProgress');
 | 
						|
    },
 | 
						|
 | 
						|
    show: function ProgressBar_show() {
 | 
						|
      if (this.visible) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      this.visible = true;
 | 
						|
      document.body.classList.add('loadingInProgress');
 | 
						|
      this.bar.classList.remove('hidden');
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  return ProgressBar;
 | 
						|
})();
 |