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