1
0
Fork 0
mirror of https://github.com/Yetangitu/owncloud-apps.git synced 2025-10-02 14:49:17 +02:00

files_reader: More PDF madness, next/previous search hit navigation w/highlighting, etc...

This commit is contained in:
frankdelange 2017-04-07 22:21:15 +02:00
parent 640256c49b
commit f909cd0f1b
11 changed files with 448 additions and 458 deletions

Binary file not shown.

View file

@ -1,4 +1,3 @@
- search
- bookmarks - bookmarks
- annotations - annotations
- settings - settings
@ -6,5 +5,7 @@
- rtl and ltr - rtl and ltr
- test canvas size restriction - test canvas size restriction
- test internal links
- add IDs to highlights so they can be marked when hovered - or when related list item is hovered
- add preload/postload of X pages around current position (optional), store resulting rendered pages in off-screen canvas?

View file

@ -258,6 +258,8 @@
<a id="slider" class="icon-menu"> <a id="slider" class="icon-menu">
<?php p($l->t("menu")); ?> <?php p($l->t("menu")); ?>
</a> </a>
<div id="status_message_left">
</div>
</div> </div>
<div id="metainfo" class="nightshift"> <div id="metainfo" class="nightshift">
<span id="book-title"> <span id="book-title">
@ -273,23 +275,11 @@
</span> </span>
</div> </div>
<div id="title-controls"> <div id="title-controls">
<!-- select works fine, except for the fact that - as usual - apple mobile does not support icons... <div id="status_message_right">
<label for="zoomlevel">zoom: </label> </div>
<select id="zoomlevel"> <div id="match_count">
<option value="spread" data-icon="&#xe86d;" data-text="2-page">&#xe86d;</option> </div>
<option value="fit_page" data-icon="&#xe86e;" data-text="fit page">&#xe86e;</option> <!-- select works fine, except for the fact that - as usual - apple mobile acts up... -->
<option value="fit_width" data-icon="&#xe85c;" data-text="fit width">&#xe85c;</option>
<option value="0.25" class="text">25%</option>
<option value="0.5" class="text">50%</option>
<option value="0.75" class="text">75%</option>
<option value="1" class="text">100%</option>
<option value="1.25" class="text">125%</option>
<option value="1.5" class="text">150%</option>
<option value="2" class="text">200%</option>
<option value="3" class="text">300%</option>
<option value="4" class="text">400%</option>
</select>
-->
<div id="zoom_options" class="hide"> <div id="zoom_options" class="hide">
<div class="zoom_option icon-double_page_mode" data-value="spread" data-class="icon-double_page_mode" data-text=""></div> <div class="zoom_option icon-double_page_mode" data-value="spread" data-class="icon-double_page_mode" data-text=""></div>
<div class="zoom_option icon-single_page_mode" data-value="fit_page" data-class="icon-single_page_mode" data-text=""></div> <div class="zoom_option icon-single_page_mode" data-value="fit_page" data-class="icon-single_page_mode" data-text=""></div>

View file

@ -24,11 +24,18 @@ PDFJS.reader.ControlsController = function(book) {
$rotate_left = $("#rotate_left"), $rotate_left = $("#rotate_left"),
$rotate_right = $("#rotate_right"), $rotate_right = $("#rotate_right"),
$page_num = $("#page_num"), $page_num = $("#page_num"),
$total_pages = $("#total_pages"); $total_pages = $("#total_pages"),
$status_message_left = $("#status_message_left"),
$status_message_right = $("#status_message_right");
var STATUS_MESSAGE_LENGTH = 30,
STATUS_MESSAGE_TIMEOUT = 3000,
status_timeout_left,
status_timeout_right;
if (reader.isMobile() === true) { if (reader.isMobile() === true) {
$titlebar.addClass("background_visible"); $titlebar.addClass("background_visible");
}; }
var show = function () { var show = function () {
$titlebar.removeClass("hide"); $titlebar.removeClass("hide");
@ -46,6 +53,31 @@ PDFJS.reader.ControlsController = function(book) {
reader.ControlsController.toggle(); reader.ControlsController.toggle();
}); });
var setStatus = function (message, right) {
$status_message = (right) ? $status_message_right : $status_message_left;
status_timeout = (right) ? status_timeout_right : status_timeout_left;
$status_message[0].textContent = reader.ellipsize(message, STATUS_MESSAGE_LENGTH);
//$status_message[0].textContent = message;
if (typeof status_timeout === "number") {
clearTimeout(status_timeout);
status_timeout = undefined;
}
status_timeout = setTimeout(function () {
$status_message[0].textContent = "";
}, STATUS_MESSAGE_TIMEOUT);
if (right) {
status_timeout_right = status_timeout;
} else {
status_timeout_left = status_timeout;
}
};
var fullscreen = false; var fullscreen = false;
$slider.on("click", function () { $slider.on("click", function () {
@ -105,21 +137,8 @@ PDFJS.reader.ControlsController = function(book) {
}); });
/* select works fine on most browsers, but - of course - apple mobile has 'special needs' /* select works fine on most browsers, but - of course - apple mobile has 'special needs' so
* in that it does not support any styling in the drop-down list, and as such can not display
* icons there. Due to the unfortunate fact that many still buy these apple-encumbered devices
* a custom select is needed... * a custom select is needed...
*
$zoomlevel.val(settings.zoomLevel);
$zoomlevel.on("change", function () {
reader.setZoom($(this).val());
var $option = $zoomlevel.find(":selected");
if ($option.data("icon") !== undefined)
$("#zoom_icon")[0].textContent = $option.data("icon");
});
*
*/ */
/* custom select, supports icons in drop-down list */ /* custom select, supports icons in drop-down list */
@ -245,7 +264,7 @@ PDFJS.reader.ControlsController = function(book) {
break; break;
} }
e.stopPropagation; e.stopPropagation();
}; };
@ -273,21 +292,23 @@ PDFJS.reader.ControlsController = function(book) {
text; text;
if (zoom === "spread") { if (zoom === "spread") {
if (oddPageRight === true) { if (oddPageRight === true) {
page -= page % 2; page -= page % 2;
} else {
page -= (page + 1) % 2;
}
}
if (page >= 0 && page <= total_pages) {
if (page === total_pages) {
text = reader.getPageLabel(page);
} else if (page === 0) {
text = reader.getPageLabel(page + 1);
} else { } else {
text = reader.getPageLabel(page) + "-" + reader.getPageLabel(page + 1); page -= (page + 1) % 2;
} }
if (page >= 0 && page <= total_pages) {
if (page === total_pages) {
text = reader.getPageLabel(page);
} else if (page === 0) {
text = reader.getPageLabel(page + 1);
} else {
text = reader.getPageLabel(page) + "-" + reader.getPageLabel(page + 1);
}
}
} else {
text = reader.getPageLabel(page);
} }
$page_num[0].textContent = text; $page_num[0].textContent = text;
@ -332,6 +353,7 @@ PDFJS.reader.ControlsController = function(book) {
"setZoomIcon": setZoomIcon, "setZoomIcon": setZoomIcon,
"setRotateIcon": setRotateIcon, "setRotateIcon": setRotateIcon,
"setCurrentPage": setCurrentPage, "setCurrentPage": setCurrentPage,
"setPageCount": setPageCount "setPageCount": setPageCount,
"setStatus": setStatus
}; };
}; };

View file

@ -1,4 +1,4 @@
PDFJS.reader.ReaderController = function(book) { PDFJS.reader.ReaderController = function() {
var $main = $("#main"), var $main = $("#main"),
$viewer = $("#viewer"), $viewer = $("#viewer"),
$divider = $("#divider"), $divider = $("#divider"),
@ -13,7 +13,8 @@ PDFJS.reader.ReaderController = function(book) {
$bookmark = $("#bookmark"), $bookmark = $("#bookmark"),
$note = $("#note"), $note = $("#note"),
$rotate_left = $("#rotate_left"), $rotate_left = $("#rotate_left"),
$rotate_right = $("#rotate_right"); $rotate_right = $("#rotate_right"),
$clear_search = $("#clear_search");
var reader = this, var reader = this,
book = this.book, book = this.book,
@ -76,6 +77,8 @@ PDFJS.reader.ReaderController = function(book) {
var page_no = false; var page_no = false;
e.preventDefault();
switch (settings.keyboard[e.keyCode]) { switch (settings.keyboard[e.keyCode]) {
case 'previous': case 'previous':
$prev.click(); $prev.click();
@ -121,6 +124,22 @@ PDFJS.reader.ReaderController = function(book) {
break; break;
case 'cycleZoom': case 'cycleZoom':
reader.cycleZoom(); reader.cycleZoom();
break;
case 'previousMatch':
reader.SearchController.nextMatch(true);
break;
case 'nextMatch':
reader.SearchController.nextMatch(false);
break;
case 'clearSearch':
$clear_search.click();
break;
case 'search':
if (e.shiftKey) {
reader.SidebarController.changePanelTo("Search");
reader.SearchController.show();
}
break; break;
default: default:
console.log("unsupported keyCode: " + e.keyCode); console.log("unsupported keyCode: " + e.keyCode);
@ -129,7 +148,7 @@ PDFJS.reader.ReaderController = function(book) {
if (page_no) { if (page_no) {
reader.queuePage(page_no); reader.queuePage(page_no);
} }
} };
document.addEventListener('keydown', keyCommands, false); document.addEventListener('keydown', keyCommands, false);

View file

@ -1,32 +1,7 @@
var FindStates = {
FIND_FOUND: 0,
FIND_NOTFOUND: 1,
FIND_WRAPPED: 2,
FIND_PENDING: 3
};
var FIND_SCROLL_OFFSET_TOP = -50;
var FIND_SCROLL_OFFSET_LEFT = -400;
var CHARACTERS_TO_NORMALIZE = {
'\u2018': '\'', // Left single quotation mark
'\u2019': '\'', // Right single quotation mark
'\u201A': '\'', // Single low-9 quotation mark
'\u201B': '\'', // Single high-reversed-9 quotation mark
'\u201C': '"', // Left double quotation mark
'\u201D': '"', // Right double quotation mark
'\u201E': '"', // Double low-9 quotation mark
'\u201F': '"', // Double high-reversed-9 quotation mark
'\u00BC': '1/4', // Vulgar fraction one quarter
'\u00BD': '1/2', // Vulgar fraction one half
'\u00BE': '3/4', // Vulgar fraction three quarters
};
PDFJS.reader.SearchController = function () { PDFJS.reader.SearchController = function () {
var reader = this, var reader = this,
book = this.book, book = this.book;
query = "";
var $searchBox = $("#searchBox"), var $searchBox = $("#searchBox"),
$clearBtn = $("#searchBox").next(), $clearBtn = $("#searchBox").next(),
@ -34,54 +9,53 @@ PDFJS.reader.SearchController = function () {
$searchResults = $("#searchResults"), $searchResults = $("#searchResults"),
$searchView = $("#searchView"), $searchView = $("#searchView"),
$body = $("#viewer iframe").contents().find('body'), $body = $("#viewer iframe").contents().find('body'),
$sidebar = $("#sidebar"); $sidebar = $("#sidebar"),
$match_count = $("#match_count");
var onShow = function() { /* search logic, partly from Mozilla pdfViewer */
$searchView.addClass("open"); var CHARACTERS_TO_NORMALIZE = {
$searchBox.focus(); '\u2018': '\'', // Left single quotation mark
'\u2019': '\'', // Right single quotation mark
'\u201A': '\'', // Single low-9 quotation mark
'\u201B': '\'', // Single high-reversed-9 quotation mark
'\u201C': '"', // Left double quotation mark
'\u201D': '"', // Right double quotation mark
'\u201E': '"', // Double low-9 quotation mark
'\u201F': '"', // Double high-reversed-9 quotation mark
'\u00BC': '1/4', // Vulgar fraction one quarter
'\u00BD': '1/2', // Vulgar fraction one half
'\u00BE': '3/4', // Vulgar fraction three quarters
}; };
var onHide = function() { var startedTextExtraction = false,
unhighlight(); extractTextPromises = [],
$searchView.removeClass("open"); matchCount = 0,
}; pendingFindMatches = Object.create(null);
this.onUpdateResultsCount = null;
this.onUpdateState = null;
// Compile the regular expression for text normalization once. // Compile the regular expression for text normalization once.
var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''); var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''),
this.normalizationRegex = new RegExp('[' + replace + ']', 'g'); normalizationRegex = new RegExp('[' + replace + ']', 'g');
var reset = function () { var reset = function () {
this.startedTextExtraction = false;
this.extractTextPromises = []; pendingFindMatches = Object.create(null);
this.pendingFindMatches = Object.create(null); reader.search_active = false; // If active, find results will be highlighted.
this.active = false; // If active, find results will be highlighted. reader.pageMatches.length = 0;
this.pageContents = []; // Stores the text for each page. reader.pageMatchesLength = null;
this.pageMatches = []; reader.search_state = null;
this.pageMatchesLength = null; matchCount = 0;
this.matchCount = 0; resetMatchCounter();
this.selected = { // Currently selected match. reader.selected = { // Currently selected match.
pageIdx: -1, pageIdx: -1,
matchIdx: -1 matchIdx: -1,
at_start: false,
at_end: false
}; };
this.offset = { // Where the find algorithm currently is in the document. updatePage();
pageIdx: null,
matchIdx: null
};
this.pagesToSearch = null;
this.resumePageIdx = null;
this.state = null;
this.dirtyMatch = false;
this.findTimeout = null;
}; };
reset();
var normalize = function (text) { var normalize = function (text) {
return text.replace(this.normalizationRegex, function (ch) { return text.replace(normalizationRegex, function (ch) {
return CHARACTERS_TO_NORMALIZE[ch]; return CHARACTERS_TO_NORMALIZE[ch];
}); });
}; };
@ -151,7 +125,7 @@ PDFJS.reader.SearchController = function () {
} }
matches.push(matchIdx); matches.push(matchIdx);
} }
this.pageMatches[pageIndex] = matches; reader.pageMatches[pageIndex] = matches;
}; };
@ -179,21 +153,22 @@ PDFJS.reader.SearchController = function () {
} }
} }
// Prepare arrays for store the matches. // Prepare arrays for store the matches.
if (!this.pageMatchesLength) { if (!reader.pageMatchesLength) {
this.pageMatchesLength = []; reader.pageMatchesLength = [];
} }
this.pageMatchesLength[pageIndex] = []; reader.pageMatchesLength[pageIndex] = [];
this.pageMatches[pageIndex] = []; reader.pageMatches[pageIndex] = [];
// Sort matchesWithLength, clean up intersecting terms // Sort matchesWithLength, clean up intersecting terms
// and put the result into the two arrays. // and put the result into the two arrays.
_prepareMatches(matchesWithLength, this.pageMatches[pageIndex], _prepareMatches(matchesWithLength, reader.pageMatches[pageIndex],
this.pageMatchesLength[pageIndex]); reader.pageMatchesLength[pageIndex]);
}; };
var getSnippet = function (pageIndex, position) { var getSnippet = function (pageIndex, position) {
var ellipse = '…', var ellipse = '…',
match_length = this.state.query.length, match_length = reader.search_state.query.length,
span = '<span class="search_match">', span = '<span class="search_match">',
span_close = '</span>', span_close = '</span>',
limit = 160 + span.length + span_close.length, limit = 160 + span.length + span_close.length,
@ -201,10 +176,10 @@ PDFJS.reader.SearchController = function () {
trailer, trailer,
context; context;
leader = this.pageContents[pageIndex].substring(position - limit/2, position); leader = reader.pageContents[pageIndex].substring(position - limit/2, position);
leader = leader.slice(leader.indexOf(" ")); leader = leader.slice(leader.indexOf(" "));
trailer = this.pageContents[pageIndex].substring(position + match_length, position + limit/2 + match_length); trailer = reader.pageContents[pageIndex].substring(position + match_length, position + limit/2 + match_length);
query = this.pageContents[pageIndex].substring(position, position + match_length); query = reader.pageContents[pageIndex].substring(position, position + match_length);
context = ellipse + leader + span + query + span_close + trailer; context = ellipse + leader + span + query + span_close + trailer;
@ -215,7 +190,6 @@ PDFJS.reader.SearchController = function () {
var listitem = document.createElement("li"), var listitem = document.createElement("li"),
link = document.createElement("a"), link = document.createElement("a"),
id = parseInt(pageIndex + 1) + ":" + position,
item = { item = {
url: null, url: null,
dest: null, dest: null,
@ -227,10 +201,9 @@ PDFJS.reader.SearchController = function () {
item.dest = [pageIndex,position]; item.dest = [pageIndex,position];
//link.textContent = getSnippet(pageIndex, position); //link.textContent = getSnippet(pageIndex, position);
link.innerHTML = getSnippet(pageIndex, position); listitem.dataset.index = ++matchCount;
link.innerHTML = '<span class="match_label">' + matchCount + '</span>' + getSnippet(pageIndex, position);
listitem.classList.add("list_item"); listitem.classList.add("list_item");
listitem.id = "search-"+id;
listitem.dataset.position = position;
reader.bindLink(link, item); reader.bindLink(link, item);
link.classList.add("search_link"); link.classList.add("search_link");
listitem.appendChild(link); listitem.appendChild(link);
@ -238,16 +211,40 @@ PDFJS.reader.SearchController = function () {
return listitem; return listitem;
}; };
var createItemList = function (pageIdx) {
var currentIdx = reader.settings.currentPage - 1,
item,
i = 0;
// currentIdx can be up to 2 different from pageIdx due to oddPageFirst and spread rendering
if (Math.abs(pageIdx - currentIdx) <= 2)
updatePage(pageIdx);
var fragment = document.createDocumentFragment();
var listitem = document.createElement("li");
listitem.textContent="page " + parseInt(pageIdx + 1);
listitem.classList.add("search_page_header");
fragment.appendChild(listitem);
reader.pageMatches[pageIdx].forEach(function (match) {
item = createItem(pageIdx, match);
item.id = "match:" + pageIdx + ":" + i;
item.classList.add("match:" + pageIdx + ":" + i++);
fragment.appendChild(item);
updateMatchCounter();
});
return fragment;
};
var calcFindMatch = function (pageIndex) { var calcFindMatch = function (pageIndex) {
var pageContent = normalize(this.pageContents[pageIndex]); var pageContent = normalize(reader.pageContents[pageIndex]);
var query = normalize(this.state.query); var query = normalize(reader.search_state.query);
var caseSensitive = this.state.caseSensitive; var caseSensitive = reader.search_state.caseSensitive;
var phraseSearch = this.state.phraseSearch; var phraseSearch = reader.search_state.phraseSearch;
var queryLen = query.length; var queryLen = query.length;
if (queryLen === 0) { if (queryLen === 0) {
// Do nothing: the matches should be wiped out already. reset();
return; return;
} }
@ -265,24 +262,25 @@ PDFJS.reader.SearchController = function () {
var extractText = function () { var extractText = function () {
if (this.startedTextExtraction) { if (startedTextExtraction) {
return; return;
} }
this.startedTextExtraction = true; startedTextExtraction = true;
this.pageContents = []; reader.pageContents = [];
var extractTextPromisesResolves = []; var extractTextPromisesResolves = [];
var numPages = reader.settings.numPages; var numPages = reader.settings.numPages;
for (var i = 0; i < numPages; i++) { for (var i = 0; i < numPages; i++) {
this.extractTextPromises.push(new Promise(function (resolve) { extractTextPromises.push(new Promise(function (resolve) {
extractTextPromisesResolves.push(resolve); extractTextPromisesResolves.push(resolve);
})); }));
} }
var self = this;
function extractPageText(pageIndex) { function extractPageText(pageIndex) {
reader.getPageTextContent(pageIndex).then( reader.getPageTextContent(pageIndex).then(
function textContentResolved(textContent) { function textContentResolved(textContent) {
reader.ControlsController.setStatus("extracting text page " + parseInt(pageIndex + 1),true);
var textItems = textContent.items; var textItems = textContent.items;
var str = []; var str = [];
@ -291,7 +289,7 @@ PDFJS.reader.SearchController = function () {
} }
// Store the pageContent as a string. // Store the pageContent as a string.
self.pageContents.push(str.join(' ').replace(/\s\s+/g, ' ')); reader.pageContents.push(str.join(''));
extractTextPromisesResolves[pageIndex](pageIndex); extractTextPromisesResolves[pageIndex](pageIndex);
if ((pageIndex + 1) < reader.settings.numPages) { if ((pageIndex + 1) < reader.settings.numPages) {
@ -303,264 +301,211 @@ PDFJS.reader.SearchController = function () {
extractPageText(0); extractPageText(0);
}; };
var executeCommand = function (cmd, state) { var updatePage = function (pageIdx) {
if (this.state === null || cmd !== 'findagain') {
this.dirtyMatch = true; var pageNum = (pageIdx) ? pageIdx + 1 : null;
if (reader.resourcelst) {
reader.resourcelst.forEach(function(list) {
if (list.textLayer && (pageNum === list.pageNum || pageNum === null)) {
list.textLayer.updateMatches();
}
});
} }
this.state = state;
updateUIState(FindStates.FIND_PENDING);
console.log("execute command ", cmd, " with state ", state);
reader.firstPagePromise.then(function() {
extractText();
clearTimeout(this.findTimeout);
if (cmd === 'find') {
// Only trigger the find action after 250ms of silence.
//this.findTimeout = setTimeout(nextMatch.bind(this), 250);
generateMatchList();
} else {
nextMatch();
}
}.bind(this));
}; };
var updatePage = function (index) { var executeCommand = function (cmd, state) {
if (this.selected.pageIdx === index) { reader.search_state = state;
// If the page is selected, scroll the page into view, which triggers
// rendering the page, which adds the textLayer. Once the textLayer is
// build, it will scroll onto the selected match.
reader.settings.currentPage = index + 1;
}
//var page = this.pdfViewer.getPageView(index); reader.firstPagePromise.then(function() {
//if (page.textLayer) { if (reader.pageContents.length < reader.settings.numPages)
// page.textLayer.updateMatches(); extractText();
//}
if (cmd === 'find') {
reader.search_active = true;
$match_count.show();
generateMatchList();
}
}.bind(this));
}; };
var generateMatchList = function () { var generateMatchList = function () {
var container = document.getElementById("searchResults"), var container = document.getElementById("searchResults"),
numPages = reader.settings.numPages, numPages = reader.settings.numPages,
self = this; currentIdx = reader.settings.currentPage - 1,
i;
for (var i = 0; i < numPages; i++) { if (reader.pageContents.length !== numPages) {
//var placeholder = document.createElement("li"); extractText();
//placeholder.style.display = "none"; for (i = 0; i < numPages; i++) {
//container.appendChild(placeholder); if (!(i in pendingFindMatches)) {
if (!(i in this.pendingFindMatches)) { pendingFindMatches[i] = true;
this.pendingFindMatches[i] = true; extractTextPromises[i].then(function(pageIdx) {
this.extractTextPromises[i].then(function(pageIdx) { delete pendingFindMatches[pageIdx];
delete self.pendingFindMatches[pageIdx];
calcFindMatch(pageIdx);
if (self.pageMatches[pageIdx].length > 0) {
reader.pageMatches[pageIdx] = self.pageMatches[pageIdx];
var fragment = document.createDocumentFragment();
var listitem = document.createElement("li");
listitem.textContent="page " + parseInt(pageIdx + 1);
listitem.classList.add("search_page_header");
fragment.appendChild(listitem);
self.pageMatches[pageIdx].forEach(function (match) {
fragment.appendChild(createItem(pageIdx, match));
});
container.appendChild(fragment);
}
});
}
}
};
var nextMatch = function () {
var previous = this.state.findPrevious;
var currentPageIndex = reader.settings.currentPage - 1;
var numPages = reader.settings.numPages;
this.active = true;
if (this.dirtyMatch) {
// Need to recalculate the matches, reset everything.
this.dirtyMatch = false;
this.selected.pageIdx = this.selected.matchIdx = -1;
this.offset.pageIdx = currentPageIndex;
this.offset.matchIdx = null;
this.hadMatch = false;
this.resumePageIdx = null;
this.pageMatches = [];
this.matchCount = 0;
this.pageMatchesLength = null;
var self = this;
for (var i = 0; i < numPages; i++) {
// Wipe out any previous highlighted matches.
updatePage(i);
// As soon as the text is extracted start finding the matches.
if (!(i in this.pendingFindMatches)) {
this.pendingFindMatches[i] = true;
this.extractTextPromises[i].then(function(pageIdx) {
delete self.pendingFindMatches[pageIdx];
calcFindMatch(pageIdx); calcFindMatch(pageIdx);
if (reader.pageMatches[pageIdx].length > 0) {
container.appendChild(createItemList(pageIdx));
}
}); });
} }
} }
} } else {
for (i = 0; i < numPages; i++) {
// If there's no query there's no point in searching. calcFindMatch(i);
if (this.state.query === '') { if (reader.pageMatches[i].length > 0) {
updateUIState(FindStates.FIND_FOUND); container.appendChild(createItemList(i));
return;
}
// If we're waiting on a page, we return since we can't do anything else.
if (this.resumePageIdx) {
return;
}
var offset = this.offset;
// Keep track of how many pages we should maximally iterate through.
this.pagesToSearch = numPages;
// If there's already a matchIdx that means we are iterating through a
// page's matches.
if (offset.matchIdx !== null) {
var numPageMatches = this.pageMatches[offset.pageIdx].length;
if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
(previous && offset.matchIdx > 0)) {
// The simple case; we just have advance the matchIdx to select
// the next match on the page.
this.hadMatch = true;
offset.matchIdx = (previous ? offset.matchIdx - 1 :
offset.matchIdx + 1);
updateMatch(true);
return;
} }
// We went beyond the current page's matches, so we advance to
// the next page.
advanceOffsetPage(previous);
}
// Start searching through the page.
nextPageMatch();
};
var matchesReady = function (matches) {
var offset = this.offset;
var numMatches = matches.length;
var previous = this.state.findPrevious;
if (numMatches) {
// There were matches for the page, so initialize the matchIdx.
this.hadMatch = true;
offset.matchIdx = (previous ? numMatches - 1 : 0);
updateMatch(true);
return true;
}
// No matches, so attempt to search the next page.
advanceOffsetPage(previous);
if (offset.wrapped) {
offset.matchIdx = null;
if (this.pagesToSearch < 0) {
// No point in wrapping again, there were no matches.
updateMatch(false);
// while matches were not found, searching for a page
// with matches should nevertheless halt.
return true;
} }
} }
// Matches were not found (and searching is not done).
return false;
}; };
/** var nextMatch = function (previous) {
* The method is called back from the text layer when match presentation
* is updated.
* @param {number} pageIndex - page index.
* @param {number} index - match index.
* @param {Array} elements - text layer div elements array.
* @param {number} beginIdx - start index of the div array for the match.
*/
var updateMatchPosition = function (
pageIndex, index, elements, beginIdx) {
if (this.selected.matchIdx === index &&
this.selected.pageIdx === pageIndex) {
//var spot = {
// top: FIND_SCROLL_OFFSET_TOP,
// left: FIND_SCROLL_OFFSET_LEFT
//};
//scrollIntoView(elements[beginIdx], spot,
// /* skipOverflowHiddenElements = */ true);
}
console.log("would scroll into view here except for the fact that Reader is a non-scrolling reader...");
};
var nextPageMatch = function () { /* don't try to follow non-existing matches */
if (this.resumePageIdx !== null) { if (!reader.search_active ||
console.error('There can only be one pending page.'); reader.pageMatches.length === 0)
} return;
do {
var pageIdx = this.offset.pageIdx;
var matches = this.pageMatches[pageIdx];
if (!matches) {
// The matches don't exist yet for processing by "matchesReady",
// so set a resume point for when they do exist.
this.resumePageIdx = pageIdx;
break;
}
} while (!matchesReady(matches));
};
var advanceOffsetPage = function (previous) { var numPages = reader.settings.numPages,
var offset = this.offset; selected = reader.selected,
var numPages = this.extractTextPromises.length; leftIdx = idxOrNull(reader.resourcelst[0].pageNum),
offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1); rightIdx = idxOrNull(reader.resourcelst[1].pageNum),
offset.matchIdx = null; try_match = false;
this.pagesToSearch--; /* prevent match cycling on first or last page */
if (!((previous && selected.at_start) || (!previous && selected.at_end))) {
if (offset.pageIdx >= numPages || offset.pageIdx < 0) { selected.at_start = selected.at_end = false;
offset.pageIdx = (previous ? numPages - 1 : 0);
offset.wrapped = true;
}
};
var updateMatch = function (found) { /* when in spread view, start at left (forward search) or right (backward search) page
var state = FindStates.FIND_NOTFOUND; * if not iterating over matches on currently visible pages
var wrapped = this.offset.wrapped; */
this.offset.wrapped = false; if (!(selected.matchIdx !== -1 && isVisible(selected.pageIdx))) {
if (previous) {
selected.pageIdx = (typeof rightIdx === "number") ? rightIdx : leftIdx;
} else {
selected.pageIdx = (typeof leftIdx === "number") ? leftIdx : rightIdx;
}
try_match = true;
if (found) { } else {
var previousPage = this.selected.pageIdx;
this.selected.pageIdx = this.offset.pageIdx; var numPageMatches = reader.pageMatches[selected.pageIdx].length;
this.selected.matchIdx = this.offset.matchIdx;
state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND); if ((!previous && selected.matchIdx + 1 < numPageMatches) || (previous && selected.matchIdx > 0)) {
// Update the currently selected page to wipe out any selected matches. selected.matchIdx = (previous ? selected.matchIdx - 1 : selected.matchIdx + 1);
if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { updateOrQueue();
updatePage(previousPage); return;
} else {
selected.pageIdx += (previous) ? -1 : 1;
try_match = true;
}
} }
} }
updateUIState(state, this.state.findPrevious); if (try_match && nextPageMatch(previous)) {
if (this.selected.pageIdx !== -1) { updateOrQueue();
updatePage(this.selected.pageIdx); return;
} else {
if (previous) {
reader.ControlsController.setStatus("at first match", true);
selected.at_start = true;
} else {
reader.ControlsController.setStatus("at last match", true);
selected.at_end = true;
}
return;
}
function idxOrNull(num) {
if (typeof num === "number") {
return num - 1;
} else {
return null;
}
}
function isVisible (idx) {
return (idx === leftIdx || idx === rightIdx);
}
function nextPageMatch (previous) {
var i,
found;
if (previous) {
for (i = selected.pageIdx; i >= -1 && reader.pageMatches[i] === undefined; i--) {}
} else {
for (i = selected.pageIdx; i <= numPages && reader.pageMatches[i] === undefined; i++) {}
}
if (i < 0 || i >= numPages) {
i = -1;
//selected.pageIdx = selected.matchIdx = -1;
selected.matchIdx = -1;
found = false;
} else {
selected.pageIdx = i;
selected.matchIdx = (previous) ? reader.pageMatches[i].length - 1 : 0;
found = true;
}
return found;
}
function updateOrQueue() {
var root = document.getElementById("searchResults"),
item,
match,
i;
item = root.getElementsByClassName("selected");
while (item.length)
item[0].classList.remove("selected");
match = document.getElementById("match:" + selected.pageIdx + ":" + selected.matchIdx);
match.classList.add("selected");
match = document.getElementsByClassName("match:" + selected.pageIdx + ":" + selected.matchIdx);
for (i = 0; i < match.length; i++)
match[i].classList.add("selected_again");
updateMatchCounter(match.dataset.index);
if (!reader.isVisible(match))
match.scrollIntoView();
if (isVisible(selected.pageIdx)) {
[ leftIdx, rightIdx ].forEach(function (idx) {
if (typeof idx === "number") updatePage(idx);
});
} else {
reader.queuePage(selected.pageIdx + 1);
}
} }
}; };
var updateUIResultsCount = function () { var updateMatchCounter = function (index) {
if (this.onUpdateResultsCount) {
onUpdateResultsCount(this.matchCount); var prefix = "";
}
if (index)
prefix = index + "/";
$match_count[0].textContent = prefix + matchCount;
}; };
var updateUIState = function (state, previous) { var resetMatchCounter = function () {
if (this.onUpdateState) { $match_count[0].textContent = "0";
onUpdateState(state, previous, this.matchCount); $match_count.hide();
}
}; };
var search = function(q) { var search = function(q) {
if (q === undefined) { if (q === undefined) {
q = $searchBox.val(); q = $searchBox.val();
@ -576,10 +521,7 @@ PDFJS.reader.SearchController = function () {
reset(); reset();
$searchResults.empty(); $searchResults.empty();
this.query = q;
executeCommand('find', {query: q}); executeCommand('find', {query: q});
highlightQuery();
}; };
$searchBox.on("keydown", function(e) { $searchBox.on("keydown", function(e) {
@ -601,14 +543,13 @@ PDFJS.reader.SearchController = function () {
$clear_search.on("click", function () { $clear_search.on("click", function () {
reset(); reset();
unhighlight();
$searchResults.empty(); $searchResults.empty();
$searchBox.val("");
}); });
var clear = function () { var clear = function () {
reset(); reset();
unhighlight();
$searchResults.empty(); $searchResults.empty();
if (reader.SidebarController.getActivePanel() == "Search") { if (reader.SidebarController.getActivePanel() == "Search") {
@ -616,14 +557,24 @@ PDFJS.reader.SearchController = function () {
} }
}; };
var highlightQuery = function(e) { // initialize search
$("#text_left").contents().highlight(this.state.query, { element: 'span' }); reset();
$("#text_right").contents().highlight(this.state.query, { element: 'span' });
if (reader.settings.preloadTextcontent) {
reader.firstPagePromise.then(function() {
setTimeout(function() {
extractText();
}, 5000);
});
}
var onShow = function() {
$searchView.addClass("open");
$searchBox.focus();
}; };
var unhighlight = function(e) { var onHide = function() {
$("#text_left").unhighlight(); $searchView.removeClass("open");
$("#text_right").unhighlight();
}; };
@ -632,7 +583,7 @@ PDFJS.reader.SearchController = function () {
"hide": onHide, "hide": onHide,
"search": search, "search": search,
"executeCommand": executeCommand, "executeCommand": executeCommand,
"highlightQuery": highlightQuery, "nextMatch": nextMatch
"unhighlight": unhighlight
}; };
}; };

View file

@ -1,40 +1,8 @@
/* Copyright 2012 Mozilla Foundation PDFJS.Reader.TextLayerController = function (options, reader) {
*
* 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.
*/
/**
* @typedef {Object} TextLayerBuilderOptions
* @property {HTMLDivElement} textLayerDiv - The text layer container.
* @property {EventBus} eventBus - The application event bus.
* @property {number} pageIndex - The page index.
* @property {PageViewport} viewport - The viewport of the text layer.
* @property {PDFFindController} findController
* @property {boolean} enhanceTextSelection - Option to turn on improved
* text selection.
*/
/**
* TextLayerBuilder provides text-selection functionality for the PDF.
* It does this by creating overlay divs over the PDF text. These divs
* contain text that matches the PDF text they are overlaying. This object
* also provides a way to highlight text that is being searched for.
* @class
*/
PDFJS.Reader.TextLayerController = function (options) {
var EXPAND_DIVS_TIMEOUT = 300; // ms var EXPAND_DIVS_TIMEOUT = 300; // ms
this.reader = reader;
this.textLayerDiv = options.textLayerDiv; this.textLayerDiv = options.textLayerDiv;
this.eventBus = options.eventBus || null; this.eventBus = options.eventBus || null;
this.textContent = null; this.textContent = null;
@ -44,7 +12,6 @@ PDFJS.Reader.TextLayerController = function (options) {
this.matches = []; this.matches = [];
this.viewport = options.viewport; this.viewport = options.viewport;
this.textDivs = []; this.textDivs = [];
this.findController = options.findController || null;
this.textLayerRenderTask = null; this.textLayerRenderTask = null;
this.enhanceTextSelection = options.enhanceTextSelection; this.enhanceTextSelection = options.enhanceTextSelection;
this._bindMouse(); this._bindMouse();
@ -117,13 +84,14 @@ PDFJS.Reader.TextLayerController.prototype.setTextContent = function (textConten
PDFJS.Reader.TextLayerController.prototype.convertMatches = function(matches, matchesLength) { PDFJS.Reader.TextLayerController.prototype.convertMatches = function(matches, matchesLength) {
var reader = this; var reader = this.reader;
var i = 0; var i = 0;
var iIndex = 0; var iIndex = 0;
var bidiTexts = this.textContent.items; var bidiTexts = this.textContent.items;
var end = bidiTexts.length - 1; var end = bidiTexts.length - 1;
var queryLen = reader.search.query.length; var queryLen = reader.search_state ?
reader.search_state.query.length : null;
var ret = []; var ret = [];
if (!matches) { if (!matches) {
return ret; return ret;
@ -179,18 +147,15 @@ PDFJS.Reader.TextLayerController.prototype.renderMatches = function (matches) {
return; return;
} }
var reader = this; var reader = this.reader;
var bidiTexts = this.textContent.items; var bidiTexts = this.textContent.items;
var textDivs = this.textDivs; var textDivs = this.textDivs;
var prevEnd = null; var prevEnd = null;
var pageIdx = this.pageIdx; var pageIdx = this.pageIdx;
var isSelectedPage = (this.findController === null ? var isSelectedPage = (pageIdx === reader.selected.pageIdx);
false : (pageIdx === this.findController.selected.pageIdx)); var selectedMatchIdx = reader.selected.matchIdx;
var selectedMatchIdx = (this.findController === null ? var highlightAll = true;
-1 : this.findController.selected.matchIdx);
var highlightAll = (this.findController === null ?
false : this.findController.state.highlightAll);
var infinity = { var infinity = {
divIdx: -1, divIdx: -1,
offset: undefined offset: undefined
@ -230,12 +195,8 @@ PDFJS.Reader.TextLayerController.prototype.renderMatches = function (matches) {
var begin = match.begin; var begin = match.begin;
var end = match.end; var end = match.end;
var isSelected = (isSelectedPage && i === selectedMatchIdx); var isSelected = (isSelectedPage && i === selectedMatchIdx);
var highlightSuffix = (isSelected ? ' selected' : ''); var id = "match:" + pageIdx + ":" + i;
var highlightSuffix = (isSelected ? ' selected ' + id : ' ' + id);
if (this.findController) {
this.findController.updateMatchPosition(pageIdx, i, textDivs,
begin.divIdx);
}
// Match inside new div. // Match inside new div.
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
@ -274,6 +235,13 @@ PDFJS.Reader.TextLayerController.prototype.updateMatches = function () {
return; return;
} }
var reader = this.reader;
// Only show matches when search is active
if (reader.search_active !== true) {
return;
}
// Clear all matches. // Clear all matches.
var matches = this.matches; var matches = this.matches;
var textDivs = this.textDivs; var textDivs = this.textDivs;
@ -292,18 +260,12 @@ PDFJS.Reader.TextLayerController.prototype.updateMatches = function () {
clearedUntilDivIdx = match.end.divIdx + 1; clearedUntilDivIdx = match.end.divIdx + 1;
} }
if (this.findController === null || !this.findController.active) {
return;
}
// Convert the matches on the page controller into the match format // Convert the matches on the page controller into the match format
// used for the textLayer. // used for the textLayer.
var pageMatches, pageMatchesLength; var pageMatches = reader.pageMatches[this.pageIdx] || null,
if (this.findController !== null) { pageMatchesLength = reader.pageMatchesLength ?
pageMatches = this.findController.pageMatches[this.pageIdx] || null; reader.pageMatchesLength[this.pageIdx] || null : null;
pageMatchesLength = (this.findController.pageMatchesLength) ?
this.findController.pageMatchesLength[this.pageIdx] || null : null;
}
this.matches = this.convertMatches(pageMatches, pageMatchesLength); this.matches = this.convertMatches(pageMatches, pageMatchesLength);
this.renderMatches(this.matches); this.renderMatches(this.matches);

View file

@ -120,7 +120,7 @@ PDFJS.reader.TocController = function() {
scale = parseFloat(settings.thumbnailWidth / width); scale = parseFloat(settings.thumbnailWidth / width);
preloadcount = parseInt(window.innerHeight / placeholder_height) + 2; preloadcount = parseInt(window.innerHeight / placeholder_height) + 2;
if (preloadcount > settings.numPages) if (preloadcount > settings.numPages)
preloadcount = numPages; preloadcount = settings.numPages;
var _timeout = setTimeout(function () { var _timeout = setTimeout(function () {
for (var i = 1; i <= preloadcount; i++) { for (var i = 1; i <= preloadcount; i++) {

View file

@ -125,6 +125,15 @@ body {
left: 0; left: 0;
} }
#status_message_left {
margin-left: 1em;
}
#status_message_right,
#match_count {
margin-right: 1em;
}
#metainfo { #metainfo {
position: fixed; position: fixed;
/* width: 80%; /* width: 80%;

View file

@ -122,6 +122,10 @@
display: block; display: block;
} }
.list_item.selected a {
color: #D2D2D2;
}
legend { legend {
margin-left: 1em; margin-left: 1em;
padding: 0.5em; padding: 0.5em;
@ -329,6 +333,20 @@ legend {
font-decoration: underline; font-decoration: underline;
} }
.search_page_header {
text-align: right;
color: #DDD;
font-style: italic;
}
.match_label {
background-color: #DDD;
color: black;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
border-radius: 3px;
padding: 0.2em;
}
.searchbox { .searchbox {
width: 80%; width: 80%;
float: left; float: left;

View file

@ -37,6 +37,7 @@ PDFJS.Reader = function(bookPath, _options) {
bookPath: bookPath, bookPath: bookPath,
textRenderDelay: TEXT_RENDER_DELAY, textRenderDelay: TEXT_RENDER_DELAY,
pageRenderDelay: PAGE_RENDER_DELAY, pageRenderDelay: PAGE_RENDER_DELAY,
preloadTextcontent: true, // true || false, preload text content to speed up first full-text search operation
canvasLimit: 0, canvasLimit: 0,
cssZoomOnly: false, // true || false, only zoom using CSS, render document at 100% size cssZoomOnly: false, // true || false, only zoom using CSS, render document at 100% size
textSelect: true, // true || false, add selectable text layer textSelect: true, // true || false, add selectable text layer
@ -70,9 +71,13 @@ PDFJS.Reader = function(bookPath, _options) {
83: 'toggleSidebar',// s 83: 'toggleSidebar',// s
84: 'toggleTitlebar', // t 84: 'toggleTitlebar', // t
68: 'toggleDay', // d 68: 'toggleDay', // d
78: 'toggleNight', // n //78: 'toggleNight', // n
55: 'search', // '/'
80: 'previousMatch', // p
78: 'nextMatch', // n
70: 'toggleFullscreen', // f 70: 'toggleFullscreen', // f
27: 'closeSidebar' // esc 27: 'closeSidebar', // esc
114: 'nextMatch' // F3
}, },
nightMode: false, nightMode: false,
dayMode: false, dayMode: false,
@ -129,8 +134,17 @@ PDFJS.Reader = function(bookPath, _options) {
this.renderQueue = false; this.renderQueue = false;
// used for search // used for search, textlayer, hightlight etc
this.pageContents = [];
this.pageMatches = []; this.pageMatches = [];
this.pageMatchesLength = null;
this.search_state = null;
this.selected = {
pageIdx: -1,
matchIdx: -1,
at_start: false,
at_end: false
};
// define which zoom states to cycle through in cycleZoom // define which zoom states to cycle through in cycleZoom
this.zoomCycle = { this.zoomCycle = {
@ -184,7 +198,7 @@ PDFJS.Reader = function(bookPath, _options) {
function(_book) { function(_book) {
reader.book = book = _book; reader.book = book = _book;
console.log(book); //console.log(book);
reader.settings.numPages = reader.book.numPages; reader.settings.numPages = reader.book.numPages;
document.getElementById('total_pages').textContent = reader.settings.numPages; document.getElementById('total_pages').textContent = reader.settings.numPages;
if(!$.isEmptyObject(reader.settings.session.cursor)) { if(!$.isEmptyObject(reader.settings.session.cursor)) {
@ -439,6 +453,8 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) {
reader.resourcelst[1].canvas.style.display = "none"; reader.resourcelst[1].canvas.style.display = "none";
// clear text layer // clear text layer
reader.resourcelst[1].textdiv.innerHTML = ""; reader.resourcelst[1].textdiv.innerHTML = "";
// clear page number
reader.resourcelst[1].pageNum = null;
// don't try to render non-existing page 0 (which is used // don't try to render non-existing page 0 (which is used
// to indicate the empty left page when oddPageRight === true) // to indicate the empty left page when oddPageRight === true)
@ -480,7 +496,6 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) {
//console.log(page); //console.log(page);
page_rotation = page.rotate; page_rotation = page.rotate;
rotation = (page_rotation + reader.settings.rotation) % 360; rotation = (page_rotation + reader.settings.rotation) % 360;
//initial_viewport = page.getViewport({scale: 1, rotation: rotation});
initial_viewport = page.getViewport(1, rotation); initial_viewport = page.getViewport(1, rotation);
page_width = initial_viewport.width; page_width = initial_viewport.width;
page_height = initial_viewport.height; page_height = initial_viewport.height;
@ -491,21 +506,6 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) {
scale_height = parseFloat(max_view_height / page_height); scale_height = parseFloat(max_view_height / page_height);
scale_width = parseFloat(max_view_width / page_width); scale_width = parseFloat(max_view_width / page_width);
/*
console.log("m_v_w: " + max_view_width
+ " m_v_h: " + max_view_height
+ " p_w: " + page_width
+ " p_h: " + page_height
+ " d_a: " + document_aspect
+ " v_a: " + view_aspect
+ " s_w: " + scale_width
+ " s_h: " + scale_height
+ " p_r: " + page_rotation
+ " r: " + rotation
+ " o: " + outputscale);
console.log("fraction: ", fraction);
*/
switch (reader.settings.zoomLevel) { switch (reader.settings.zoomLevel) {
case "spread": case "spread":
@ -604,10 +604,10 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) {
page.getTextContent({ normalizeWhitespace: true }).then(function (textContent) { page.getTextContent({ normalizeWhitespace: true }).then(function (textContent) {
resourcelst.textLayer = textLayer = new PDFJS.Reader.TextLayerController({ resourcelst.textLayer = textLayer = new PDFJS.Reader.TextLayerController({
textLayerDiv: textdiv, textLayerDiv: textdiv,
pageIdx: pageNum - 1, pageIndex: pageNum - 1,
viewport: viewport, viewport: viewport,
enhanceTextSelection: true enhanceTextSelection: true
}); }, reader);
textLayer.setTextContent(textContent); textLayer.setTextContent(textContent);
}); });
} else { } else {
@ -661,6 +661,8 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) {
canvas.style.width = reader.roundToDivide(max_view_width, fraction[1]) + 'px'; canvas.style.width = reader.roundToDivide(max_view_width, fraction[1]) + 'px';
canvas.style.height = reader.roundToDivide(max_view_height, fraction[1]) + 'px'; canvas.style.height = reader.roundToDivide(max_view_height, fraction[1]) + 'px';
} }
// reset pageNum
resourcelst.pageNum = null;
} }
}; };
@ -1065,3 +1067,19 @@ PDFJS.Reader.prototype.ellipsize = function(str, max, opts) {
return str; return str;
}; };
PDFJS.Reader.prototype.isVisible = function (element) {
var reader = this,
viewport = element.getBoundingClientRect(),
visible;
visible = (
viewport.top >= 0
&& viewport.left >= 0
&& viewport.right < window.innerWidth
&& viewport.bottom < window.innerHeight
);
return visible;
};