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