5 // Created by Heberti Almeida on 06/05/15.
\r
6 // Copyright (c) 2015 Folio Reader. All rights reserved.
\r
11 var wordsPerMinute = 180;
\r
13 document.addEventListener("DOMContentLoaded", function(event) {
\r
14 // var lnk = document.getElementsByClassName("lnk");
\r
15 // for (var i=0; i<lnk.length; i++) {
\r
16 // lnk[i].setAttribute("onclick","return callVerseURL(this);");
\r
23 return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
\r
25 var guid = s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
\r
26 return guid.toUpperCase();
\r
30 function getHTML() {
\r
31 Highlight.getHtmlAndSaveHighlight(document.documentElement.outerHTML);
\r
32 //return document.documentElement.outerHTML;
\r
35 // Class manipulation
\r
36 function hasClass(ele,cls) {
\r
37 return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
\r
40 function addClass(ele,cls) {
\r
41 if (!hasClass(ele,cls)) ele.className += " "+cls;
\r
44 function removeClass(ele,cls) {
\r
45 if (hasClass(ele,cls)) {
\r
46 var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
\r
47 ele.className=ele.className.replace(reg,' ');
\r
52 function setFontName(cls) {
\r
53 var elm = document.documentElement;
\r
54 removeClass(elm, "andada");
\r
55 removeClass(elm, "lato");
\r
56 removeClass(elm, "lora");
\r
57 removeClass(elm, "raleway");
\r
61 // Toggle night mode
\r
62 function nightMode(enable) {
\r
63 var elm = document.documentElement;
\r
65 addClass(elm, "nightMode");
\r
67 removeClass(elm, "nightMode");
\r
72 function setFontSize(cls) {
\r
73 var elm = document.documentElement;
\r
74 removeClass(elm, "textSizeOne");
\r
75 removeClass(elm, "textSizeTwo");
\r
76 removeClass(elm, "textSizeThree");
\r
77 removeClass(elm, "textSizeFour");
\r
78 removeClass(elm, "textSizeFive");
\r
84 function setHighlightStyle(style) {
\r
85 Highlight.getUpdatedHighlightId(thisHighlight.id, style);
\r
88 function removeThisHighlight() {
\r
89 return thisHighlight.id;
\r
92 function removeHighlightById(elmId) {
\r
93 var elm = document.getElementById(elmId);
\r
94 elm.outerHTML = elm.innerHTML;
\r
98 function getHighlightContent() {
\r
99 return thisHighlight.textContent
\r
102 function getBodyText() {
\r
103 return document.body.innerText;
\r
106 // Method that returns only selected text plain
\r
107 var getSelectedText = function() {
\r
108 return window.getSelection().toString();
\r
111 // Method that gets the Rect of current selected text
\r
112 // and returns in a JSON format
\r
113 var getRectForSelectedText = function(elm) {
\r
114 if (typeof elm === "undefined") elm = window.getSelection().getRangeAt(0);
\r
116 var rect = elm.getBoundingClientRect();
\r
117 return "{{" + rect.left + "," + rect.top + "}, {" + rect.width + "," + rect.height + "}}";
\r
120 // Method that call that a hightlight was clicked
\r
121 // with URL scheme and rect informations
\r
122 var callHighlightURL = function(elm) {
\r
123 event.stopPropagation();
\r
124 var URLBase = "highlight://";
\r
125 var currentHighlightRect = getRectForSelectedText(elm);
\r
126 thisHighlight = elm;
\r
128 window.location = URLBase + encodeURIComponent(currentHighlightRect);
\r
132 function getReadingTime() {
\r
133 var text = document.body.innerText;
\r
134 var totalWords = text.trim().split(/\s+/g).length;
\r
135 var wordsPerSecond = wordsPerMinute / 60; //define words per second based on words per minute
\r
136 var totalReadingTimeSeconds = totalWords / wordsPerSecond; //define total reading time in seconds
\r
137 var readingTimeMinutes = Math.round(totalReadingTimeSeconds / 60);
\r
139 return readingTimeMinutes;
\r
143 Get Vertical or Horizontal paged #anchor offset
\r
145 var getAnchorOffset = function(target, horizontal) {
\r
146 var elem = document.getElementById(target);
\r
149 elem = document.getElementsByName(target)[0];
\r
153 return document.body.clientWidth * Math.floor(elem.offsetTop / window.innerHeight);
\r
156 return elem.offsetTop;
\r
159 function scrollAnchor(id) {
\r
160 window.location.hash = id;
\r
163 function findElementWithID(node) {
\r
164 if( !node || node.tagName == "BODY")
\r
169 return findElementWithID(node)
\r
172 function findElementWithIDInView() {
\r
174 if(audioMarkClass) {
\r
175 // attempt to find an existing "audio mark"
\r
176 var el = document.querySelector("."+audioMarkClass)
\r
178 // if that existing audio mark exists and is in view, use it
\r
179 if( el && el.offsetTop > document.body.scrollTop && el.offsetTop < (window.innerHeight + document.body.scrollTop))
\r
183 // @NOTE: is `span` too limiting?
\r
184 var els = document.querySelectorAll("span[id]")
\r
187 var element = els[indx];
\r
189 // Horizontal scroll
\r
190 if (document.body.scrollTop == 0) {
\r
191 var elLeft = document.body.clientWidth * Math.floor(element.offsetTop / window.innerHeight);
\r
192 // document.body.scrollLeft = elLeft;
\r
194 if (elLeft == document.body.scrollLeft) {
\r
199 } else if(element.offsetTop > document.body.scrollTop) {
\r
209 Play Audio - called by native UIMenuController when a user selects a bit of text and presses "play"
\r
211 function playAudio() {
\r
212 var sel = getSelection();
\r
215 // user selected text? start playing from the selected node
\r
216 if (sel.toString() != "") {
\r
217 node = sel.anchorNode ? findElementWithID(sel.anchorNode.parentNode) : null;
\r
219 // find the first ID'd element that is within view (it will
\r
221 node = findElementWithIDInView()
\r
224 playAudioFragmentID(node ? node.id : null)
\r
229 Play Audio Fragment ID - tells page controller to begin playing audio from the following ID
\r
231 function playAudioFragmentID(fragmentID) {
\r
232 var URLBase = "play-audio://";
\r
233 window.location = URLBase + (fragmentID?encodeURIComponent(fragmentID):"")
\r
237 Go To Element - scrolls the webview to the requested element
\r
239 function goToEl(el) {
\r
240 var top = document.body.scrollTop;
\r
241 var elTop = el.offsetTop - 20;
\r
242 var bottom = window.innerHeight + document.body.scrollTop;
\r
243 var elBottom = el.offsetHeight + el.offsetTop + 60
\r
245 if(elBottom > bottom || elTop < top) {
\r
246 document.body.scrollTop = el.offsetTop - 20
\r
249 /* Set scroll left in case horz scroll is activated.
\r
251 The following works because el.offsetTop accounts for each page turned
\r
252 as if the document was scrolling vertical. We then divide by the window
\r
253 height to figure out what page the element should appear on and set scroll left
\r
254 to scroll to that page.
\r
256 if( document.body.scrollTop == 0 ){
\r
257 var elLeft = document.body.clientWidth * Math.floor(el.offsetTop / window.innerHeight);
\r
258 document.body.scrollLeft = elLeft;
\r
265 Remove All Classes - removes the given class from all elements in the DOM
\r
267 function removeAllClasses(className) {
\r
268 var els = document.body.getElementsByClassName(className)
\r
269 if( els.length > 0 )
\r
270 for( i = 0; i <= els.length; i++) {
\r
271 els[i].classList.remove(className);
\r
276 Audio Mark ID - marks an element with an ID with the given class and scrolls to it
\r
278 function audioMarkID(className, id) {
\r
279 if (audioMarkClass)
\r
280 removeAllClasses(audioMarkClass);
\r
282 audioMarkClass = className
\r
283 var el = document.getElementById(id);
\r
286 el.classList.add(className)
\r
289 function setMediaOverlayStyle(style){
\r
290 document.documentElement.classList.remove("mediaOverlayStyle0", "mediaOverlayStyle1", "mediaOverlayStyle2")
\r
291 document.documentElement.classList.add(style)
\r
294 function setMediaOverlayStyleColors(color, colorHighlight) {
\r
295 var stylesheet = document.styleSheets[document.styleSheets.length-1];
\r
296 stylesheet.insertRule(".mediaOverlayStyle0 span.epub-media-overlay-playing { background: "+colorHighlight+" !important }")
\r
297 stylesheet.insertRule(".mediaOverlayStyle1 span.epub-media-overlay-playing { border-color: "+color+" !important }")
\r
298 stylesheet.insertRule(".mediaOverlayStyle2 span.epub-media-overlay-playing { color: "+color+" !important }")
\r
301 var currentIndex = -1;
\r
304 function findSentenceWithIDInView(els) {
\r
305 // @NOTE: is `span` too limiting?
\r
307 var element = els[indx];
\r
309 // Horizontal scroll
\r
310 if (document.body.scrollTop == 0) {
\r
311 var elLeft = document.body.clientWidth * Math.floor(element.offsetTop / window.innerHeight);
\r
312 // document.body.scrollLeft = elLeft;
\r
314 if (elLeft == document.body.scrollLeft) {
\r
315 currentIndex = indx;
\r
320 } else if(element.offsetTop > document.body.scrollTop) {
\r
321 currentIndex = indx;
\r
329 function findNextSentenceInArray(els) {
\r
330 if(currentIndex >= 0) {
\r
332 return els[currentIndex];
\r
338 function resetCurrentSentenceIndex() {
\r
342 function rewindCurrentIndex() {
\r
343 currentIndex = currentIndex-1;
\r
346 function getSentenceWithIndex(className) {
\r
348 var sel = getSelection();
\r
350 var elements = document.querySelectorAll("span.sentence");
\r
352 // Check for a selected text, if found start reading from it
\r
353 if (sel.toString() != "") {
\r
354 console.log(sel.anchorNode.parentNode);
\r
355 node = sel.anchorNode.parentNode;
\r
357 if (node.className == "sentence") {
\r
360 for(var i = 0, len = elements.length; i < len; i++) {
\r
361 if (elements[i] === sentence) {
\r
367 sentence = findSentenceWithIDInView(elements);
\r
369 } else if (currentIndex < 0) {
\r
370 sentence = findSentenceWithIDInView(elements);
\r
372 sentence = findNextSentenceInArray(elements);
\r
375 var text = sentence.innerText || sentence.textContent;
\r
379 if (audioMarkClass){
\r
380 removeAllClasses(audioMarkClass);
\r
383 audioMarkClass = className;
\r
384 sentence.classList.add(className)
\r
388 function wrappingSentencesWithinPTags(){
\r
392 var rxOpen = new RegExp("<[^\\/].+?>"),
\r
393 rxClose = new RegExp("<\\/.+?>"),
\r
394 rxSupStart = new RegExp("^<sup\\b[^>]*>"),
\r
395 rxSupEnd = new RegExp("<\/sup>"),
\r
399 sentenceEnd.push(new RegExp("[^\\d][\\.!\\?]+"));
\r
400 sentenceEnd.push(new RegExp("(?=([^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*?$)"));
\r
401 sentenceEnd.push(new RegExp("(?![^\\(]*?\\))"));
\r
402 sentenceEnd.push(new RegExp("(?![^\\[]*?\\])"));
\r
403 sentenceEnd.push(new RegExp("(?![^\\{]*?\\})"));
\r
404 sentenceEnd.push(new RegExp("(?![^\\|]*?\\|)"));
\r
405 sentenceEnd.push(new RegExp("(?![^\\\\]*?\\\\)"));
\r
406 //sentenceEnd.push(new RegExp("(?![^\\/.]*\\/)")); // all could be a problem, but this one is problematic
\r
408 rxIndex = new RegExp(sentenceEnd.reduce(function (previousValue, currentValue) {
\r
409 return previousValue + currentValue.source;
\r
412 function indexSentenceEnd(html) {
\r
413 var index = html.search(rxIndex);
\r
415 if (index !== -1) {
\r
416 index += html.match(rxIndex)[0].length - 1;
\r
422 function pushSpan(array, className, string, classNameOpt) {
\r
423 if (!string.match('[a-zA-Z0-9]+')) {
\r
424 array.push(string);
\r
426 array.push('<span class="' + className + '">' + string + '</span>');
\r
430 function addSupToPrevious(html, array) {
\r
431 var sup = html.search(rxSupStart),
\r
436 end = html.search(rxSupEnd);
\r
438 last = array.pop();
\r
440 array.push(last.slice(0, -7) + html.slice(0, end) + last.slice(-7));
\r
444 return html.slice(end);
\r
447 function paragraphIsSentence(html, array) {
\r
448 var index = indexSentenceEnd(html);
\r
450 if (index === -1 || index === html.length) {
\r
451 pushSpan(array, "sentence", html, "paragraphIsSentence");
\r
458 function paragraphNoMarkup(html, array) {
\r
459 var open = html.search(rxOpen),
\r
463 index = indexSentenceEnd(html);
\r
464 if (index === -1) {
\r
465 index = html.length;
\r
468 pushSpan(array, "sentence", html.slice(0, index += 1), "paragraphNoMarkup");
\r
471 return html.slice(index);
\r
474 function sentenceUncontained(html, array) {
\r
475 var open = html.search(rxOpen),
\r
480 index = indexSentenceEnd(html);
\r
481 if (index === -1) {
\r
482 index = html.length;
\r
485 close = html.search(rxClose);
\r
486 if (index < open || index > close) {
\r
487 pushSpan(array, "sentence", html.slice(0, index += 1), "sentenceUncontained");
\r
493 return html.slice(index);
\r
496 function sentenceContained(html, array) {
\r
497 var open = html.search(rxOpen),
\r
503 index = indexSentenceEnd(html);
\r
504 if (index === -1) {
\r
505 index = html.length;
\r
508 close = html.search(rxClose);
\r
509 if (index > open && index < close) {
\r
510 count = html.match(rxClose)[0].length;
\r
511 pushSpan(array, "sentence", html.slice(0, close + count), "sentenceContained");
\r
512 index = close + count;
\r
518 return html.slice(index);
\r
521 function anythingElse(html, array) {
\r
522 pushSpan(array, "sentence", html, "anythingElse");
\r
527 function guessSenetences() {
\r
528 var paragraphs = document.getElementsByTagName("p");
\r
530 Array.prototype.forEach.call(paragraphs, function (paragraph) {
\r
531 var html = paragraph.innerHTML,
\r
532 length = html.length,
\r
536 while (length && safety) {
\r
537 html = addSupToPrevious(html, array);
\r
538 if (html.length === length) {
\r
539 if (html.length === length) {
\r
540 html = paragraphIsSentence(html, array);
\r
541 if (html.length === length) {
\r
542 html = paragraphNoMarkup(html, array);
\r
543 if (html.length === length) {
\r
544 html = sentenceUncontained(html, array);
\r
545 if (html.length === length) {
\r
546 html = sentenceContained(html, array);
\r
547 if (html.length === length) {
\r
548 html = anythingElse(html, array);
\r
556 length = html.length;
\r
560 paragraph.innerHTML = array.join("");
\r
567 // Class based onClick listener
\r
569 function addClassBasedOnClickListener(schemeName, querySelector, attributeName, selectAll) {
\r
571 // Get all elements with the given query selector
\r
572 var elements = document.querySelectorAll(querySelector);
\r
573 for (elementIndex = 0; elementIndex < elements.length; elementIndex++) {
\r
574 var element = elements[elementIndex];
\r
575 addClassBasedOnClickListenerToElement(element, schemeName, attributeName);
\r
578 // Get the first element with the given query selector
\r
579 var element = document.querySelector(querySelector);
\r
580 addClassBasedOnClickListenerToElement(element, schemeName, attributeName);
\r
584 function addClassBasedOnClickListenerToElement(element, schemeName, attributeName) {
\r
585 // Get the content from the given attribute name
\r
586 var attributeContent = element.getAttribute(attributeName);
\r
587 // Add the on click logic
\r
588 element.setAttribute("onclick", "onClassBasedListenerClick(\"" + schemeName + "\", \"" + encodeURIComponent(attributeContent) + "\");");
\r
591 var onClassBasedListenerClick = function(schemeName, attributeContent) {
\r
592 // Prevent the browser from performing the default on click behavior
\r
593 event.preventDefault();
\r
594 // Don't pass the click event to other elemtents
\r
595 event.stopPropagation();
\r
596 // Create parameters containing the click position inside the web view.
\r
597 var positionParameterString = "/clientX=" + event.clientX + "&clientY=" + event.clientY;
\r
598 // Set the custom link URL to the event
\r
599 window.location = schemeName + "://" + attributeContent + positionParameterString;
\r
602 function getHighlightString(style) {
\r
603 var range = window.getSelection().getRangeAt(0);
\r
604 var selectionContents = range.extractContents();
\r
605 var elm = document.createElement("highlight");
\r
608 elm.appendChild(selectionContents);
\r
609 elm.setAttribute("id", id);
\r
610 elm.setAttribute("onclick","callHighlightURL(this);");
\r
611 elm.setAttribute("class", style);
\r
613 range.insertNode(elm);
\r
614 thisHighlight = elm;
\r
617 params.push({id: id, rect: getRectForSelectedText(elm)});
\r
618 Highlight.getHighlightJson(JSON.stringify(params));
\r
621 function gotoHighlight(highlightId){
\r
622 var element = document.getElementById(highlightId.toString());
\r
623 if(element != null) {
\r
629 window.ssReader = Class({
\r
635 this.highlighter = rangy.createHighlighter();
\r
637 this.highlighter.addClassApplier(rangy.createClassApplier("highlight_yellow", {
\r
638 ignoreWhiteSpace: true,
\r
639 tagNames: ["span", "a"]
\r
642 this.highlighter.addClassApplier(rangy.createClassApplier("highlight_green", {
\r
643 ignoreWhiteSpace: true,
\r
644 tagNames: ["span", "a"]
\r
647 this.highlighter.addClassApplier(rangy.createClassApplier("highlight_blue", {
\r
648 ignoreWhiteSpace: true,
\r
649 tagNames: ["span", "a"]
\r
652 this.highlighter.addClassApplier(rangy.createClassApplier("highlight_pink", {
\r
653 ignoreWhiteSpace: true,
\r
654 tagNames: ["span", "a"]
\r
657 this.highlighter.addClassApplier(rangy.createClassApplier("highlight_underline", {
\r
658 ignoreWhiteSpace: true,
\r
659 tagNames: ["span", "a"]
\r
664 setFontAndada: function(){
\r
665 this.setFont("andada");
\r
668 setFontLato: function(){
\r
669 this.setFont("lato");
\r
672 setFontPtSerif: function(){
\r
673 this.setFont("pt-serif");
\r
676 setFontPtSans: function(){
\r
677 this.setFont("pt-sans");
\r
680 base64encode: function(str){
\r
681 return btoa(unescape(encodeURIComponent(str)));
\r
684 base64decode: function(str){
\r
685 return decodeURIComponent(escape(atob(str)));
\r
688 clearSelection: function(){
\r
689 if (window.getSelection) {
\r
690 if (window.getSelection().empty) { // Chrome
\r
691 window.getSelection().empty();
\r
692 } else if (window.getSelection().removeAllRanges) { // Firefox
\r
693 window.getSelection().removeAllRanges();
\r
695 } else if (document.selection) { // IE?
\r
696 document.selection.empty();
\r
702 setFont: function(fontName){
\r
703 $("#ss-wrapper-font").removeClass().addClass("ss-wrapper-"+fontName);
\r
706 setSize: function(size){
\r
707 $("#ss-wrapper-size").removeClass().addClass("ss-wrapper-"+size);
\r
710 setTheme: function(theme){
\r
711 $("body, #ss-wrapper-theme").removeClass().addClass("ss-wrapper-"+theme);
\r
714 setComment: function(comment, inputId){
\r
715 $("#"+inputId).val(ssReader.base64decode(comment));
\r
716 $("#"+inputId).trigger("input", ["true"]);
\r
719 highlightSelection: function(color){
\r
722 this.highlighter.highlightSelection("highlight_" + color, null);
\r
723 var range = window.getSelection().toString();
\r
724 var params = {content: range,rangy: this.getHighlights(),color: color};
\r
725 this.clearSelection();
\r
726 Highlight.onReceiveHighlights(JSON.stringify(params));
\r
728 console.log("highlightSelection : " + err);
\r
732 unHighlightSelection: function(){
\r
734 this.highlighter.unhighlightSelection();
\r
735 Highlight.onReceiveHighlights(this.getHighlights());
\r
739 getHighlights: function(){
\r
741 return this.highlighter.serialize();
\r
745 setHighlights: function(serializedHighlight){
\r
747 this.highlighter.removeAllHighlights();
\r
748 this.highlighter.deserialize(serializedHighlight);
\r
752 removeAll: function(){
\r
754 this.highlighter.removeAllHighlights();
\r
759 SSBridge.onCopy(window.getSelection().toString());
\r
760 this.clearSelection();
\r
764 SSBridge.onShare(window.getSelection().toString());
\r
765 this.clearSelection();
\r
768 search: function(){
\r
769 SSBridge.onSearch(window.getSelection().toString());
\r
770 this.clearSelection();
\r
774 if(typeof ssReader !== "undefined"){
\r
778 $(".verse").click(function(){
\r
779 SSBridge.onVerseClick(ssReader.base64encode($(this).attr("verse")));
\r
782 $("code").each(function(i){
\r
783 var textarea = $("<textarea class='textarea'/>").attr("id", "input-"+i).on("input propertychange", function(event, isInit) {
\r
784 $(this).css({'height': 'auto', 'overflow-y': 'hidden'}).height(this.scrollHeight);
\r
785 $(this).next().css({'height': 'auto', 'overflow-y': 'hidden'}).height(this.scrollHeight);
\r
789 if (timeout !== null) {
\r
790 clearTimeout(timeout);
\r
792 timeout = setTimeout(function () {
\r
793 SSBridge.onCommentsClick(
\r
794 ssReader.base64encode($(that).val()),
\r
800 var border = $("<div class='textarea-border' />");
\r
801 var container = $("<div class='textarea-container' />");
\r
803 $(textarea).appendTo(container);
\r
804 $(border).appendTo(container);
\r
806 $(this).after(container);
\r
810 function array_diff(array1, array2){
\r
811 var difference = $.grep(array1, function(el) { return $.inArray(el,array2) < 0});
\r
812 return difference.concat($.grep(array2, function(el) { return $.inArray(el,array1) < 0}));;
\r