5 // Created by Heberti Almeida on 06/05/15.
6 // Copyright (c) 2015 Folio Reader. All rights reserved.
11 var wordsPerMinute = 180;
13 document.addEventListener("DOMContentLoaded", function(event) {
14 // var lnk = document.getElementsByClassName("lnk");
15 // for (var i=0; i<lnk.length; i++) {
16 // lnk[i].setAttribute("onclick","return callVerseURL(this);");
23 return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
25 var guid = s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
26 return guid.toUpperCase();
31 return document.documentElement.outerHTML;
35 function hasClass(ele,cls) {
36 return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
39 function addClass(ele,cls) {
40 if (!hasClass(ele,cls)) ele.className += " "+cls;
43 function removeClass(ele,cls) {
44 if (hasClass(ele,cls)) {
45 var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
46 ele.className=ele.className.replace(reg,' ');
51 function setFontName(cls) {
52 var elm = document.documentElement;
53 removeClass(elm, "andada");
54 removeClass(elm, "lato");
55 removeClass(elm, "lora");
56 removeClass(elm, "raleway");
61 function nightMode(enable) {
62 var elm = document.documentElement;
64 addClass(elm, "nightMode");
66 removeClass(elm, "nightMode");
71 function setFontSize(cls) {
72 var elm = document.documentElement;
73 removeClass(elm, "textSizeOne");
74 removeClass(elm, "textSizeTwo");
75 removeClass(elm, "textSizeThree");
76 removeClass(elm, "textSizeFour");
77 removeClass(elm, "textSizeFive");
82 function setMarginSize(cls) {
83 var elm = document.documentElement;
84 removeClass(elm, "marginSizeOne");
85 removeClass(elm, "marginSizeTwo");
86 removeClass(elm, "marginSizeThree");
87 removeClass(elm, "marginSizeFour");
88 removeClass(elm, "marginSizeFive");
93 function setInterlineSize(cls) {
94 var elm = document.documentElement;
95 removeClass(elm, "interlineSizeOne");
96 removeClass(elm, "interlineSizeTwo");
97 removeClass(elm, "interlineSizeThree");
98 removeClass(elm, "interlineSizeFour");
99 removeClass(elm, "interlineSizeFive");
104 * Native bridge Highlight text
106 function highlightString(style) {
107 var range = window.getSelection().getRangeAt(0);
108 var startOffset = range.startOffset;
109 var endOffset = range.endOffset;
110 var selectionContents = range.extractContents();
111 var elm = document.createElement("highlight");
114 elm.appendChild(selectionContents);
115 elm.setAttribute("id", id);
116 elm.setAttribute("onclick","callHighlightURL(this);");
117 elm.setAttribute("class", style);
119 range.insertNode(elm);
123 params.push({id: id, rect: getRectForSelectedText(elm), startOffset: startOffset.toString(), endOffset: endOffset.toString()});
125 return JSON.stringify(params);
129 function setHighlightStyle(style) {
130 thisHighlight.className = style;
131 return thisHighlight.id;
134 function removeThisHighlight() {
135 thisHighlight.outerHTML = thisHighlight.innerHTML;
136 return thisHighlight.id;
139 function removeHighlightById(elmId) {
140 var elm = document.getElementById(elmId);
141 elm.outerHTML = elm.innerHTML;
145 function getHighlightContent() {
146 return thisHighlight.textContent
149 function getBodyText() {
150 return document.body.innerText;
153 // Method that returns only selected text plain
154 var getSelectedText = function() {
155 return window.getSelection().toString();
158 // Method that gets the Rect of current selected text
159 // and returns in a JSON format
160 var getRectForSelectedText = function(elm) {
161 if (typeof elm === "undefined") elm = window.getSelection().getRangeAt(0);
163 var rect = elm.getBoundingClientRect();
164 return "{{" + rect.left + "," + rect.top + "}, {" + rect.width + "," + rect.height + "}}";
167 // Method that call that a hightlight was clicked
168 // with URL scheme and rect informations
169 var callHighlightURL = function(elm) {
170 event.stopPropagation();
171 var URLBase = "highlight://";
172 var currentHighlightRect = getRectForSelectedText(elm);
175 window.location = URLBase + encodeURIComponent(currentHighlightRect);
179 function getReadingTime() {
180 var text = document.body.innerText;
181 var totalWords = text.trim().split(/\s+/g).length;
182 var wordsPerSecond = wordsPerMinute / 60; //define words per second based on words per minute
183 var totalReadingTimeSeconds = totalWords / wordsPerSecond; //define total reading time in seconds
184 var readingTimeMinutes = Math.round(totalReadingTimeSeconds / 60);
186 return readingTimeMinutes;
190 Get Vertical or Horizontal paged #anchor offset
192 var getAnchorOffset = function(target, horizontal) {
193 var elem = document.getElementById(target);
196 elem = document.getElementsByName(target)[0];
200 return window.innerWidth * Math.floor(elem.offsetTop / window.innerHeight);
203 return elem.offsetTop;
206 function findElementWithID(node) {
207 if( !node || node.tagName == "BODY")
212 return findElementWithID(node)
215 function findElementWithIDInView() {
218 // attempt to find an existing "audio mark"
219 var el = document.querySelector("."+audioMarkClass)
221 // if that existing audio mark exists and is in view, use it
222 if( el && el.offsetTop > document.body.scrollTop && el.offsetTop < (window.innerHeight + document.body.scrollTop))
226 // @NOTE: is `span` too limiting?
227 var els = document.querySelectorAll("span[id]")
230 var element = els[indx];
233 if (document.body.scrollTop == 0) {
234 var elLeft = window.innerWidth * Math.floor(element.offsetTop / window.innerHeight);
235 // document.body.scrollLeft = elLeft;
237 if (elLeft == document.body.scrollLeft) {
242 } else if(element.offsetTop > document.body.scrollTop) {
252 Play Audio - called by native UIMenuController when a user selects a bit of text and presses "play"
254 function playAudio() {
255 var sel = getSelection();
258 // user selected text? start playing from the selected node
259 if (sel.toString() != "") {
260 node = sel.anchorNode ? findElementWithID(sel.anchorNode.parentNode) : null;
262 // find the first ID'd element that is within view (it will
264 node = findElementWithIDInView()
267 playAudioFragmentID(node ? node.id : null)
272 Play Audio Fragment ID - tells page controller to begin playing audio from the following ID
274 function playAudioFragmentID(fragmentID) {
275 var URLBase = "play-audio://";
276 window.location = URLBase + (fragmentID?encodeURIComponent(fragmentID):"")
280 Go To Element - scrolls the webview to the requested element
282 function goToEl(el) {
283 var top = document.body.scrollTop;
284 var elTop = el.offsetTop - 20;
285 var bottom = window.innerHeight + document.body.scrollTop;
286 var elBottom = el.offsetHeight + el.offsetTop + 60
288 if(elBottom > bottom || elTop < top) {
289 document.body.scrollTop = el.offsetTop - 20
292 /* Set scroll left in case horz scroll is activated.
294 The following works because el.offsetTop accounts for each page turned
295 as if the document was scrolling vertical. We then divide by the window
296 height to figure out what page the element should appear on and set scroll left
297 to scroll to that page.
299 if( document.body.scrollTop == 0 ){
300 var elLeft = window.innerWidth * Math.floor(el.offsetTop / window.innerHeight);
301 document.body.scrollLeft = elLeft;
308 Remove All Classes - removes the given class from all elements in the DOM
310 function removeAllClasses(className) {
311 var els = document.body.getElementsByClassName(className)
313 for( i = 0; i <= els.length; i++) {
314 els[i].classList.remove(className);
319 Audio Mark ID - marks an element with an ID with the given class and scrolls to it
321 function audioMarkID(className, id) {
323 removeAllClasses(audioMarkClass);
325 audioMarkClass = className
326 var el = document.getElementById(id);
329 el.classList.add(className)
332 function setMediaOverlayStyle(style){
333 document.documentElement.classList.remove("mediaOverlayStyle0", "mediaOverlayStyle1", "mediaOverlayStyle2")
334 document.documentElement.classList.add(style)
337 function setMediaOverlayStyleColors(color, colorHighlight) {
338 var stylesheet = document.styleSheets[document.styleSheets.length-1];
339 stylesheet.insertRule(".mediaOverlayStyle0 span.epub-media-overlay-playing { background: "+colorHighlight+" !important }")
340 stylesheet.insertRule(".mediaOverlayStyle1 span.epub-media-overlay-playing { border-color: "+color+" !important }")
341 stylesheet.insertRule(".mediaOverlayStyle2 span.epub-media-overlay-playing { color: "+color+" !important }")
344 var currentIndex = -1;
347 function findSentenceWithIDInView(els) {
348 // @NOTE: is `span` too limiting?
350 var element = els[indx];
353 if (document.body.scrollTop == 0) {
354 var elLeft = window.innerWidth * Math.floor(element.offsetTop / window.innerHeight);
355 // document.body.scrollLeft = elLeft;
357 if (elLeft == document.body.scrollLeft) {
363 } else if(element.offsetTop > document.body.scrollTop) {
372 function findNextSentenceInArray(els) {
373 if(currentIndex >= 0) {
375 return els[currentIndex];
381 function resetCurrentSentenceIndex() {
385 function getSentenceWithIndex(className) {
387 var sel = getSelection();
389 var elements = document.querySelectorAll("span.sentence");
391 // Check for a selected text, if found start reading from it
392 if (sel.toString() != "") {
393 console.log(sel.anchorNode.parentNode);
394 node = sel.anchorNode.parentNode;
396 if (node.className == "sentence") {
399 for(var i = 0, len = elements.length; i < len; i++) {
400 if (elements[i] === sentence) {
406 sentence = findSentenceWithIDInView(elements);
408 } else if (currentIndex < 0) {
409 sentence = findSentenceWithIDInView(elements);
411 sentence = findNextSentenceInArray(elements);
414 var text = sentence.innerText || sentence.textContent;
419 removeAllClasses(audioMarkClass);
422 audioMarkClass = className;
423 sentence.classList.add(className)
427 function wrappingSentencesWithinPTags(){
431 var rxOpen = new RegExp("<[^\\/].+?>"),
432 rxClose = new RegExp("<\\/.+?>"),
433 rxSupStart = new RegExp("^<sup\\b[^>]*>"),
434 rxSupEnd = new RegExp("<\/sup>"),
438 sentenceEnd.push(new RegExp("[^\\d][\\.!\\?]+"));
439 sentenceEnd.push(new RegExp("(?=([^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*?$)"));
440 sentenceEnd.push(new RegExp("(?![^\\(]*?\\))"));
441 sentenceEnd.push(new RegExp("(?![^\\[]*?\\])"));
442 sentenceEnd.push(new RegExp("(?![^\\{]*?\\})"));
443 sentenceEnd.push(new RegExp("(?![^\\|]*?\\|)"));
444 sentenceEnd.push(new RegExp("(?![^\\\\]*?\\\\)"));
445 //sentenceEnd.push(new RegExp("(?![^\\/.]*\\/)")); // all could be a problem, but this one is problematic
447 rxIndex = new RegExp(sentenceEnd.reduce(function (previousValue, currentValue) {
448 return previousValue + currentValue.source;
451 function indexSentenceEnd(html) {
452 var index = html.search(rxIndex);
455 index += html.match(rxIndex)[0].length - 1;
461 function pushSpan(array, className, string, classNameOpt) {
462 if (!string.match('[a-zA-Z0-9]+')) {
465 array.push('<span class="' + className + '">' + string + '</span>');
469 function addSupToPrevious(html, array) {
470 var sup = html.search(rxSupStart),
475 end = html.search(rxSupEnd);
479 array.push(last.slice(0, -7) + html.slice(0, end) + last.slice(-7));
483 return html.slice(end);
486 function paragraphIsSentence(html, array) {
487 var index = indexSentenceEnd(html);
489 if (index === -1 || index === html.length) {
490 pushSpan(array, "sentence", html, "paragraphIsSentence");
497 function paragraphNoMarkup(html, array) {
498 var open = html.search(rxOpen),
502 index = indexSentenceEnd(html);
507 pushSpan(array, "sentence", html.slice(0, index += 1), "paragraphNoMarkup");
510 return html.slice(index);
513 function sentenceUncontained(html, array) {
514 var open = html.search(rxOpen),
519 index = indexSentenceEnd(html);
524 close = html.search(rxClose);
525 if (index < open || index > close) {
526 pushSpan(array, "sentence", html.slice(0, index += 1), "sentenceUncontained");
532 return html.slice(index);
535 function sentenceContained(html, array) {
536 var open = html.search(rxOpen),
542 index = indexSentenceEnd(html);
547 close = html.search(rxClose);
548 if (index > open && index < close) {
549 count = html.match(rxClose)[0].length;
550 pushSpan(array, "sentence", html.slice(0, close + count), "sentenceContained");
551 index = close + count;
557 return html.slice(index);
560 function anythingElse(html, array) {
561 pushSpan(array, "sentence", html, "anythingElse");
566 function guessSenetences() {
567 var paragraphs = document.getElementsByTagName("p");
569 Array.prototype.forEach.call(paragraphs, function (paragraph) {
570 var html = paragraph.innerHTML,
571 length = html.length,
575 while (length && safety) {
576 html = addSupToPrevious(html, array);
577 if (html.length === length) {
578 if (html.length === length) {
579 html = paragraphIsSentence(html, array);
580 if (html.length === length) {
581 html = paragraphNoMarkup(html, array);
582 if (html.length === length) {
583 html = sentenceUncontained(html, array);
584 if (html.length === length) {
585 html = sentenceContained(html, array);
586 if (html.length === length) {
587 html = anythingElse(html, array);
595 length = html.length;
599 paragraph.innerHTML = array.join("");
606 // Class based onClick listener
608 function addClassBasedOnClickListener(schemeName, querySelector, attributeName, selectAll) {
610 // Get all elements with the given query selector
611 var elements = document.querySelectorAll(querySelector);
612 for (elementIndex = 0; elementIndex < elements.length; elementIndex++) {
613 var element = elements[elementIndex];
614 addClassBasedOnClickListenerToElement(element, schemeName, attributeName);
617 // Get the first element with the given query selector
618 var element = document.querySelector(querySelector);
619 addClassBasedOnClickListenerToElement(element, schemeName, attributeName);
623 function addClassBasedOnClickListenerToElement(element, schemeName, attributeName) {
624 // Get the content from the given attribute name
625 var attributeContent = element.getAttribute(attributeName);
626 // Add the on click logic
627 element.setAttribute("onclick", "onClassBasedListenerClick(\"" + schemeName + "\", \"" + encodeURIComponent(attributeContent) + "\");");
630 var onClassBasedListenerClick = function(schemeName, attributeContent) {
631 // Prevent the browser from performing the default on click behavior
632 event.preventDefault();
633 // Don't pass the click event to other elemtents
634 event.stopPropagation();
635 // Create parameters containing the click position inside the web view.
636 var positionParameterString = "/clientX=" + event.clientX + "&clientY=" + event.clientY;
637 // Set the custom link URL to the event
638 window.location = schemeName + "://" + attributeContent + positionParameterString;