From f909cd0f1b86017a164c2dc029e8c91ccfa132ce Mon Sep 17 00:00:00 2001 From: frankdelange Date: Fri, 7 Apr 2017 22:21:15 +0200 Subject: [PATCH] files_reader: More PDF madness, next/previous search hit navigation w/highlighting, etc... --- files_reader/.TODO.swp | Bin 12288 -> 0 bytes files_reader/TODO | 5 +- files_reader/templates/pdfreader.php | 24 +- .../pdfjs/controllers/controls_controller.js | 84 ++- .../pdfjs/controllers/reader_controller.js | 25 +- .../pdfjs/controllers/search_controller.js | 595 ++++++++---------- .../pdfjs/controllers/textlayer_controller.js | 82 +-- .../pdfjs/controllers/toc_controller.js | 2 +- files_reader/vendor/pdfjs/css/main.css | 9 + files_reader/vendor/pdfjs/css/sidebar.css | 18 + files_reader/vendor/pdfjs/pdf.reader.js | 62 +- 11 files changed, 448 insertions(+), 458 deletions(-) delete mode 100644 files_reader/.TODO.swp diff --git a/files_reader/.TODO.swp b/files_reader/.TODO.swp deleted file mode 100644 index cc6ba4cc16a8dabecbb2def234f273d75a191990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&KTh315C-ri>5!5jI6x*z6C{iuNka!kniS+U1kpwF>@m-JX4jfs6Dem&PQV$+ z4U!(}2#E{O!mLIAL`n3m^vS#Scr5=q|CvpWJ|EET_Qcs(bME^5^7!@hTeqK_Yn1&W z?_V-zGMCek=1a<4dB$2st;+D-S6vwDg8M#}P1{NQroy#oc{u$vUDgN3Apn6#3Jj?! zc3-%UA9g11-oBxY^{$Y6=FV_y$^|V>A0MQusCI1jEg9&)!f|! D_N90R diff --git a/files_reader/TODO b/files_reader/TODO index 51e72ed..362b435 100644 --- a/files_reader/TODO +++ b/files_reader/TODO @@ -1,4 +1,3 @@ - - search - bookmarks - annotations - settings @@ -6,5 +5,7 @@ - rtl and ltr - 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? diff --git a/files_reader/templates/pdfreader.php b/files_reader/templates/pdfreader.php index da97607..49d5748 100644 --- a/files_reader/templates/pdfreader.php +++ b/files_reader/templates/pdfreader.php @@ -258,6 +258,8 @@ t("menu")); ?> +
+
@@ -273,23 +275,11 @@
- +
+
+
+
+
diff --git a/files_reader/vendor/pdfjs/controllers/controls_controller.js b/files_reader/vendor/pdfjs/controllers/controls_controller.js index 71a1d88..b23c2c6 100644 --- a/files_reader/vendor/pdfjs/controllers/controls_controller.js +++ b/files_reader/vendor/pdfjs/controllers/controls_controller.js @@ -24,11 +24,18 @@ PDFJS.reader.ControlsController = function(book) { $rotate_left = $("#rotate_left"), $rotate_right = $("#rotate_right"), $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) { $titlebar.addClass("background_visible"); - }; + } var show = function () { $titlebar.removeClass("hide"); @@ -46,6 +53,31 @@ PDFJS.reader.ControlsController = function(book) { 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; $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' - * 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 + /* select works fine on most browsers, but - of course - apple mobile has 'special needs' so * 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 */ @@ -245,7 +264,7 @@ PDFJS.reader.ControlsController = function(book) { break; } - e.stopPropagation; + e.stopPropagation(); }; @@ -273,21 +292,23 @@ PDFJS.reader.ControlsController = function(book) { text; if (zoom === "spread") { - if (oddPageRight === true) { - 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); + if (oddPageRight === true) { + page -= page % 2; } 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; @@ -332,6 +353,7 @@ PDFJS.reader.ControlsController = function(book) { "setZoomIcon": setZoomIcon, "setRotateIcon": setRotateIcon, "setCurrentPage": setCurrentPage, - "setPageCount": setPageCount + "setPageCount": setPageCount, + "setStatus": setStatus }; }; diff --git a/files_reader/vendor/pdfjs/controllers/reader_controller.js b/files_reader/vendor/pdfjs/controllers/reader_controller.js index b2ab5a7..a02b29e 100644 --- a/files_reader/vendor/pdfjs/controllers/reader_controller.js +++ b/files_reader/vendor/pdfjs/controllers/reader_controller.js @@ -1,4 +1,4 @@ -PDFJS.reader.ReaderController = function(book) { +PDFJS.reader.ReaderController = function() { var $main = $("#main"), $viewer = $("#viewer"), $divider = $("#divider"), @@ -13,7 +13,8 @@ PDFJS.reader.ReaderController = function(book) { $bookmark = $("#bookmark"), $note = $("#note"), $rotate_left = $("#rotate_left"), - $rotate_right = $("#rotate_right"); + $rotate_right = $("#rotate_right"), + $clear_search = $("#clear_search"); var reader = this, book = this.book, @@ -76,6 +77,8 @@ PDFJS.reader.ReaderController = function(book) { var page_no = false; + e.preventDefault(); + switch (settings.keyboard[e.keyCode]) { case 'previous': $prev.click(); @@ -121,6 +124,22 @@ PDFJS.reader.ReaderController = function(book) { break; case '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; default: console.log("unsupported keyCode: " + e.keyCode); @@ -129,7 +148,7 @@ PDFJS.reader.ReaderController = function(book) { if (page_no) { reader.queuePage(page_no); } - } + }; document.addEventListener('keydown', keyCommands, false); diff --git a/files_reader/vendor/pdfjs/controllers/search_controller.js b/files_reader/vendor/pdfjs/controllers/search_controller.js index d2ce383..e9fa9fa 100644 --- a/files_reader/vendor/pdfjs/controllers/search_controller.js +++ b/files_reader/vendor/pdfjs/controllers/search_controller.js @@ -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 () { var reader = this, - book = this.book, - query = ""; + book = this.book; var $searchBox = $("#searchBox"), $clearBtn = $("#searchBox").next(), @@ -34,54 +9,53 @@ PDFJS.reader.SearchController = function () { $searchResults = $("#searchResults"), $searchView = $("#searchView"), $body = $("#viewer iframe").contents().find('body'), - $sidebar = $("#sidebar"); + $sidebar = $("#sidebar"), + $match_count = $("#match_count"); - var onShow = function() { - $searchView.addClass("open"); - $searchBox.focus(); + /* search logic, partly from Mozilla pdfViewer */ + 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 }; - var onHide = function() { - unhighlight(); - $searchView.removeClass("open"); - }; - - this.onUpdateResultsCount = null; - this.onUpdateState = null; + var startedTextExtraction = false, + extractTextPromises = [], + matchCount = 0, + pendingFindMatches = Object.create(null); // Compile the regular expression for text normalization once. - var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''); - this.normalizationRegex = new RegExp('[' + replace + ']', 'g'); + var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(''), + normalizationRegex = new RegExp('[' + replace + ']', 'g'); var reset = function () { - this.startedTextExtraction = false; - this.extractTextPromises = []; - this.pendingFindMatches = Object.create(null); - this.active = false; // If active, find results will be highlighted. - this.pageContents = []; // Stores the text for each page. - this.pageMatches = []; - this.pageMatchesLength = null; - this.matchCount = 0; - this.selected = { // Currently selected match. + + pendingFindMatches = Object.create(null); + reader.search_active = false; // If active, find results will be highlighted. + reader.pageMatches.length = 0; + reader.pageMatchesLength = null; + reader.search_state = null; + matchCount = 0; + resetMatchCounter(); + reader.selected = { // Currently selected match. pageIdx: -1, - matchIdx: -1 + matchIdx: -1, + at_start: false, + at_end: false }; - this.offset = { // Where the find algorithm currently is in the document. - pageIdx: null, - matchIdx: null - }; - this.pagesToSearch = null; - this.resumePageIdx = null; - this.state = null; - this.dirtyMatch = false; - this.findTimeout = null; + updatePage(); }; - reset(); - - var normalize = function (text) { - return text.replace(this.normalizationRegex, function (ch) { + return text.replace(normalizationRegex, function (ch) { return CHARACTERS_TO_NORMALIZE[ch]; }); }; @@ -151,7 +125,7 @@ PDFJS.reader.SearchController = function () { } 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. - if (!this.pageMatchesLength) { - this.pageMatchesLength = []; + if (!reader.pageMatchesLength) { + reader.pageMatchesLength = []; } - this.pageMatchesLength[pageIndex] = []; - this.pageMatches[pageIndex] = []; + reader.pageMatchesLength[pageIndex] = []; + reader.pageMatches[pageIndex] = []; // Sort matchesWithLength, clean up intersecting terms // and put the result into the two arrays. - _prepareMatches(matchesWithLength, this.pageMatches[pageIndex], - this.pageMatchesLength[pageIndex]); + _prepareMatches(matchesWithLength, reader.pageMatches[pageIndex], + reader.pageMatchesLength[pageIndex]); + }; var getSnippet = function (pageIndex, position) { var ellipse = '…', - match_length = this.state.query.length, + match_length = reader.search_state.query.length, span = '', span_close = '', limit = 160 + span.length + span_close.length, @@ -201,10 +176,10 @@ PDFJS.reader.SearchController = function () { trailer, context; - leader = this.pageContents[pageIndex].substring(position - limit/2, position); + leader = reader.pageContents[pageIndex].substring(position - limit/2, position); leader = leader.slice(leader.indexOf(" ")); - trailer = this.pageContents[pageIndex].substring(position + match_length, position + limit/2 + match_length); - query = this.pageContents[pageIndex].substring(position, position + match_length); + trailer = reader.pageContents[pageIndex].substring(position + match_length, position + limit/2 + match_length); + query = reader.pageContents[pageIndex].substring(position, position + match_length); context = ellipse + leader + span + query + span_close + trailer; @@ -215,7 +190,6 @@ PDFJS.reader.SearchController = function () { var listitem = document.createElement("li"), link = document.createElement("a"), - id = parseInt(pageIndex + 1) + ":" + position, item = { url: null, dest: null, @@ -227,10 +201,9 @@ PDFJS.reader.SearchController = function () { item.dest = [pageIndex,position]; //link.textContent = getSnippet(pageIndex, position); - link.innerHTML = getSnippet(pageIndex, position); + listitem.dataset.index = ++matchCount; + link.innerHTML = '' + matchCount + '' + getSnippet(pageIndex, position); listitem.classList.add("list_item"); - listitem.id = "search-"+id; - listitem.dataset.position = position; reader.bindLink(link, item); link.classList.add("search_link"); listitem.appendChild(link); @@ -238,16 +211,40 @@ PDFJS.reader.SearchController = function () { 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 pageContent = normalize(this.pageContents[pageIndex]); - var query = normalize(this.state.query); - var caseSensitive = this.state.caseSensitive; - var phraseSearch = this.state.phraseSearch; + var pageContent = normalize(reader.pageContents[pageIndex]); + var query = normalize(reader.search_state.query); + var caseSensitive = reader.search_state.caseSensitive; + var phraseSearch = reader.search_state.phraseSearch; var queryLen = query.length; if (queryLen === 0) { - // Do nothing: the matches should be wiped out already. + reset(); return; } @@ -265,24 +262,25 @@ PDFJS.reader.SearchController = function () { var extractText = function () { - if (this.startedTextExtraction) { + if (startedTextExtraction) { return; } - this.startedTextExtraction = true; + startedTextExtraction = true; - this.pageContents = []; + reader.pageContents = []; var extractTextPromisesResolves = []; var numPages = reader.settings.numPages; + for (var i = 0; i < numPages; i++) { - this.extractTextPromises.push(new Promise(function (resolve) { + extractTextPromises.push(new Promise(function (resolve) { extractTextPromisesResolves.push(resolve); })); } - var self = this; function extractPageText(pageIndex) { reader.getPageTextContent(pageIndex).then( function textContentResolved(textContent) { + reader.ControlsController.setStatus("extracting text page " + parseInt(pageIndex + 1),true); var textItems = textContent.items; var str = []; @@ -291,7 +289,7 @@ PDFJS.reader.SearchController = function () { } // Store the pageContent as a string. - self.pageContents.push(str.join(' ').replace(/\s\s+/g, ' ')); + reader.pageContents.push(str.join('')); extractTextPromisesResolves[pageIndex](pageIndex); if ((pageIndex + 1) < reader.settings.numPages) { @@ -303,264 +301,211 @@ PDFJS.reader.SearchController = function () { extractPageText(0); }; - var executeCommand = function (cmd, state) { - if (this.state === null || cmd !== 'findagain') { - this.dirtyMatch = true; + var updatePage = function (pageIdx) { + + 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) { - // 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; - } + reader.search_state = state; - //var page = this.pdfViewer.getPageView(index); - //if (page.textLayer) { - // page.textLayer.updateMatches(); - //} + reader.firstPagePromise.then(function() { + if (reader.pageContents.length < reader.settings.numPages) + extractText(); + + if (cmd === 'find') { + reader.search_active = true; + $match_count.show(); + generateMatchList(); + } + }.bind(this)); }; var generateMatchList = function () { var container = document.getElementById("searchResults"), numPages = reader.settings.numPages, - self = this; + currentIdx = reader.settings.currentPage - 1, + i; - for (var i = 0; i < numPages; i++) { - //var placeholder = document.createElement("li"); - //placeholder.style.display = "none"; - //container.appendChild(placeholder); - if (!(i in this.pendingFindMatches)) { - this.pendingFindMatches[i] = true; - this.extractTextPromises[i].then(function(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]; + if (reader.pageContents.length !== numPages) { + extractText(); + for (i = 0; i < numPages; i++) { + if (!(i in pendingFindMatches)) { + pendingFindMatches[i] = true; + extractTextPromises[i].then(function(pageIdx) { + delete pendingFindMatches[pageIdx]; calcFindMatch(pageIdx); + if (reader.pageMatches[pageIdx].length > 0) { + container.appendChild(createItemList(pageIdx)); + } }); } } - } - - // If there's no query there's no point in searching. - if (this.state.query === '') { - updateUIState(FindStates.FIND_FOUND); - 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; + } else { + for (i = 0; i < numPages; i++) { + calcFindMatch(i); + if (reader.pageMatches[i].length > 0) { + container.appendChild(createItemList(i)); } - // 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; }; - /** - * 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 nextMatch = function (previous) { - var nextPageMatch = function () { - if (this.resumePageIdx !== null) { - console.error('There can only be one pending page.'); - } - 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)); - }; + /* don't try to follow non-existing matches */ + if (!reader.search_active || + reader.pageMatches.length === 0) + return; - var advanceOffsetPage = function (previous) { - var offset = this.offset; - var numPages = this.extractTextPromises.length; - offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1); - offset.matchIdx = null; + var numPages = reader.settings.numPages, + selected = reader.selected, + leftIdx = idxOrNull(reader.resourcelst[0].pageNum), + rightIdx = idxOrNull(reader.resourcelst[1].pageNum), + 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) { - offset.pageIdx = (previous ? numPages - 1 : 0); - offset.wrapped = true; - } - }; + selected.at_start = selected.at_end = false; - var updateMatch = function (found) { - var state = FindStates.FIND_NOTFOUND; - var wrapped = this.offset.wrapped; - this.offset.wrapped = false; + /* when in spread view, start at left (forward search) or right (backward search) page + * if not iterating over matches on currently visible pages + */ + 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) { - var previousPage = this.selected.pageIdx; - this.selected.pageIdx = this.offset.pageIdx; - this.selected.matchIdx = this.offset.matchIdx; - state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND); - // Update the currently selected page to wipe out any selected matches. - if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { - updatePage(previousPage); + } else { + + var numPageMatches = reader.pageMatches[selected.pageIdx].length; + + if ((!previous && selected.matchIdx + 1 < numPageMatches) || (previous && selected.matchIdx > 0)) { + selected.matchIdx = (previous ? selected.matchIdx - 1 : selected.matchIdx + 1); + updateOrQueue(); + return; + } else { + selected.pageIdx += (previous) ? -1 : 1; + try_match = true; + } } } - updateUIState(state, this.state.findPrevious); - if (this.selected.pageIdx !== -1) { - updatePage(this.selected.pageIdx); + if (try_match && nextPageMatch(previous)) { + updateOrQueue(); + 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 () { - if (this.onUpdateResultsCount) { - onUpdateResultsCount(this.matchCount); - } + var updateMatchCounter = function (index) { + + var prefix = ""; + + if (index) + prefix = index + "/"; + + $match_count[0].textContent = prefix + matchCount; }; - var updateUIState = function (state, previous) { - if (this.onUpdateState) { - onUpdateState(state, previous, this.matchCount); - } + var resetMatchCounter = function () { + $match_count[0].textContent = "0"; + $match_count.hide(); }; - var search = function(q) { if (q === undefined) { q = $searchBox.val(); @@ -576,10 +521,7 @@ PDFJS.reader.SearchController = function () { reset(); $searchResults.empty(); - this.query = q; - executeCommand('find', {query: q}); - highlightQuery(); }; $searchBox.on("keydown", function(e) { @@ -601,14 +543,13 @@ PDFJS.reader.SearchController = function () { $clear_search.on("click", function () { reset(); - unhighlight(); $searchResults.empty(); + $searchBox.val(""); }); var clear = function () { reset(); - unhighlight(); $searchResults.empty(); if (reader.SidebarController.getActivePanel() == "Search") { @@ -616,14 +557,24 @@ PDFJS.reader.SearchController = function () { } }; - var highlightQuery = function(e) { - $("#text_left").contents().highlight(this.state.query, { element: 'span' }); - $("#text_right").contents().highlight(this.state.query, { element: 'span' }); + // initialize search + reset(); + + if (reader.settings.preloadTextcontent) { + reader.firstPagePromise.then(function() { + setTimeout(function() { + extractText(); + }, 5000); + }); + } + + var onShow = function() { + $searchView.addClass("open"); + $searchBox.focus(); }; - var unhighlight = function(e) { - $("#text_left").unhighlight(); - $("#text_right").unhighlight(); + var onHide = function() { + $searchView.removeClass("open"); }; @@ -632,7 +583,7 @@ PDFJS.reader.SearchController = function () { "hide": onHide, "search": search, "executeCommand": executeCommand, - "highlightQuery": highlightQuery, - "unhighlight": unhighlight + "nextMatch": nextMatch + }; }; diff --git a/files_reader/vendor/pdfjs/controllers/textlayer_controller.js b/files_reader/vendor/pdfjs/controllers/textlayer_controller.js index a8b7e6a..b0b175d 100644 --- a/files_reader/vendor/pdfjs/controllers/textlayer_controller.js +++ b/files_reader/vendor/pdfjs/controllers/textlayer_controller.js @@ -1,40 +1,8 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @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) { +PDFJS.Reader.TextLayerController = function (options, reader) { var EXPAND_DIVS_TIMEOUT = 300; // ms + this.reader = reader; this.textLayerDiv = options.textLayerDiv; this.eventBus = options.eventBus || null; this.textContent = null; @@ -44,7 +12,6 @@ PDFJS.Reader.TextLayerController = function (options) { this.matches = []; this.viewport = options.viewport; this.textDivs = []; - this.findController = options.findController || null; this.textLayerRenderTask = null; this.enhanceTextSelection = options.enhanceTextSelection; this._bindMouse(); @@ -117,13 +84,14 @@ PDFJS.Reader.TextLayerController.prototype.setTextContent = function (textConten PDFJS.Reader.TextLayerController.prototype.convertMatches = function(matches, matchesLength) { - var reader = this; + var reader = this.reader; var i = 0; var iIndex = 0; var bidiTexts = this.textContent.items; var end = bidiTexts.length - 1; - var queryLen = reader.search.query.length; + var queryLen = reader.search_state ? + reader.search_state.query.length : null; var ret = []; if (!matches) { return ret; @@ -179,18 +147,15 @@ PDFJS.Reader.TextLayerController.prototype.renderMatches = function (matches) { return; } - var reader = this; + var reader = this.reader; var bidiTexts = this.textContent.items; var textDivs = this.textDivs; var prevEnd = null; var pageIdx = this.pageIdx; - var isSelectedPage = (this.findController === null ? - false : (pageIdx === this.findController.selected.pageIdx)); - var selectedMatchIdx = (this.findController === null ? - -1 : this.findController.selected.matchIdx); - var highlightAll = (this.findController === null ? - false : this.findController.state.highlightAll); + var isSelectedPage = (pageIdx === reader.selected.pageIdx); + var selectedMatchIdx = reader.selected.matchIdx; + var highlightAll = true; var infinity = { divIdx: -1, offset: undefined @@ -230,12 +195,8 @@ PDFJS.Reader.TextLayerController.prototype.renderMatches = function (matches) { var begin = match.begin; var end = match.end; var isSelected = (isSelectedPage && i === selectedMatchIdx); - var highlightSuffix = (isSelected ? ' selected' : ''); - - if (this.findController) { - this.findController.updateMatchPosition(pageIdx, i, textDivs, - begin.divIdx); - } + var id = "match:" + pageIdx + ":" + i; + var highlightSuffix = (isSelected ? ' selected ' + id : ' ' + id); // Match inside new div. if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { @@ -274,6 +235,13 @@ PDFJS.Reader.TextLayerController.prototype.updateMatches = function () { return; } + var reader = this.reader; + + // Only show matches when search is active + if (reader.search_active !== true) { + return; + } + // Clear all matches. var matches = this.matches; var textDivs = this.textDivs; @@ -292,18 +260,12 @@ PDFJS.Reader.TextLayerController.prototype.updateMatches = function () { clearedUntilDivIdx = match.end.divIdx + 1; } - if (this.findController === null || !this.findController.active) { - return; - } - // Convert the matches on the page controller into the match format // used for the textLayer. - var pageMatches, pageMatchesLength; - if (this.findController !== null) { - pageMatches = this.findController.pageMatches[this.pageIdx] || null; - pageMatchesLength = (this.findController.pageMatchesLength) ? - this.findController.pageMatchesLength[this.pageIdx] || null : null; - } + var pageMatches = reader.pageMatches[this.pageIdx] || null, + pageMatchesLength = reader.pageMatchesLength ? + reader.pageMatchesLength[this.pageIdx] || null : null; + this.matches = this.convertMatches(pageMatches, pageMatchesLength); this.renderMatches(this.matches); diff --git a/files_reader/vendor/pdfjs/controllers/toc_controller.js b/files_reader/vendor/pdfjs/controllers/toc_controller.js index 89df621..1f50595 100644 --- a/files_reader/vendor/pdfjs/controllers/toc_controller.js +++ b/files_reader/vendor/pdfjs/controllers/toc_controller.js @@ -120,7 +120,7 @@ PDFJS.reader.TocController = function() { scale = parseFloat(settings.thumbnailWidth / width); preloadcount = parseInt(window.innerHeight / placeholder_height) + 2; if (preloadcount > settings.numPages) - preloadcount = numPages; + preloadcount = settings.numPages; var _timeout = setTimeout(function () { for (var i = 1; i <= preloadcount; i++) { diff --git a/files_reader/vendor/pdfjs/css/main.css b/files_reader/vendor/pdfjs/css/main.css index df97916..0a98133 100644 --- a/files_reader/vendor/pdfjs/css/main.css +++ b/files_reader/vendor/pdfjs/css/main.css @@ -125,6 +125,15 @@ body { left: 0; } +#status_message_left { + margin-left: 1em; +} + +#status_message_right, +#match_count { + margin-right: 1em; +} + #metainfo { position: fixed; /* width: 80%; diff --git a/files_reader/vendor/pdfjs/css/sidebar.css b/files_reader/vendor/pdfjs/css/sidebar.css index 7232f36..0628315 100644 --- a/files_reader/vendor/pdfjs/css/sidebar.css +++ b/files_reader/vendor/pdfjs/css/sidebar.css @@ -122,6 +122,10 @@ display: block; } +.list_item.selected a { + color: #D2D2D2; +} + legend { margin-left: 1em; padding: 0.5em; @@ -329,6 +333,20 @@ legend { 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 { width: 80%; float: left; diff --git a/files_reader/vendor/pdfjs/pdf.reader.js b/files_reader/vendor/pdfjs/pdf.reader.js index 68d75ed..1ebcf75 100644 --- a/files_reader/vendor/pdfjs/pdf.reader.js +++ b/files_reader/vendor/pdfjs/pdf.reader.js @@ -37,6 +37,7 @@ PDFJS.Reader = function(bookPath, _options) { bookPath: bookPath, textRenderDelay: TEXT_RENDER_DELAY, pageRenderDelay: PAGE_RENDER_DELAY, + preloadTextcontent: true, // true || false, preload text content to speed up first full-text search operation canvasLimit: 0, cssZoomOnly: false, // true || false, only zoom using CSS, render document at 100% size textSelect: true, // true || false, add selectable text layer @@ -70,9 +71,13 @@ PDFJS.Reader = function(bookPath, _options) { 83: 'toggleSidebar',// s 84: 'toggleTitlebar', // t 68: 'toggleDay', // d - 78: 'toggleNight', // n + //78: 'toggleNight', // n + 55: 'search', // '/' + 80: 'previousMatch', // p + 78: 'nextMatch', // n 70: 'toggleFullscreen', // f - 27: 'closeSidebar' // esc + 27: 'closeSidebar', // esc + 114: 'nextMatch' // F3 }, nightMode: false, dayMode: false, @@ -129,8 +134,17 @@ PDFJS.Reader = function(bookPath, _options) { this.renderQueue = false; - // used for search + // used for search, textlayer, hightlight etc + this.pageContents = []; 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 this.zoomCycle = { @@ -184,7 +198,7 @@ PDFJS.Reader = function(bookPath, _options) { function(_book) { reader.book = book = _book; - console.log(book); + //console.log(book); reader.settings.numPages = reader.book.numPages; document.getElementById('total_pages').textContent = reader.settings.numPages; if(!$.isEmptyObject(reader.settings.session.cursor)) { @@ -439,6 +453,8 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) { reader.resourcelst[1].canvas.style.display = "none"; // clear text layer 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 // to indicate the empty left page when oddPageRight === true) @@ -480,7 +496,6 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) { //console.log(page); page_rotation = page.rotate; rotation = (page_rotation + reader.settings.rotation) % 360; - //initial_viewport = page.getViewport({scale: 1, rotation: rotation}); initial_viewport = page.getViewport(1, rotation); page_width = initial_viewport.width; page_height = initial_viewport.height; @@ -491,21 +506,6 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) { scale_height = parseFloat(max_view_height / page_height); 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) { case "spread": @@ -604,10 +604,10 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) { page.getTextContent({ normalizeWhitespace: true }).then(function (textContent) { resourcelst.textLayer = textLayer = new PDFJS.Reader.TextLayerController({ textLayerDiv: textdiv, - pageIdx: pageNum - 1, + pageIndex: pageNum - 1, viewport: viewport, enhanceTextSelection: true - }); + }, reader); textLayer.setTextContent(textContent); }); } else { @@ -661,6 +661,8 @@ PDFJS.Reader.prototype.renderPage = function(pageNum) { canvas.style.width = reader.roundToDivide(max_view_width, 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; }; +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; +}; +