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