Initial bookmarks.
[wolnelektury.git] / src / wolnelektury / static / js / book_text / references.js
1 (function($){$(function(){
2     let csrf = $("[name='csrfmiddlewaretoken']").val();
3
4     var interestingReferences = $("#interesting-references").text();
5     if (interestingReferences) {
6         interestingReferences = $.parseJSON(interestingReferences);
7     }
8     if (Object.keys(interestingReferences).length) {
9         $("#settings-references").css('display', 'block');
10     }
11
12     var map_enabled = false;
13     var marker = L.circleMarker([0,0]);
14     var map = null;
15
16     function enable_map() {
17         $("#reference-map").show('slow');
18
19         if (map_enabled) return;
20
21         map = L.map('reference-map').setView([0, 0], 11);
22         L.tileLayer('https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=a8a97f0ae5134403ac38c1a075b03e15', {
23             attribution: 'Maps © <a href="http://www.thunderforest.com">Thunderforest</a>, Data © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
24         }).addTo(map);
25
26         map_enabled = true;
27     }
28     function disable_map() {
29         $("#reference-map").hide('slow');
30     }
31     
32
33     $("#reference-close").on("click", function(event) {
34         event.preventDefault();
35         $("#reference-box").hide();
36     });
37     
38     $('a.reference').each(function() {
39         $this = $(this);
40         uri = $this.attr('data-uri');
41         if (uri == '') {
42             $this.remove();
43             return;
44         }
45         if (interestingReferences.hasOwnProperty(uri)) {
46             $this.addClass('interesting');
47             ref = interestingReferences[uri];
48
49             $this.attr('href', ref.wikipedia_link);
50             $this.attr('target', '_blank');
51         }
52     });
53
54
55     $('a.reference.interesting').on('click', function(event) {
56         event.preventDefault();
57
58         $("#reference-box").show();
59
60         $this = $(this);
61         uri = $this.attr('data-uri');
62         ref = interestingReferences[uri];
63
64         if (ref.location) {
65             enable_map();
66
67             let newLoc = [
68                 ref.location[0],
69                 ref.location[1] + Math.round(
70                     (map.getCenter().lng - ref.location[1]) / 360
71                 ) * 360
72             ];
73
74             marker.setLatLng(newLoc);
75             marker.bindTooltip(ref.label).openTooltip();
76             map.addLayer(marker);
77
78             map.panTo(newLoc, {
79                 animate: true,
80                 duration: 1,
81             });
82         } else {
83             disable_map();
84             if (map) {
85                 map.removeLayer(marker);
86             }
87         }
88
89         $("#reference-images a").remove();
90         if (ref.images) {
91             $.each(ref.images, function(i, e) {
92                 $i = $("<a target='_blank'><img></a>");
93                 $i.attr('href', e.page);
94                 $('img', $i).attr('src', e.thumburl || e.url);
95                 if (e.thumbresolution) {
96                     $('img', $i).attr('width', e.thumbresolution[0]).attr('height', e.thumbresolution[1]);
97                 }
98
99                 $("#reference-images").append($i);
100             })
101         }
102
103         $("#reference-link").text(ref.label);
104         $("#reference-link").attr('href', ref.wikipedia_link);
105
106         _paq.push(['trackEvent', 'html', 'reference']);
107     });
108
109
110     function putNoteAt($elem, anchor, side) {
111         $elem.data('anchoredTo', anchor);
112         updateNote($elem, side);
113     }
114
115     function updateNote($elem, side) {
116         anchor = $elem.data('anchoredTo')
117         if (!anchor) return;
118         let anchorRect = anchor.getBoundingClientRect();
119
120         let x = anchorRect.x + anchorRect.width / 2;
121         let y = anchorRect.y;
122         if ($elem.data('attach-bottom')) {
123             y += anchorRect.height;
124         }
125         minx = $("#book-text").position().left;
126         maxx = minx + $("#book-text").width();
127
128         margin = 20;
129         minx += margin;
130         maxx -= margin;
131         maxx += 10000;
132
133         //boxwidth = 470;
134         boxwidth = $elem.width();
135         
136         if (maxx - minx <= boxwidth) {
137             nx = margin;
138             right = margin;
139             leftoffset = x - margin;
140         } else {
141             right = '';
142         
143             // default position.
144             leftoffset = 40;
145             leftoffset = $elem.data('default-leftoffset');
146             
147             nx = x - leftoffset;
148
149             $elem.css({right: ''});
150
151             // Do we need to move away from the left?
152             if (nx < minx) {
153                 let d = minx - nx;
154                 nx += d;
155                 leftoffset -= d;
156             }
157
158             // Do we need to move away from the right?
159             if (nx + boxwidth > maxx) {
160                 // ACTUALLY CALCULATE STUFF
161                 // if maxx - minx < 470 px -- daj z lewej do prawej i już!
162                 
163                 right = '';
164                 let d = nx + boxwidth - maxx;
165                 //if (leftoffset + d > $elem.width() - 10) d = $elem.width() - leftoffset - 10;
166                 nx -= d;
167                 leftoffset += d;
168             }
169         }
170         $elem.css({
171             left: nx,
172             right: right
173         });
174         if (!$elem.data('attach-bottom')) {
175             ny = y - $elem.height() - 10;
176         } else {
177             ny = y + 10;
178         }
179         $elem.css({
180             top: ny
181         });
182         $('.pointer', $elem).css({
183             left: leftoffset - 6
184         });
185
186         $elem.css({
187             display: "block"
188         });
189     }
190
191     function closeNoteBox() {
192         $('#annotation-box').data('anchoredTo', null).fadeOut();
193     }
194     $(document).on('click', function(event) {
195         let t = $(event.target);
196         if (t.parents('#annotation-box').length && !t.is('#footnote-link')) {
197             return;
198         }
199         closeNoteBox();
200     });
201     $(window).on('resize', closeNoteBox);
202
203     function getPositionInBookText($e) {
204         let x = 0, y = 0;
205
206         // Ok dla Y, nie ok dla X
207         
208         while ($e.attr('id') != 'book-text') {
209             let p = $e.position();
210             x += p.left;
211             y += p.top;
212             $e = $e.offsetParent();
213             break;
214         }
215         return {"x": x, "y": y}
216     }
217     
218     $('#book-text .annotation').on('click', function(event) {
219         if ($(this).parents('#footnotes').length) return;
220         event.preventDefault();
221
222
223
224         let x = $(this).width() / 2, y = 0;
225         let elem = $(this);
226         while (elem.attr('id') != 'book-text') {
227             let p = $(elem).position();
228             x += p.left;
229             y += p.top;
230             elem = elem.parent();
231         }
232         href = $(this).attr('href').substr(1);
233         content = $("[name='" + href + "']").next().next().html();
234         if (!content) return;
235         $("#annotation-content").html(content);
236         $("#footnote-link").attr('href', '#' + href)
237
238         
239         putNoteAt($('#annotation-box'), this);
240         event.stopPropagation();
241     });
242
243
244     
245     let zakladki = {};
246     $.get({
247         url: '/zakladki/',
248         success: function(data) {
249             zakladki = data;
250             $.each(zakladki, (i, e) => {
251                 zakladkaUpdateFor(
252                     // TODO: not just paragraphs.
253                     $('[href="#' + e.anchor + '"]').nextAll('.paragraph').first()
254                 );
255             });
256         }
257     });
258
259     // TODO: create bookmarks on init
260     // We need to do that from anchors.
261     
262     function zakladkaUpdateFor($item) {
263
264         let anchor = $item.prevAll('.target').first().attr('name');
265         
266         if (anchor in zakladki) {
267             let $booktag = $item.data('booktag');
268             if (!$booktag) {
269
270                 // TODO: only copy without the dialog.
271                 $booktag = $("<div class='zakladka'>");
272                 $booktag.append($('.icon', $zakladka).clone());
273                 
274                 $item.data('booktag', $booktag);
275                 $booktag.data('p', $item);
276                 $booktag.data('anchor', anchor);
277                 $zakladka.after($booktag);
278
279                 zakladkaSetPosition($booktag);
280                 $booktag.show();
281             }
282
283             $z = $booktag;
284             if (zakladki[anchor].note) {
285                 $z.removeClass('zakladka-exists');
286                 $z.addClass('zakladka-note');
287             } else {
288                 $z.removeClass('zakladka-note');
289                 $z.addClass('zakladka-exists');
290             }
291         } else {
292             let $booktag = $item.data('booktag');
293             if ($booktag) {
294                 $item.data('booktag', null);
295                 $zakladka.append($("#zakladka-box"));
296                 $booktag.remove();
297             }
298         }
299     }
300
301     function zakladkaSetPosition($z) {
302         $item = $z.data('p');
303         pos = getPositionInBookText($item);
304         $z.css({
305             display: 'block',
306             top: pos.y,
307             right: ($('#main-text').width() - $('#book-text').width()) / 2,
308         });
309     }
310
311     let $zakladka = $('#zakladka');
312     $('#book-text .paragraph').on('mouseover', function() {showMarker($(this));});
313     $('#book-text .verse').on('mouseover', function() {showMarker($(this));});
314         //$.PMarker.showForP(this);
315
316
317     function showMarker(p) {
318         
319         // Close the currently tag box when moving to another one.
320         // TBD: Do we want to keep the box open and prevent moving?
321         $("#zakladka-box").hide();
322
323         let anchor = p.prevAll('.target').first().attr('name');
324         // Don't bother if there's no anchor to use.
325         if (!anchor) return;
326
327         // Only show tag if there is not already a tag for this p.
328         if (p.data('booktag')) {
329             $zakladka.hide();
330         } else {
331             $zakladka.data('p', p);
332             $zakladka.data('anchor', anchor);
333
334             // (not needed) zakladkaUpdateClass();
335             // TODO: UPDATE THIS ON OPEN?
336             //let note = anchor in zakladki ? zakladki[anchor].note : ''; 
337             //$('textarea', $zakladka).val(note);
338
339             zakladkaSetPosition($zakladka);
340             $zakladka.show();
341         }
342     }
343
344     $(".zakladka-tool_zakladka").on('click', function() {
345         let $z = $("#zakladka-box").data('z');
346         let anchor = $z.data('anchor');
347         let $p = $z.data('p');
348         $.post({
349             url: '/zakladki/',
350             data: {
351                 csrfmiddlewaretoken: csrf,
352                 anchor: anchor
353             },
354             success: function(data) {
355                 zakladki[data.anchor] = data;
356                 $("#zakladka-box").hide();
357
358                 // Just hide, and create new .zakladka if not already exists?
359                 // In general no hiding 'classed' .zakladka.
360                 // So the 'cursor' .zakladka doesn't ever need class update.
361                 //zakladkaUpdateClass();
362                 zakladkaUpdateFor($p);
363
364             }
365         });
366     });
367
368     $(".zakladka-tool_notka_text textarea").on('input', function() {
369         // FIXME: no use const $zakladka here, check which .zakladka are we attached to.
370         let $z = $(this).closest('.zakladka');
371         let anchor = $z.data('anchor');
372
373         $("#notka-saved").hide();
374         //$("#notka-save").show();
375         $.post({
376             url: '/zakladki/' + zakladki[anchor].uuid + '/',
377             data: {
378                 csrfmiddlewaretoken: csrf,
379                 note: $(this).val()
380             },
381             success: function(data) {
382                 zakladki[anchor] = data;
383                 zakladkaUpdateFor($z.data('p'));
384                 $("#notka-save").hide();
385                 $("#notka-saved").fadeIn();
386             }
387         });
388     });
389
390     $(".zakladka-tool_zakladka_delete").on('click', function() {
391         let $z = $(this).closest('.zakladka');
392         let anchor = $z.data('anchor');
393         $.post({
394             url: '/zakladki/' + zakladki[anchor].uuid + '/delete/',
395             data: {
396                 csrfmiddlewaretoken: csrf,
397             },
398             success: function(data) {
399                 delete zakladki[anchor];
400                 $("#zakladka-box").hide();
401                 zakladkaUpdateFor($z.data('p'));
402             }
403         });
404     });
405
406     $("#main-text").on("click", ".zakladka .icon", function() {
407         let $z = $(this).closest('.zakladka');
408         let $box = $("#zakladka-box");
409         $z.append($box);
410         $box.data('z', $z);
411
412         let $p = $z.data('p');
413         let anchor = $z.data('anchor');
414         let note = anchor in zakladki ? zakladki[anchor].note : ''; 
415
416         $('.zakladka-tool_zakladka', $box).toggle(!(anchor in zakladki));
417         $('.zakladka-tool_sluchaj', $box).toggle($p.hasClass('syncable')).data('sync', $p.attr('id'));
418         $('textarea', $box).val(note);
419
420         $box.toggle();
421     });
422
423
424     class QBox {
425         constructor(qbox) {
426             this.qbox = qbox;
427         }
428         showForSelection(sel) {
429             // TODO: only consider ranges inside text.?
430             this.selection = sel;
431
432             // TODO: multiple ranges.
433             let range = sel.getRangeAt(0);
434             let rect = range.getBoundingClientRect();
435
436             putNoteAt(this.qbox, range)
437         }
438         showForBlock(b) {
439             let rect = b.getBoundingClientRect();
440
441             putNoteAt(this.qbox, b, 'left')
442         }
443         hide() {
444             this.qbox.data('anchoredTo', null);
445             this.qbox.fadeOut();
446         }
447         hideCopied() {
448             this.qbox.data('anchoredTo', null);
449             this.qbox.addClass('copied').fadeOut(1500, () => {
450                 this.qbox.removeClass('copied');
451             });
452         }
453
454         copyText() {
455             // TODO: only consider ranges inside text.?
456             let range = this.selection.getRangeAt(0);
457             let e = range.startContainer;
458             let anchor = getIdForElem(e);
459             let text = window.location.protocol + '//' +
460                 window.location.host +
461                 window.location.pathname;
462
463             navigator.clipboard.writeText(
464                 this.selection.toString() +
465                     '\n\nCałość czytaj na: ' + text
466             );
467             this.hideCopied();
468         }
469         copyLink() {
470             // TODO: only consider ranges inside text.?
471             let range = this.selection.getRangeAt(0);
472             let e = range.startContainer;
473             let anchor = getIdForElem(e);
474             let text = window.location.protocol + '//' +
475                 window.location.host +
476                 window.location.pathname;
477             if (anchor) text += anchor;
478             navigator.clipboard.writeText(text);
479             
480             this.hideCopied();
481         }
482         quote() {
483             // What aboot non-contiguous selections?
484             let sel = this.selection;
485             let textContent = sel.toString();
486             let anchor = getIdForElem(sel.getRangeAt(0).startContainer);
487             let paths = getSelectionPaths(sel);
488             $.post({
489                 url: '/cytaty/',
490                 data: {
491                     csrfmiddlewaretoken: csrf,
492                     text: textContent,
493                     startElem: anchor,
494                     //endElem: endElem,
495                     //startOffset: 0,
496                     //endOffset: 0,
497                     paths: paths,
498                 },
499                 success: function (data) {
500                     var win = window.open('/cytaty/' + data.uuid + '/', '_blank');
501                 }
502             });
503             
504         }
505         
506     }
507     let qbox = new QBox($("#qbox"));
508
509
510     function getPathToNode(elem) {
511         // Need normalize?
512         let path = [];
513         while (elem.id != 'book-text') {
514             let p = elem.parentElement;
515             path.unshift([...p.childNodes].indexOf(elem))
516             elem = p;
517         }
518         return path;
519     }
520     function getSelectionPaths(selection) {
521         // does it work?
522         let range1 = selection.getRangeAt(0);
523         let range2 = selection.getRangeAt(selection.rangeCount - 1);
524         let paths = [
525             getPathToNode(range1.startContainer) + [range1.startOffset],
526             getPathToNode(range2.endContainer) + [range2.endOffset]
527         ]
528         return paths;
529     }
530     
531
532     function getIdForElem(elem) {
533         // is it used?
534         let $elem = $(elem);
535         // check if inside book-text?
536
537         while (true) {
538             if ($elem.hasClass('target')) {
539                 return $elem.attr('name');
540             }
541             $p = $elem.prev();
542             if ($p.length) {
543                 $elem = $p;
544             } else {
545                 // Gdy wychodzimy w górę -- to jest ten moment, w którym znajdujemy element od którego wychodzimy i zliczamy znaki.
546
547                 
548                 $p = $elem.parent()
549                 if ($p.length) {
550                     // is there text?
551                     $elem = $p;
552                 } else {
553                     return undefined;
554                 }
555             }
556         }
557     }
558
559     function getIdForElem(elem) {
560         // is it used?
561         // check if inside book-text?
562         $elem = $(elem);
563         while (true) {
564             if ($elem.hasClass('target')) {
565                 return $elem.attr('name');
566             }
567             $p = $elem.prev();
568             if ($p.length) {
569                 $elem = $p;
570             } else {
571                 $p = $elem.parent()
572                 if ($p.length) {
573                     // is there text?
574                     $elem = $p;
575                 } else {
576                     return undefined;
577                 }
578             }
579         }
580     }
581
582
583     function positionToIIDOffset(container, offset) {
584         // Container and offset follow Range rules.
585         // If container is a text node, offset is text offset.
586         // If container is an element node, offset is number of child nodes from container start.
587         // (containers of type Comment, CDATASection - ignore)z
588     }
589
590
591     function updateQBox() {
592         sel = document.getSelection();
593         let goodS = true;
594         if (sel.isCollapsed || sel.rangeCount < 1) {
595             goodS = false;
596         }
597         
598         if (!goodS) {
599             qbox.hide();
600         } else {
601             qbox.showForSelection(sel);
602         }
603     };
604     $(document).on('selectionchange', updateQBox);
605
606     function updateBoxes() {
607         updateNote(qbox.qbox);
608         updateNote($('#annotation-box'));
609         
610     }
611     $(window).on('scroll', updateBoxes);
612     $(window).on('resize', updateBoxes);
613
614
615     $(window).on('resize', function() {
616         $('.zakladka').each(function() {
617             zakladkaSetPosition($(this));
618         });
619     });
620
621     $('a.anchor').on('click', function(e) {
622         e.preventDefault();
623
624         let sel = window.getSelection();
625         sel.removeAllRanges();
626         let range = document.createRange();
627
628         let $p = $(this).nextAll('.paragraph').first()
629         range.selectNode($p[0]);
630         sel.addRange(range);
631         
632         qbox.showForSelection(sel);
633
634         showMarker($p);
635     });
636     
637    
638     
639     $('.qbox-t-copy').on('click', function(e) {
640         e.preventDefault();
641         qbox.copyText();
642     });
643     $('.qbox-t-link').on('click', function(e) {
644         e.preventDefault();
645         qbox.copyLink();
646     });
647     $('.qbox-t-quote').on('click', function(e) {
648         e.preventDefault();
649         qbox.quote();
650     });
651
652
653     /*
654     $(".paragraph").on('click', function(e) {
655         qbox.showForBlock(this);
656     });
657     */
658
659     
660     function scrollToAnchor(anchor) {
661         if (anchor) {
662             var anchor_name = anchor.slice(1);
663             var element = $('a[name="' + anchor_name + '"]');
664             if (element.length > 0) {
665                 $("html").animate({
666                     scrollTop: element.offset().top - 55
667                 }, {
668                     duration: 500,
669                     done: function() {
670                         history.pushState({}, '', anchor);
671                     },
672                 });
673             }
674         }
675     }
676     scrollToAnchor(window.location.hash)
677     $('#toc, #themes, #book-text, #annotation').on('click', 'a', function(event) {
678         event.preventDefault();
679         scrollToAnchor($(this).attr('href'));
680     });
681
682     
683 })})(jQuery);