mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
make new pdf viewer the default for all users
remove old pdf viewer
This commit is contained in:
parent
ff55e4c5ed
commit
b0a32b1ef8
11 changed files with 16 additions and 1508 deletions
|
@ -23,9 +23,6 @@ for path in [
|
||||||
"#{jsPath}main.js",
|
"#{jsPath}main.js",
|
||||||
"#{jsPath}libs.js",
|
"#{jsPath}libs.js",
|
||||||
"#{jsPath}ace/ace.js",
|
"#{jsPath}ace/ace.js",
|
||||||
"#{jsPath}libs/pdf.js",
|
|
||||||
"#{jsPath}libs/pdf.worker.js",
|
|
||||||
"#{jsPath}libs/compatibility.js",
|
|
||||||
"#{jsPath}libs/pdfjs-1.0.712/pdf.js",
|
"#{jsPath}libs/pdfjs-1.0.712/pdf.js",
|
||||||
"#{jsPath}libs/pdfjs-1.0.712/pdf.worker.js",
|
"#{jsPath}libs/pdfjs-1.0.712/pdf.worker.js",
|
||||||
"#{jsPath}libs/pdfjs-1.0.712/compatibility.js",
|
"#{jsPath}libs/pdfjs-1.0.712/compatibility.js",
|
||||||
|
|
|
@ -89,14 +89,14 @@ block content
|
||||||
window.requirejs = {
|
window.requirejs = {
|
||||||
"paths" : {
|
"paths" : {
|
||||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
|
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
|
||||||
"moment": "libs/moment-2.7.0"
|
"moment": "libs/moment-2.7.0",
|
||||||
|
"libs/pdf": "libs/pdfjs-1.0.712/pdf",
|
||||||
|
"libs/compatibility": "libs/pdfjs-1.0.712/compatibility"
|
||||||
|
|
||||||
},
|
},
|
||||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||||
"waitSeconds": 0,
|
"waitSeconds": 0,
|
||||||
"shim": {
|
"shim": {
|
||||||
"libs/pdfListView/PdfListView": {
|
|
||||||
deps: ["libs/pdf"]
|
|
||||||
},
|
|
||||||
"libs/pdf": {
|
"libs/pdf": {
|
||||||
deps: ["libs/compatibility"]
|
deps: ["libs/compatibility"]
|
||||||
},
|
},
|
||||||
|
@ -117,21 +117,12 @@ block content
|
||||||
|
|
||||||
- locals.suppressDefaultJs = true
|
- locals.suppressDefaultJs = true
|
||||||
|
|
||||||
- // user.featureSwitches = {} // override
|
- var pdfPath = 'libs/pdfjs-1.0.712/pdf.worker.js'
|
||||||
- var usePdfNG = user.featureSwitches && user.featureSwitches.pdfng;
|
|
||||||
- var pdfPath = 'libs/pdf.worker.js';
|
|
||||||
- if (usePdfNG) { pdfPath = 'libs/pdfjs-1.0.712/pdf.worker.js'; }
|
|
||||||
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
|
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
|
||||||
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
|
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
|
||||||
script(type='text/javascript').
|
script(type='text/javascript').
|
||||||
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
|
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
|
||||||
|
|
||||||
// when using pdfng we need a different pdfjs version
|
|
||||||
if usePdfNG
|
|
||||||
script(type='text/javascript').
|
|
||||||
window.requirejs.paths['libs/pdf'] = 'libs/pdfjs-1.0.712/pdf'
|
|
||||||
window.requirejs.paths['libs/compatibility'] = 'libs/pdfjs-1.0.712/compatibility'
|
|
||||||
|
|
||||||
script(
|
script(
|
||||||
data-main=jsPath+"ide.js",
|
data-main=jsPath+"ide.js",
|
||||||
baseurl=jsPath,
|
baseurl=jsPath,
|
||||||
|
|
|
@ -58,20 +58,6 @@ div.full-size.pdf(ng-controller="PdfController")
|
||||||
i.split-screen
|
i.split-screen
|
||||||
|
|
||||||
.pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error")
|
.pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error")
|
||||||
- // user.featureSwitches = {} // override
|
|
||||||
- var usePdfNG = user.featureSwitches && user.featureSwitches.pdfng;
|
|
||||||
if !usePdfNG
|
|
||||||
div(
|
|
||||||
pdfjs
|
|
||||||
ng-if="settings.pdfViewer == 'pdfjs'"
|
|
||||||
pdf-src="pdf.url"
|
|
||||||
key="{{ project_id }}"
|
|
||||||
resize-on="layout:main:resize,layout:pdf:resize"
|
|
||||||
highlights="pdf.highlights"
|
|
||||||
position="pdf.position"
|
|
||||||
dbl-click-callback="syncToCode"
|
|
||||||
)
|
|
||||||
else
|
|
||||||
div(
|
div(
|
||||||
pdfng
|
pdfng
|
||||||
ng-if="settings.pdfViewer == 'pdfjs'"
|
ng-if="settings.pdfViewer == 'pdfjs'"
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
define [
|
define [
|
||||||
"ide/pdf/controllers/PdfController"
|
"ide/pdf/controllers/PdfController"
|
||||||
"ide/pdf/controllers/PdfViewToggleController"
|
"ide/pdf/controllers/PdfViewToggleController"
|
||||||
"ide/pdf/directives/pdfJs"
|
"ide/pdfng/directives/pdfJs"
|
||||||
"ide/pdfng/directives/pdfJs" # alternative incremental viewer
|
|
||||||
], () ->
|
], () ->
|
||||||
class PdfManager
|
class PdfManager
|
||||||
constructor: (@ide, @$scope) ->
|
constructor: (@ide, @$scope) ->
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
define [
|
|
||||||
"base"
|
|
||||||
"libs/pdfListView/PdfListView"
|
|
||||||
"libs/pdfListView/TextLayerBuilder"
|
|
||||||
"libs/pdfListView/AnnotationsLayerBuilder"
|
|
||||||
"libs/pdfListView/HighlightsLayerBuilder"
|
|
||||||
"text!libs/pdfListView/TextLayer.css"
|
|
||||||
"text!libs/pdfListView/AnnotationsLayer.css"
|
|
||||||
"text!libs/pdfListView/HighlightsLayer.css"
|
|
||||||
], (
|
|
||||||
App
|
|
||||||
PDFListView
|
|
||||||
TextLayerBuilder
|
|
||||||
AnnotationsLayerBuilder
|
|
||||||
HighlightsLayerBuilder
|
|
||||||
textLayerCss
|
|
||||||
annotationsLayerCss
|
|
||||||
highlightsLayerCss
|
|
||||||
) ->
|
|
||||||
if PDFJS?
|
|
||||||
PDFJS.workerSrc = window.pdfJsWorkerPath
|
|
||||||
|
|
||||||
style = $("<style/>")
|
|
||||||
style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
|
|
||||||
$("body").append(style)
|
|
||||||
|
|
||||||
App.directive "pdfjs", ["$timeout", "localStorage", ($timeout, localStorage) ->
|
|
||||||
return {
|
|
||||||
scope: {
|
|
||||||
"pdfSrc": "="
|
|
||||||
"highlights": "="
|
|
||||||
"position": "="
|
|
||||||
"dblClickCallback": "="
|
|
||||||
}
|
|
||||||
link: (scope, element, attrs) ->
|
|
||||||
pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
|
|
||||||
textLayerBuilder: TextLayerBuilder
|
|
||||||
annotationsLayerBuilder: AnnotationsLayerBuilder
|
|
||||||
highlightsLayerBuilder: HighlightsLayerBuilder
|
|
||||||
ondblclick: (e) -> onDoubleClick(e)
|
|
||||||
# logLevel: PDFListView.Logger.DEBUG
|
|
||||||
pdfListView.listView.pageWidthOffset = 20
|
|
||||||
pdfListView.listView.pageHeightOffset = 20
|
|
||||||
|
|
||||||
scope.loading = false
|
|
||||||
|
|
||||||
onProgress = (progress) ->
|
|
||||||
scope.$apply () ->
|
|
||||||
scope.progress = Math.floor(progress.loaded/progress.total*100)
|
|
||||||
|
|
||||||
initializedPosition = false
|
|
||||||
initializePosition = () ->
|
|
||||||
return if initializedPosition
|
|
||||||
initializedPosition = true
|
|
||||||
|
|
||||||
if (scale = localStorage("pdf.scale"))?
|
|
||||||
pdfListView.setScaleMode(scale.scaleMode, scale.scale)
|
|
||||||
else
|
|
||||||
pdfListView.setToFitWidth()
|
|
||||||
|
|
||||||
if (position = localStorage("pdf.position.#{attrs.key}"))
|
|
||||||
pdfListView.setPdfPosition(position)
|
|
||||||
|
|
||||||
scope.position = pdfListView.getPdfPosition(true)
|
|
||||||
|
|
||||||
$(window).unload () =>
|
|
||||||
localStorage "pdf.scale", {
|
|
||||||
scaleMode: pdfListView.getScaleMode()
|
|
||||||
scale: pdfListView.getScale()
|
|
||||||
}
|
|
||||||
localStorage "pdf.position.#{attrs.key}", pdfListView.getPdfPosition()
|
|
||||||
|
|
||||||
flashControls = () ->
|
|
||||||
scope.flashControls = true
|
|
||||||
$timeout () ->
|
|
||||||
scope.flashControls = false
|
|
||||||
, 1000
|
|
||||||
|
|
||||||
element.find(".pdfjs-viewer").scroll () ->
|
|
||||||
scope.position = pdfListView.getPdfPosition(true)
|
|
||||||
|
|
||||||
onDoubleClick = (e) ->
|
|
||||||
scope.dblClickCallback?(page: e.page, offset: { top: e.y, left: e.x })
|
|
||||||
|
|
||||||
scope.$watch "pdfSrc", (url) ->
|
|
||||||
if url
|
|
||||||
scope.loading = true
|
|
||||||
scope.progress = 0
|
|
||||||
|
|
||||||
pdfListView
|
|
||||||
.loadPdf(url, onProgress)
|
|
||||||
.then () ->
|
|
||||||
scope.$apply () ->
|
|
||||||
scope.loading = false
|
|
||||||
delete scope.progress
|
|
||||||
initializePosition()
|
|
||||||
flashControls()
|
|
||||||
|
|
||||||
scope.$watch "highlights", (areas) ->
|
|
||||||
return if !areas?
|
|
||||||
highlights = for area in areas or []
|
|
||||||
{
|
|
||||||
page: area.page - 1
|
|
||||||
highlight:
|
|
||||||
left: area.h
|
|
||||||
top: area.v
|
|
||||||
height: area.height
|
|
||||||
width: area.width
|
|
||||||
}
|
|
||||||
|
|
||||||
if highlights.length > 0
|
|
||||||
first = highlights[0]
|
|
||||||
pdfListView.setPdfPosition({
|
|
||||||
page: first.page
|
|
||||||
offset:
|
|
||||||
left: first.highlight.left
|
|
||||||
top: first.highlight.top - 80
|
|
||||||
}, true)
|
|
||||||
|
|
||||||
pdfListView.clearHighlights()
|
|
||||||
pdfListView.setHighlights(highlights, true)
|
|
||||||
|
|
||||||
setTimeout () =>
|
|
||||||
pdfListView.clearHighlights()
|
|
||||||
, 1000
|
|
||||||
|
|
||||||
scope.fitToHeight = () ->
|
|
||||||
pdfListView.setToFitHeight()
|
|
||||||
|
|
||||||
scope.fitToWidth = () ->
|
|
||||||
pdfListView.setToFitWidth()
|
|
||||||
|
|
||||||
scope.zoomIn = () ->
|
|
||||||
scale = pdfListView.getScale()
|
|
||||||
pdfListView.setScale(scale * 1.2)
|
|
||||||
|
|
||||||
scope.zoomOut = () ->
|
|
||||||
scale = pdfListView.getScale()
|
|
||||||
pdfListView.setScale(scale / 1.2)
|
|
||||||
|
|
||||||
if attrs.resizeOn?
|
|
||||||
for event in attrs.resizeOn.split(",")
|
|
||||||
scope.$on event, () ->
|
|
||||||
pdfListView.onResize()
|
|
||||||
|
|
||||||
template: """
|
|
||||||
<div class="pdfjs-viewer"></div>
|
|
||||||
<div class="pdfjs-controls" ng-class="{'flash': flashControls }">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href
|
|
||||||
class="btn btn-info btn-lg"
|
|
||||||
ng-click="fitToWidth()"
|
|
||||||
tooltip="Fit to Width"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
>
|
|
||||||
<i class="fa fa-fw fa-arrows-h"></i>
|
|
||||||
</a>
|
|
||||||
<a href
|
|
||||||
class="btn btn-info btn-lg"
|
|
||||||
ng-click="fitToHeight()"
|
|
||||||
tooltip="Fit to Height"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
>
|
|
||||||
<i class="fa fa-fw fa-arrows-v"></i>
|
|
||||||
</a>
|
|
||||||
<a href
|
|
||||||
class="btn btn-info btn-lg"
|
|
||||||
ng-click="zoomIn()"
|
|
||||||
tooltip="Zoom In"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
>
|
|
||||||
<i class="fa fa-fw fa-search-plus"></i>
|
|
||||||
</a>
|
|
||||||
<a href
|
|
||||||
class="btn btn-info btn-lg"
|
|
||||||
ng-click="zoomOut()"
|
|
||||||
tooltip="Zoom Out"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
>
|
|
||||||
<i class="fa fa-fw fa-search-minus"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-thin" ng-show="loading">
|
|
||||||
<div class="progress-bar" ng-style="{ 'width': progress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
]
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,64 +0,0 @@
|
||||||
(function (name, context, definition) {
|
|
||||||
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
|
||||||
else if (typeof define == 'function' && define.amd) define(definition)
|
|
||||||
else context[name] = definition()
|
|
||||||
})('AnnotationsLayerBuilder', this, function (name, context) {
|
|
||||||
|
|
||||||
function AnnotationsLayerBuilder(pageView, annotationsLayerDiv) {
|
|
||||||
this.annotationsLayerDiv = annotationsLayerDiv;
|
|
||||||
this.pageView = pageView;
|
|
||||||
};
|
|
||||||
|
|
||||||
AnnotationsLayerBuilder.EXTERNAL_LINK_TARGET = "_blank";
|
|
||||||
|
|
||||||
AnnotationsLayerBuilder.prototype = {
|
|
||||||
setAnnotations: function(annotations) {
|
|
||||||
for (var i = 0; i < annotations.length; i++) {
|
|
||||||
var annotation = annotations[i];
|
|
||||||
switch (annotation.subtype) {
|
|
||||||
case 'Link':
|
|
||||||
this.addLink(annotation);
|
|
||||||
break;
|
|
||||||
case 'Text':
|
|
||||||
// TODO
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addLink: function(link) {
|
|
||||||
var element = this.buildLinkElementFromRect(link.rect);
|
|
||||||
this.setLinkTarget(element, link);
|
|
||||||
this.annotationsLayerDiv.appendChild(element);
|
|
||||||
},
|
|
||||||
|
|
||||||
buildLinkElementFromRect: function(rect) {
|
|
||||||
rect = this.pageView.viewport.convertToViewportRectangle(rect);
|
|
||||||
rect = PDFJS.Util.normalizeRect(rect);
|
|
||||||
var element = document.createElement("a");
|
|
||||||
element.style.left = Math.floor(rect[0]) + 'px';
|
|
||||||
element.style.top = Math.floor(rect[1]) + 'px';
|
|
||||||
element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
|
|
||||||
element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
|
|
||||||
setLinkTarget: function(element, link) {
|
|
||||||
if (link.url) {
|
|
||||||
element.href = link.url;
|
|
||||||
element.target = this.EXTERNAL_LINK_TARGET;
|
|
||||||
} else if (link.dest) {
|
|
||||||
element.href = "#" + link.dest;
|
|
||||||
var listView = this.pageView.listView;
|
|
||||||
element.onclick = function(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
listView.navigateTo(link.dest);
|
|
||||||
}
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnnotationsLayerBuilder;
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
(function (name, context, definition) {
|
|
||||||
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
|
||||||
else if (typeof define == 'function' && define.amd) define(definition)
|
|
||||||
else context[name] = definition()
|
|
||||||
})('HighlightsLayerBuilder', this, function (name, context) {
|
|
||||||
|
|
||||||
function HighlightsLayerBuilder(pageView, highlightsLayerDiv) {
|
|
||||||
this.highlightsLayerDiv = highlightsLayerDiv;
|
|
||||||
this.pageView = pageView;
|
|
||||||
this.highlightElements = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
HighlightsLayerBuilder.EXTERNAL_LINK_TARGET = "_blank";
|
|
||||||
|
|
||||||
HighlightsLayerBuilder.prototype = {
|
|
||||||
addHighlight: function(left, top, width, height) {
|
|
||||||
rect = this.pageView.viewport.convertToViewportRectangle([left, top, left + width, top + height]);
|
|
||||||
rect = PDFJS.Util.normalizeRect(rect);
|
|
||||||
var element = document.createElement("div");
|
|
||||||
element.style.left = Math.floor(rect[0]) + 'px';
|
|
||||||
element.style.top = Math.floor(rect[1]) + 'px';
|
|
||||||
element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
|
|
||||||
element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
|
|
||||||
this.highlightElements.push(element);
|
|
||||||
this.highlightsLayerDiv.appendChild(element);
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearHighlights: function() {
|
|
||||||
for (var i = 0; i < this.highlightElements.length; i++) {
|
|
||||||
this.highlightElements[i].remove();
|
|
||||||
}
|
|
||||||
this.highlightElements = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return HighlightsLayerBuilder;
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,972 +0,0 @@
|
||||||
(function (name, context, definition) {
|
|
||||||
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
|
||||||
else if (typeof define == 'function' && define.amd) define(definition)
|
|
||||||
else context[name] = definition()
|
|
||||||
})('PDFListView', this, function (name, context) {
|
|
||||||
|
|
||||||
//#expand __BUNDLE__
|
|
||||||
|
|
||||||
function Logger() {
|
|
||||||
this.logLevel = Logger.INFO;
|
|
||||||
var self = this;
|
|
||||||
if (typeof(console) == "object" && typeof(console.log) == "function") {
|
|
||||||
this.debug = function() {
|
|
||||||
if (self.logLevel <= Logger.DEBUG) {
|
|
||||||
console.log.apply(console, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.info = function() {
|
|
||||||
if (self.logLevel <= Logger.INFO) {
|
|
||||||
console.log.apply(console, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.error = function() {
|
|
||||||
if (self.logLevel <= Logger.ERROR) {
|
|
||||||
console.log.apply(console, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.debug = this.info = this.error = function nop() {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.DEBUG = 0;
|
|
||||||
Logger.INFO = 1;
|
|
||||||
Logger.ERROR = 2;
|
|
||||||
|
|
||||||
var logger = new Logger();
|
|
||||||
|
|
||||||
function _flat(arr) {
|
|
||||||
var res = arr.reduce(function(a, b) {
|
|
||||||
return a.concat(b);
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function failDumper(err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(PDFJS) === "undefined") {
|
|
||||||
logger.error("PDF.js is not yet loaded.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around the raw PDF.JS document.
|
|
||||||
*/
|
|
||||||
function Document(url, password, onProgress) {
|
|
||||||
this.pdfDocument = null;
|
|
||||||
this.pages = null;
|
|
||||||
|
|
||||||
var parameters = {password: password};
|
|
||||||
if (typeof url === 'string') { // URL
|
|
||||||
parameters.url = url;
|
|
||||||
} else if (url && 'byteLength' in url) { // ArrayBuffer
|
|
||||||
parameters.data = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = new PDFJS.LegacyPromise();
|
|
||||||
PDFJS.getDocument(parameters, null, null, onProgress).then(this.loadPages.bind(this), failDumper);
|
|
||||||
}
|
|
||||||
|
|
||||||
Document.prototype.loadPages = function(pdfDocument) {
|
|
||||||
this.pdfDocument = pdfDocument;
|
|
||||||
var pagesCount = this.pagesCount = pdfDocument.numPages;
|
|
||||||
|
|
||||||
var pagePromises = [];
|
|
||||||
for (var i = 1; i <= pagesCount; i++) {
|
|
||||||
pagePromises.push(pdfDocument.getPage(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pageRefMap = pageRefMap = {};
|
|
||||||
var pagesPromise = Promise.all(pagePromises);
|
|
||||||
var doc = this
|
|
||||||
pagesPromise.then(function(promisedPages) {
|
|
||||||
doc.pages = promisedPages.map(function(pdfPage, i) {
|
|
||||||
var pageRef = pdfPage.ref
|
|
||||||
pageRefMap[pageRef.num + ' ' + pageRef.gen] = i;
|
|
||||||
return new Page(pdfPage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var destinationsPromise = pdfDocument.getDestinations();
|
|
||||||
destinationsPromise.then(function(destinations) {
|
|
||||||
doc.destinations = destinations;
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([pagesPromise, destinationsPromise]).then(function() {
|
|
||||||
doc.initialized.resolve();
|
|
||||||
}, failDumper);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the rendering. Multiple ListViews can be bound to a RenderController.
|
|
||||||
* In that case, the RenderController figures out what's page has the highest
|
|
||||||
* priority to render
|
|
||||||
*/
|
|
||||||
function RenderController() {
|
|
||||||
this.listViews = [];
|
|
||||||
this.renderList = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderController.prototype = {
|
|
||||||
addListView: function(listView) {
|
|
||||||
// TODO: assert listView not already in list of this.listView
|
|
||||||
this.listViews.push(listView);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRenderList: function() {
|
|
||||||
this.renderList = _flat(this.listViews.map(function(listView) {
|
|
||||||
return listView.getPagesToRender();
|
|
||||||
}));
|
|
||||||
|
|
||||||
// TODO: Some "highest-priority" sorting algorithm on the renderList.
|
|
||||||
|
|
||||||
this.doRender();
|
|
||||||
},
|
|
||||||
|
|
||||||
pageToRender: function() {
|
|
||||||
if (this.renderList.length === 0) return null;
|
|
||||||
|
|
||||||
return this.renderList[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
doRender: function() {
|
|
||||||
var pageToRender = this.pageToRender();
|
|
||||||
|
|
||||||
if (!pageToRender) return;
|
|
||||||
|
|
||||||
pageToRender.render(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
finishedRendering: function(pageView) {
|
|
||||||
var idx = this.renderList.indexOf(pageView);
|
|
||||||
|
|
||||||
pageView.callAfterRenderedCallbacks();
|
|
||||||
|
|
||||||
// If the finished pageView is in the list of pages to render,
|
|
||||||
// then remove it from the list and render start rendering the
|
|
||||||
// next page.
|
|
||||||
if (idx !== -1) {
|
|
||||||
this.renderList.splice(idx, 1);
|
|
||||||
this.doRender();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onResize: function() {
|
|
||||||
var renderAgain = false
|
|
||||||
this.listViews.map(function(listView) {
|
|
||||||
if (listView.calculateScale()) {
|
|
||||||
listView.layout();
|
|
||||||
renderAgain = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (renderAgain) {
|
|
||||||
this.updateRenderList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var LAYOUT_SINGLE = 'layout_single';
|
|
||||||
var SCALE_MODE_AUTO = 'scale_mode_auto';
|
|
||||||
var SCALE_MODE_VALUE = 'scale_mode_value';
|
|
||||||
var SCALE_MODE_FIT_WIDTH = 'scale_mode_fit_width';
|
|
||||||
var SCALE_MODE_FIT_HEIGHT = 'scale_mode_fit_height';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main view that holds the single pageContainer/pageViews of the pdfDoc.
|
|
||||||
*/
|
|
||||||
function ListView(dom, options) {
|
|
||||||
this.dom = dom;
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
this.pageLayout = LAYOUT_SINGLE;
|
|
||||||
this.scaleMode = SCALE_MODE_VALUE;
|
|
||||||
this.scale = 1.0;
|
|
||||||
|
|
||||||
this.pageWidthOffset = 0;
|
|
||||||
this.pageHeightOffset = 0;
|
|
||||||
|
|
||||||
this.pageViews = [];
|
|
||||||
this.containerViews = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView.prototype = {
|
|
||||||
setDocument: function(pdfDoc) {
|
|
||||||
this.clearPages();
|
|
||||||
|
|
||||||
this.pdfDoc = pdfDoc;
|
|
||||||
|
|
||||||
this.assignPagesToContainer();
|
|
||||||
this.layout();
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPages: function() {
|
|
||||||
var self = this;
|
|
||||||
this.containerViews.map(function(container) {
|
|
||||||
self.dom.removeChild(container.dom);
|
|
||||||
});
|
|
||||||
this.pageViews = [];
|
|
||||||
this.containerViews = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
assignPagesToContainer: function() {
|
|
||||||
// TODO: Handle multiple layout types here. For now, assume to have one page
|
|
||||||
// per pageContainer.
|
|
||||||
this.pdfDoc.pages.map(function(page) {
|
|
||||||
var pageView = new PageView(page, this);
|
|
||||||
|
|
||||||
// TODO: Switch over to a proper event handler
|
|
||||||
var that = this;
|
|
||||||
var index = that.pageViews.length;
|
|
||||||
pageView.ondblclick = function(e) {
|
|
||||||
e.page = index;
|
|
||||||
if (that.ondblclick) {
|
|
||||||
that.ondblclick.call(that, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.pageViews.push(pageView);
|
|
||||||
|
|
||||||
var container = new PageContainerView(this);
|
|
||||||
container.setPageView(pageView, 0);
|
|
||||||
this.containerViews.push(container);
|
|
||||||
|
|
||||||
this.dom.appendChild(container.dom);
|
|
||||||
}, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
layout: function() {
|
|
||||||
this.savePdfPosition();
|
|
||||||
this.containerViews.forEach(function(containerView) {
|
|
||||||
containerView.layout();
|
|
||||||
});
|
|
||||||
this.restorePdfPosition();
|
|
||||||
},
|
|
||||||
|
|
||||||
getScale: function() {
|
|
||||||
return this.scale;
|
|
||||||
},
|
|
||||||
|
|
||||||
getScaleMode: function() {
|
|
||||||
return this.scaleMode;
|
|
||||||
},
|
|
||||||
|
|
||||||
setScaleMode: function(scaleMode, scale) {
|
|
||||||
this.scaleMode = scaleMode;
|
|
||||||
if (scaleMode == SCALE_MODE_VALUE) {
|
|
||||||
this.scale = scale;
|
|
||||||
}
|
|
||||||
this.calculateScale();
|
|
||||||
this.layout();
|
|
||||||
},
|
|
||||||
|
|
||||||
setScale: function(scale) {
|
|
||||||
this.setScaleMode(SCALE_MODE_VALUE, scale);
|
|
||||||
},
|
|
||||||
|
|
||||||
setToAutoScale: function() {
|
|
||||||
this.setScaleMode(SCALE_MODE_AUTO);
|
|
||||||
},
|
|
||||||
|
|
||||||
setToFitWidth: function() {
|
|
||||||
this.setScaleMode(SCALE_MODE_FIT_WIDTH);
|
|
||||||
},
|
|
||||||
|
|
||||||
setToFitHeight: function() {
|
|
||||||
this.setScaleMode(SCALE_MODE_FIT_HEIGHT);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Calculates the new scale. Returns `true` if the scale changed.
|
|
||||||
calculateScale: function() {
|
|
||||||
var newScale = this.scale;
|
|
||||||
var oldScale = newScale;
|
|
||||||
var scaleMode = this.scaleMode;
|
|
||||||
if (scaleMode === SCALE_MODE_FIT_WIDTH || scaleMode === SCALE_MODE_AUTO) {
|
|
||||||
var clientWidth = this.dom.clientWidth;
|
|
||||||
if (clientWidth == 0) {
|
|
||||||
logger.debug("LIST VIEW NOT VISIBLE")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var maxNormalWidth = 0;
|
|
||||||
this.containerViews.forEach(function(containerView) {
|
|
||||||
maxNormalWidth = Math.max(maxNormalWidth, containerView.normalWidth);
|
|
||||||
});
|
|
||||||
var scale = (clientWidth - this.pageWidthOffset)/maxNormalWidth;
|
|
||||||
if (scaleMode === SCALE_MODE_AUTO) {
|
|
||||||
scale = Math.min(1.0, scale);
|
|
||||||
}
|
|
||||||
newScale = scale;
|
|
||||||
} else if (scaleMode === SCALE_MODE_FIT_HEIGHT) {
|
|
||||||
var clientHeight = this.dom.clientHeight;
|
|
||||||
if (clientHeight == 0) {
|
|
||||||
logger.debug("LIST VIEW NOT VISIBLE")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var maxNormalHeight = 0;
|
|
||||||
this.containerViews.forEach(function(containerView) {
|
|
||||||
maxNormalHeight = Math.max(maxNormalHeight, containerView.normalHeight);
|
|
||||||
});
|
|
||||||
newScale = (clientHeight - this.pageHeightOffset)/maxNormalHeight;
|
|
||||||
}
|
|
||||||
this.scale = newScale;
|
|
||||||
return newScale !== oldScale;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPagesToRender: function() {
|
|
||||||
// Cache these results to avoid dom access.
|
|
||||||
this.scrollTop = this.dom.scrollTop;
|
|
||||||
this.scrollBottom = this.scrollTop + this.dom.clientHeight;
|
|
||||||
|
|
||||||
// TODO: For now, this only returns the visible pages and not
|
|
||||||
// +1/-1 one to render in advance.
|
|
||||||
return this.pageViews.filter(function(pageView) {
|
|
||||||
var isVisible = pageView.isVisible();
|
|
||||||
|
|
||||||
if (isVisible && !pageView.isRendered) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
navigateTo: function(destRef) {
|
|
||||||
var destination = this.pdfDoc.destinations[destRef]
|
|
||||||
if (typeof destination !== "object") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.debug("NAVIGATING TO", destination);
|
|
||||||
|
|
||||||
var pageRef = destination[0];
|
|
||||||
var pageNumber = this.pdfDoc.pageRefMap[pageRef.num + ' ' + pageRef.gen];
|
|
||||||
if (typeof pageNumber !== "number") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.debug("PAGE NUMBER", pageNumber);
|
|
||||||
|
|
||||||
var pageView = this.pageViews[pageNumber];
|
|
||||||
|
|
||||||
var destinationType = destination[1].name;
|
|
||||||
switch(destinationType) {
|
|
||||||
case "XYZ":
|
|
||||||
var x = destination[2];
|
|
||||||
var y = destination[3];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// TODO
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var position = pageView.getPdfPositionInViewer(x,y);
|
|
||||||
this.dom.scrollTop = position.top;
|
|
||||||
this.dom.scrollLeft = position.left;
|
|
||||||
},
|
|
||||||
|
|
||||||
savePdfPosition: function() {
|
|
||||||
this.pdfPosition = this.getPdfPosition();
|
|
||||||
logger.debug("SAVED PDF POSITION", this.pdfPosition);
|
|
||||||
},
|
|
||||||
|
|
||||||
restorePdfPosition: function() {
|
|
||||||
logger.debug("RESTORING PDF POSITION", this.pdfPosition);
|
|
||||||
this.setPdfPosition(this.pdfPosition);
|
|
||||||
},
|
|
||||||
|
|
||||||
getPdfPosition: function(fromTop) {
|
|
||||||
var pdfPosition = null;
|
|
||||||
for (var i = 0; i < this.pageViews.length; i++) {
|
|
||||||
var pageView = this.pageViews[i];
|
|
||||||
var pdfOffset = pageView.getUppermostVisiblePdfOffset();
|
|
||||||
if (pdfOffset !== null) {
|
|
||||||
if (fromTop) {
|
|
||||||
pdfOffset = pageView.normalHeight - pdfOffset;
|
|
||||||
}
|
|
||||||
pdfPosition = {
|
|
||||||
page: i,
|
|
||||||
offset: {
|
|
||||||
top: pdfOffset,
|
|
||||||
left: 0 // TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pdfPosition;
|
|
||||||
},
|
|
||||||
|
|
||||||
setPdfPosition: function(pdfPosition, fromTop) {
|
|
||||||
if (typeof pdfPosition !== "undefined" && pdfPosition != null) {
|
|
||||||
var offset = pdfPosition.offset;
|
|
||||||
var page_index = pdfPosition.page;
|
|
||||||
var pageView = this.pageViews[page_index];
|
|
||||||
if (fromTop) {
|
|
||||||
offset.top = pageView.normalHeight - offset.top;
|
|
||||||
}
|
|
||||||
var position = pageView.getPdfPositionInViewer(offset.left, offset.top);
|
|
||||||
this.dom.scrollTop = position.top;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setHighlights: function(highlights, fromTop) {
|
|
||||||
for (i = 0; i < highlights.length; i++) {
|
|
||||||
var pageIndex = highlights[i].page;
|
|
||||||
var pageView = this.pageViews[pageIndex];
|
|
||||||
(function(pageView, highlight) {
|
|
||||||
pageView.doWhenRendered(function() {
|
|
||||||
pageView.addHighlight(highlight.highlight, fromTop);
|
|
||||||
});
|
|
||||||
})(pageView, highlights[i]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearHighlights: function() {
|
|
||||||
for (var i = 0; i < this.pageViews.length; i++) {
|
|
||||||
var pageView = this.pageViews[i].clearHighlights();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A PageContainerView holds multiple PageViews. E.g. in a two-page layout,
|
|
||||||
* every pageContainerView holds two PageViews and is responsible to layout
|
|
||||||
* them.
|
|
||||||
*/
|
|
||||||
function PageContainerView(listView) {
|
|
||||||
this.listView = listView;
|
|
||||||
|
|
||||||
var dom = this.dom = document.createElement('div');
|
|
||||||
dom.className = 'plv-page-container page-container';
|
|
||||||
this.pages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
PageContainerView.prototype = {
|
|
||||||
setPageView: function(pageView, idx) {
|
|
||||||
// TODO: handle case if there is already a page here
|
|
||||||
this.pages[idx] = pageView;
|
|
||||||
|
|
||||||
// TODO: handle page idx properly
|
|
||||||
this.dom.appendChild(pageView.dom);
|
|
||||||
},
|
|
||||||
|
|
||||||
removePageView: function(idx) {
|
|
||||||
// TODO: check if idx is set on page[]
|
|
||||||
this.dom.removeChild(this.pages[idx].dom);
|
|
||||||
},
|
|
||||||
|
|
||||||
layout: function() {
|
|
||||||
var scale = this.listView.scale;
|
|
||||||
|
|
||||||
var normalWidth = 0;
|
|
||||||
var normalHeight = 0;
|
|
||||||
|
|
||||||
this.pages.forEach(function(pageView) {
|
|
||||||
pageView.layout();
|
|
||||||
normalWidth += pageView.normalWidth;
|
|
||||||
normalHeight = Math.max(pageView.normalHeight, normalHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.normalWidth = normalWidth;
|
|
||||||
this.normalHeight = normalHeight;
|
|
||||||
|
|
||||||
this.dom.style.width = (normalWidth * scale) + 'px';
|
|
||||||
this.dom.style.height = (normalHeight * scale) + 'px';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var RenderingStates = {
|
|
||||||
INITIAL: 0,
|
|
||||||
RUNNING: 1,
|
|
||||||
PAUSED: 2,
|
|
||||||
FINISHED: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
var idCounter = 0;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The view for a single page.
|
|
||||||
*/
|
|
||||||
function PageView(page, listView) {
|
|
||||||
this.page = page;
|
|
||||||
this.listView = listView;
|
|
||||||
this.id = idCounter++;
|
|
||||||
this.number = this.page.number;
|
|
||||||
|
|
||||||
this.rotation = 0;
|
|
||||||
|
|
||||||
this.isRendered = false;
|
|
||||||
this.renderState = RenderingStates.INITIAL;
|
|
||||||
this.afterRenderCallbacks = [];
|
|
||||||
|
|
||||||
var dom = this.dom = document.createElement('div');
|
|
||||||
dom.className = "plv-page-view page-view";
|
|
||||||
var that = this;
|
|
||||||
dom.ondblclick = function(e) {
|
|
||||||
var layerX = e.layerX;
|
|
||||||
var layerY = e.layerY;
|
|
||||||
var element = e.target;
|
|
||||||
while (element.offsetParent && element.offsetParent !== dom) {
|
|
||||||
layerX = layerX + element.offsetLeft;
|
|
||||||
layerY = layerY + element.offsetTop;
|
|
||||||
element = element.offsetParent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pdfPoint = that.viewport.convertToPdfPoint(layerX, layerY);
|
|
||||||
var event = {
|
|
||||||
x: pdfPoint[0],
|
|
||||||
y: that.normalHeight - pdfPoint[1]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (that.ondblclick) {
|
|
||||||
that.ondblclick.call(that.listView, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.createNewCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
PageView.prototype = {
|
|
||||||
layout: function() {
|
|
||||||
var scale = this.listView.scale;
|
|
||||||
|
|
||||||
var viewport = this.viewport =
|
|
||||||
this.page.pdfPage.getViewport(scale, this.rotation);
|
|
||||||
|
|
||||||
this.normalWidth = viewport.width / scale;
|
|
||||||
this.normalHeight = viewport.height / scale;
|
|
||||||
|
|
||||||
// Only change the width/height property of the canvas if it really
|
|
||||||
// changed. Every assignment to the width/height property clears the
|
|
||||||
// content of the canvas.
|
|
||||||
|
|
||||||
var outputScale = this.getOutputScale();
|
|
||||||
|
|
||||||
var scaledWidth = (Math.floor(viewport.width) * outputScale.sx) | 0;
|
|
||||||
var scaledHeight = (Math.floor(viewport.height) * outputScale.sy) | 0;
|
|
||||||
|
|
||||||
var newWidth = Math.floor(viewport.width);
|
|
||||||
var newHeight = Math.floor(viewport.height);
|
|
||||||
|
|
||||||
if (this.canvas.width !== newWidth) {
|
|
||||||
this.canvas.width = scaledWidth;
|
|
||||||
this.canvas.style.width = newWidth + 'px';
|
|
||||||
this.resetRenderState();
|
|
||||||
}
|
|
||||||
if (this.canvas.height !== newHeight) {
|
|
||||||
this.canvas.height = scaledHeight;
|
|
||||||
this.canvas.style.height = newHeight + 'px';
|
|
||||||
this.resetRenderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(outputScale.scaled){
|
|
||||||
var ctx = this.getCanvasContext()
|
|
||||||
ctx.scale(outputScale.sx, outputScale.sy)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.width = viewport.width;
|
|
||||||
this.height = viewport.height;
|
|
||||||
},
|
|
||||||
|
|
||||||
isVisible: function() {
|
|
||||||
var listView = this.listView;
|
|
||||||
var dom = this.dom;
|
|
||||||
var offsetTop = dom.offsetTop;
|
|
||||||
var offsetBottom = offsetTop + this.height;
|
|
||||||
|
|
||||||
return offsetBottom >= listView.scrollTop &&
|
|
||||||
offsetTop <= listView.scrollBottom;
|
|
||||||
},
|
|
||||||
|
|
||||||
resetRenderState: function() {
|
|
||||||
this.renderState = RenderingStates.INITIAL;
|
|
||||||
this.isRendered = false;
|
|
||||||
if (this.textLayerDiv) {
|
|
||||||
this.dom.removeChild(this.textLayerDiv);
|
|
||||||
delete this.textLayerDiv;
|
|
||||||
}
|
|
||||||
if (this.annotationsLayerDiv) {
|
|
||||||
this.dom.removeChild(this.annotationsLayerDiv);
|
|
||||||
delete this.annotationsLayerDiv;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function(renderController) {
|
|
||||||
return this.page.render(this, renderController);
|
|
||||||
},
|
|
||||||
|
|
||||||
getCanvasContext: function() {
|
|
||||||
return this.canvas.getContext('2d');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
getOutputScale: function(){
|
|
||||||
var ctx = this.getCanvasContext()
|
|
||||||
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
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
createNewCanvas: function() {
|
|
||||||
if (this.canvas) {
|
|
||||||
this.dom.removeChild(this.canvas);
|
|
||||||
}
|
|
||||||
var canvas = this.canvas = document.createElement('canvas');
|
|
||||||
this.dom.appendChild(canvas);
|
|
||||||
this.layout();
|
|
||||||
},
|
|
||||||
|
|
||||||
pdfPositionToPixels: function(x, y) {
|
|
||||||
return this.viewport.convertToViewportPoint(x, y);
|
|
||||||
},
|
|
||||||
|
|
||||||
getCanvasPositionInViewer: function() {
|
|
||||||
return {
|
|
||||||
left: this.canvas.offsetLeft + this.dom.offsetLeft,
|
|
||||||
top: this.canvas.offsetTop + this.dom.offsetTop
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getPdfPositionInViewer: function(x, y) {
|
|
||||||
pageOffset = this.pdfPositionToPixels(x, y);
|
|
||||||
canvasOffset = this.getCanvasPositionInViewer();
|
|
||||||
return {
|
|
||||||
left: canvasOffset.left + pageOffset[0],
|
|
||||||
top: canvasOffset.top + pageOffset[1]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getUppermostVisibleCanvasOffset: function() {
|
|
||||||
var pagePosition = this.getCanvasPositionInViewer();
|
|
||||||
var pageHeight = this.canvas.height;
|
|
||||||
var viewportTop = this.listView.dom.scrollTop;
|
|
||||||
var viewportHeight = this.listView.dom.clientHeight;
|
|
||||||
// Check if the top of the page is showing, i.e:
|
|
||||||
// _______________
|
|
||||||
// | |
|
|
||||||
// | ........ |
|
|
||||||
// | . . |
|
|
||||||
// ----.------.---
|
|
||||||
// . .
|
|
||||||
// ........
|
|
||||||
var topVisible = (pagePosition.top > viewportTop && pagePosition.top < viewportTop + viewportHeight);
|
|
||||||
// Check if at least some of the page is showing, i.e:
|
|
||||||
// ........ ........
|
|
||||||
// ____.______.___ ---.------.---
|
|
||||||
// | . . | or | . . |
|
|
||||||
// | . . | | . . |
|
|
||||||
// | ........ | | . . |
|
|
||||||
// --------------- ---.------.---
|
|
||||||
// ........
|
|
||||||
var someContentVisible = (pagePosition.top < viewportTop && pagePosition.top + pageHeight > viewportTop);
|
|
||||||
if (topVisible) {
|
|
||||||
return 0;
|
|
||||||
} else if (someContentVisible) {
|
|
||||||
return viewportTop - pagePosition.top;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getUppermostVisiblePdfOffset: function() {
|
|
||||||
var canvasOffset = this.getUppermostVisibleCanvasOffset();
|
|
||||||
if (canvasOffset === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var pdfOffset = this.viewport.convertToPdfPoint(0, canvasOffset);
|
|
||||||
return pdfOffset[1];
|
|
||||||
},
|
|
||||||
|
|
||||||
clearHighlights: function() {
|
|
||||||
if (this.highlightsLayer) {
|
|
||||||
this.highlightsLayer.clearHighlights();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addHighlight: function(highlight, fromTop) {
|
|
||||||
if (this.highlightsLayer) {
|
|
||||||
var top = highlight.top;
|
|
||||||
var left = highlight.left;
|
|
||||||
var width = highlight.width;
|
|
||||||
var height = highlight.height;
|
|
||||||
if (fromTop) {
|
|
||||||
top = this.normalHeight - top;
|
|
||||||
}
|
|
||||||
this.highlightsLayer.addHighlight(left, top, width, height);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
doWhenRendered: function(callback) {
|
|
||||||
if (this.isRendered) {
|
|
||||||
callback()
|
|
||||||
} else {
|
|
||||||
this.afterRenderCallbacks.push(callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
callAfterRenderedCallbacks: function () {
|
|
||||||
var callbacks = this.afterRenderCallbacks;
|
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
|
||||||
callbacks[i]();
|
|
||||||
}
|
|
||||||
this.afterRenderCallbacks = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstraction around the raw page object of PDF.JS, that also handles the
|
|
||||||
* rendering logic of (maybe multiple) pageView(s) that are based on this page.
|
|
||||||
*/
|
|
||||||
function Page(pdfPage, number) {
|
|
||||||
this.number = number;
|
|
||||||
this.pdfPage = pdfPage;
|
|
||||||
|
|
||||||
this.renderContextList = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.prototype = {
|
|
||||||
render: function(pageView, renderController) {
|
|
||||||
var renderContext;
|
|
||||||
|
|
||||||
// FEATURE: If the page was rendered already once, then use the old
|
|
||||||
// version as a placeholder until the new version is rendered at the
|
|
||||||
// expected quality.
|
|
||||||
|
|
||||||
// FEATURE: If the page can be rendered at low quality (thumbnail) and
|
|
||||||
// there is already a higher resolution rendering, then use this one
|
|
||||||
// instead of rerendering from scratch again.
|
|
||||||
|
|
||||||
// PageView is not layouted.
|
|
||||||
if (!pageView.viewport) return;
|
|
||||||
|
|
||||||
// Nothing todo.
|
|
||||||
if (pageView.isRendered) return;
|
|
||||||
|
|
||||||
// Not most important page to render ATM.
|
|
||||||
if (renderController.pageToRender() !== pageView) return;
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
var viewport;
|
|
||||||
if (renderContext = this.renderContextList[pageView.id]) {
|
|
||||||
viewport = renderContext.viewport;
|
|
||||||
|
|
||||||
// TODO: handle rotation
|
|
||||||
if (viewport.height !== pageView.viewport.height ||
|
|
||||||
viewport.height !== pageView.viewport.height)
|
|
||||||
{
|
|
||||||
// The viewport changed -> need to rerender.
|
|
||||||
renderContext.abandon = true;
|
|
||||||
delete self.renderContextList[pageView.id];
|
|
||||||
pageView.createNewCanvas();
|
|
||||||
self.render(pageView, renderController);
|
|
||||||
} else if (renderContext.state === RenderingStates.PAUSED) {
|
|
||||||
// There is already a not finished renderState ->
|
|
||||||
logger.debug('RESUME', pageView.id);
|
|
||||||
renderContext.resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!renderContext) {
|
|
||||||
viewport = pageView.viewport;
|
|
||||||
// No rendering data yet -> create a new renderContext and start
|
|
||||||
// the rendering process.
|
|
||||||
|
|
||||||
var textLayer;
|
|
||||||
var textLayerBuilder = pageView.listView.options.textLayerBuilder;
|
|
||||||
if (textLayerBuilder) {
|
|
||||||
var textLayerDiv = pageView.textLayerDiv = document.createElement("div");
|
|
||||||
textLayerDiv.className = 'plv-text-layer text-layer';
|
|
||||||
pageView.dom.appendChild(textLayerDiv);
|
|
||||||
textLayer = new textLayerBuilder(textLayerDiv);
|
|
||||||
this.pdfPage.getTextContent().then(
|
|
||||||
function(textContent) {
|
|
||||||
textLayer.setTextContent(textContent);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var annotationsLayerBuilder = pageView.listView.options.annotationsLayerBuilder;
|
|
||||||
if (annotationsLayerBuilder) {
|
|
||||||
var annotationsLayerDiv = pageView.annotationsLayerDiv = document.createElement("div");
|
|
||||||
annotationsLayerDiv.className = 'plv-annotations-layer annotations-layer';
|
|
||||||
pageView.dom.appendChild(annotationsLayerDiv);
|
|
||||||
var annotationsLayer = new annotationsLayerBuilder(pageView, annotationsLayerDiv);
|
|
||||||
this.pdfPage.getAnnotations().then(
|
|
||||||
function(annotations) {
|
|
||||||
annotationsLayer.setAnnotations(annotations)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var highlightsLayerBuilder = pageView.listView.options.highlightsLayerBuilder;
|
|
||||||
if (highlightsLayerBuilder) {
|
|
||||||
var highlightsLayerDiv = pageView.highlightsLayerDiv = document.createElement("div");
|
|
||||||
highlightsLayerDiv.className = 'plv-highlights-layer highlights-layer';
|
|
||||||
pageView.dom.appendChild(highlightsLayerDiv);
|
|
||||||
pageView.highlightsLayer = new highlightsLayerBuilder(pageView, highlightsLayerDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContext = {
|
|
||||||
canvasContext: pageView.getCanvasContext(),
|
|
||||||
viewport: viewport,
|
|
||||||
textLayer: textLayer,
|
|
||||||
continueCallback: function pdfViewContinueCallback(cont) {
|
|
||||||
if (renderContext.abandon) {
|
|
||||||
logger.debug("ABANDON", pageView.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderController.pageToRender() !== pageView) {
|
|
||||||
logger.debug('PAUSE', pageView.id);
|
|
||||||
renderContext.state = RenderingStates.PAUSED;
|
|
||||||
renderContext.resume = function resumeCallback() {
|
|
||||||
renderContext.state = RenderingStates.RUNNING;
|
|
||||||
cont();
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.debug('CONT', pageView.id);
|
|
||||||
cont();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.renderContextList[pageView.id] = renderContext;
|
|
||||||
|
|
||||||
logger.debug("BEGIN", pageView.id);
|
|
||||||
renderContext.renderPromise = this.pdfPage.render(renderContext).promise;
|
|
||||||
renderContext.renderPromise.then(
|
|
||||||
function pdfPageRenderCallback() {
|
|
||||||
logger.debug('DONE', pageView.id);
|
|
||||||
pageView.isRendered = true;
|
|
||||||
renderController.finishedRendering(pageView);
|
|
||||||
},
|
|
||||||
failDumper
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderContext.renderPromise;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function PDFListView(mainDiv, options) {
|
|
||||||
if (typeof(options) != "object") {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
if (typeof(options.logLevel) != "number") {
|
|
||||||
options.logLevel = Logger.INFO;
|
|
||||||
}
|
|
||||||
logger.logLevel = options.logLevel;
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.listView = new ListView(mainDiv, options);
|
|
||||||
this.listView.ondblclick = function(e) {
|
|
||||||
if (options.ondblclick) {
|
|
||||||
options.ondblclick.call(self, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderController = new RenderController();
|
|
||||||
this.renderController.addListView(this.listView);
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
|
|
||||||
|
|
||||||
mainDiv.addEventListener('scroll', function() {
|
|
||||||
// This will update the list AND start rendering if needed.
|
|
||||||
self.renderController.updateRenderList();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
// Check if the scale changed due to the resizing.
|
|
||||||
if (self.listView.calculateScale()) {
|
|
||||||
// Update the layout and start rendering. Changing the layout
|
|
||||||
// of the PageView makes it rendering stop.
|
|
||||||
self.listView.layout();
|
|
||||||
self.renderController.updateRenderList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
PDFListView.prototype = {
|
|
||||||
loadPdf: function(url, onProgress) {
|
|
||||||
this.doc = new Document(url, null, onProgress);
|
|
||||||
var self = this;
|
|
||||||
var promise = this.doc.initialized;
|
|
||||||
promise.then(function() {
|
|
||||||
logger.debug('LOADED');
|
|
||||||
self.listView.setDocument(self.doc);
|
|
||||||
self.renderController.updateRenderList();
|
|
||||||
}, failDumper);
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
getScale: function() {
|
|
||||||
return this.listView.getScale();
|
|
||||||
},
|
|
||||||
|
|
||||||
getScaleMode: function() {
|
|
||||||
return this.listView.getScaleMode()
|
|
||||||
},
|
|
||||||
|
|
||||||
setScaleMode: function(scaleMode, scale) {
|
|
||||||
this.listView.setScaleMode(scaleMode, scale);
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
},
|
|
||||||
|
|
||||||
setScale: function(scale) {
|
|
||||||
this.listView.setScale(scale);
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
},
|
|
||||||
|
|
||||||
setToAutoScale: function() {
|
|
||||||
this.listView.setToAutoScale();
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
},
|
|
||||||
|
|
||||||
setToFitWidth: function() {
|
|
||||||
this.listView.setToFitWidth();
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
},
|
|
||||||
|
|
||||||
setToFitHeight: function() {
|
|
||||||
this.listView.setToFitHeight();
|
|
||||||
this.renderController.updateRenderList();
|
|
||||||
},
|
|
||||||
|
|
||||||
onResize: function() {
|
|
||||||
this.renderController.onResize();
|
|
||||||
},
|
|
||||||
|
|
||||||
getPdfPosition: function(fromTop) {
|
|
||||||
return this.listView.getPdfPosition(fromTop);
|
|
||||||
},
|
|
||||||
|
|
||||||
setPdfPosition: function(pdfPosition, fromTop) {
|
|
||||||
this.listView.setPdfPosition(pdfPosition, fromTop);
|
|
||||||
},
|
|
||||||
|
|
||||||
setHighlights: function(highlights, fromTop) {
|
|
||||||
this.listView.setHighlights(highlights, fromTop);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearHighlights: function() {
|
|
||||||
this.listView.clearHighlights();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
PDFListView.Logger = Logger;
|
|
||||||
|
|
||||||
return PDFListView;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,197 +0,0 @@
|
||||||
(function (name, context, definition) {
|
|
||||||
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
|
||||||
else if (typeof define == 'function' && define.amd) define(definition)
|
|
||||||
else context[name] = definition()
|
|
||||||
})('TextLayerBuilder', this, function (name, context) {
|
|
||||||
|
|
||||||
// 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 TextLayerBuilder(textLayerDiv) {
|
|
||||||
this.textLayerDiv = textLayerDiv;
|
|
||||||
};
|
|
||||||
|
|
||||||
TextLayerBuilder.prototype = {
|
|
||||||
beginLayout: function() {
|
|
||||||
this.textDivs = [];
|
|
||||||
this.textLayerQueue = [];
|
|
||||||
this.renderingDone = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
endLayout: function() {
|
|
||||||
this.layoutDone = true;
|
|
||||||
this.insertDivContent();
|
|
||||||
},
|
|
||||||
|
|
||||||
appendText: function(geom) {
|
|
||||||
var textDiv = document.createElement('div');
|
|
||||||
|
|
||||||
// vScale and hScale already contain the scaling to pixel units
|
|
||||||
var fontHeight = geom.fontSize * Math.abs(geom.vScale);
|
|
||||||
textDiv.dataset.canvasWidth = geom.canvasWidth * geom.hScale;
|
|
||||||
textDiv.dataset.fontName = geom.fontName;
|
|
||||||
|
|
||||||
textDiv.style.fontSize = fontHeight + 'px';
|
|
||||||
textDiv.style.fontFamily = geom.fontFamily;
|
|
||||||
textDiv.style.left = geom.x + 'px';
|
|
||||||
textDiv.style.top = (geom.y - fontHeight) + 'px';
|
|
||||||
|
|
||||||
textDiv.ondblclick = function(e) {
|
|
||||||
if (window.getSelection)
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
else if (document.selection)
|
|
||||||
document.selection.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The content of the div is set in the `setTextContent` function.
|
|
||||||
|
|
||||||
this.textDivs.push(textDiv);
|
|
||||||
},
|
|
||||||
|
|
||||||
setTextContent: function(textContent) {
|
|
||||||
this.textContent = textContent;
|
|
||||||
this.insertDivContent();
|
|
||||||
},
|
|
||||||
|
|
||||||
insertDivContent: function() {
|
|
||||||
// Only set the content of the divs once layout has finished, the content
|
|
||||||
// for the divs is available and content is not yet set on the divs.
|
|
||||||
if (!this.layoutDone || this.divContentDone || !this.textContent)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.divContentDone = true;
|
|
||||||
|
|
||||||
var textDivs = this.textDivs;
|
|
||||||
var bidiTexts = this.textContent.bidiTexts;
|
|
||||||
|
|
||||||
for (var i = 0; i < bidiTexts.length; i++) {
|
|
||||||
var bidiText = bidiTexts[i];
|
|
||||||
var textDiv = textDivs[i];
|
|
||||||
if (!/\S/.test(bidiText.str)) {
|
|
||||||
textDiv.dataset.isWhitespace = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
textDiv.textContent = bidiText.str;
|
|
||||||
// bidiText.dir may be 'ttb' for vertical texts.
|
|
||||||
textDiv.dir = bidiText.dir === 'rtl' ? 'rtl' : 'ltr';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderLayer();
|
|
||||||
},
|
|
||||||
|
|
||||||
renderLayer: function() {
|
|
||||||
var self = this;
|
|
||||||
var textDivs = this.textDivs;
|
|
||||||
var bidiTexts = this.textContent.bidiTexts;
|
|
||||||
var textLayerDiv = this.textLayerDiv;
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
var ctx = canvas.getContext('2d');
|
|
||||||
var textLayerFrag = document.createDocumentFragment();
|
|
||||||
|
|
||||||
// No point in rendering so many divs as it'd make the browser unusable
|
|
||||||
// even after the divs are rendered
|
|
||||||
var MAX_TEXT_DIVS_TO_RENDER = 100000;
|
|
||||||
if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (var i = 0, ii = textDivs.length; i < ii; i++) {
|
|
||||||
var textDiv = textDivs[i];
|
|
||||||
if ('isWhitespace' in textDiv.dataset) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
textLayerFrag.appendChild(textDiv);
|
|
||||||
|
|
||||||
ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily;
|
|
||||||
var width = ctx.measureText(textDiv.textContent).width;
|
|
||||||
|
|
||||||
if (width > 0) {
|
|
||||||
var textScale = textDiv.dataset.canvasWidth / width;
|
|
||||||
|
|
||||||
var transform = 'scale(' + textScale + ', 1)';
|
|
||||||
if (bidiTexts[i].dir === 'ttb') {
|
|
||||||
transform = 'rotate(90deg) ' + transform;
|
|
||||||
}
|
|
||||||
CustomStyle.setProp('transform' , textDiv, transform);
|
|
||||||
CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
|
|
||||||
|
|
||||||
textLayerDiv.appendChild(textDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderingDone = true;
|
|
||||||
//this.updateMatches();
|
|
||||||
|
|
||||||
textLayerDiv.appendChild(textLayerFrag);
|
|
||||||
},
|
|
||||||
|
|
||||||
/*setupRenderLayoutTimer: function() {
|
|
||||||
// Schedule renderLayout() if user has been scrolling, otherwise
|
|
||||||
// run it right away
|
|
||||||
var RENDER_DELAY = 200; // in ms
|
|
||||||
var self = this;
|
|
||||||
if (Date.now() - PDFView.lastScroll > RENDER_DELAY) {
|
|
||||||
// Render right away
|
|
||||||
this.renderLayer();
|
|
||||||
} else {
|
|
||||||
// Schedule
|
|
||||||
if (this.renderTimer)
|
|
||||||
clearTimeout(this.renderTimer);
|
|
||||||
this.renderTimer = setTimeout(function() {
|
|
||||||
self.setupRenderLayoutTimer();
|
|
||||||
}, RENDER_DELAY);
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
};
|
|
||||||
|
|
||||||
return TextLayerBuilder;
|
|
||||||
|
|
||||||
});
|
|
Loading…
Reference in a new issue