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