From: Radek Czajka Date: Wed, 5 Jun 2024 11:44:54 +0000 (+0200) Subject: Initial bookmarks. X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/f7d8acded4a58d423035c5759f6dee9a34396959?ds=sidebyside;hp=d9c51b94bfe6b1cda1790d57a9108b83c6e1643b Initial bookmarks. --- diff --git a/src/bookmarks/__init__.py b/src/bookmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/bookmarks/admin.py b/src/bookmarks/admin.py new file mode 100644 index 000000000..956f233c2 --- /dev/null +++ b/src/bookmarks/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from . import models + + +@admin.register(models.Bookmark) +class BookmarkAdmin(admin.ModelAdmin): + date_hierarchy = 'created_at' + list_display = ['uuid', 'created_at', 'user', 'book', 'anchor'] + raw_id_fields = ['book', 'user'] + + +@admin.register(models.Quote) +class BookmarkAdmin(admin.ModelAdmin): + date_hierarchy = 'created_at' + list_display = ['uuid', 'created_at', 'user', 'book', 'start_elem'] + raw_id_fields = ['book', 'user'] diff --git a/src/bookmarks/apps.py b/src/bookmarks/apps.py new file mode 100644 index 000000000..c7fc01929 --- /dev/null +++ b/src/bookmarks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BookmarksConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'bookmarks' diff --git a/src/bookmarks/migrations/0001_initial.py b/src/bookmarks/migrations/0001_initial.py new file mode 100644 index 000000000..eb5f7ff71 --- /dev/null +++ b/src/bookmarks/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.0.8 on 2024-02-28 11:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('anchor', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('note', models.TextField(blank=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/bookmarks/migrations/0002_quote.py b/src/bookmarks/migrations/0002_quote.py new file mode 100644 index 000000000..f413c5104 --- /dev/null +++ b/src/bookmarks/migrations/0002_quote.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.8 on 2024-03-06 11:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('bookmarks', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Quote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('start_elem', models.CharField(blank=True, max_length=100)), + ('end_elem', models.CharField(blank=True, max_length=100)), + ('start_offset', models.IntegerField(blank=True, null=True)), + ('end_offset', models.IntegerField(blank=True, null=True)), + ('text', models.TextField(blank=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/bookmarks/migrations/__init__.py b/src/bookmarks/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py new file mode 100644 index 000000000..67a4fa5e6 --- /dev/null +++ b/src/bookmarks/models.py @@ -0,0 +1,63 @@ +import uuid +from django.db import models + + +class Bookmark(models.Model): + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey('auth.User', models.CASCADE) + book = models.ForeignKey('catalogue.Book', models.CASCADE) + anchor = models.CharField(max_length=100, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + note = models.TextField(blank=True) + + def __str__(self): + return str(self.uuid) + + def get_for_json(self): + return { + 'uuid': self.uuid, + 'anchor': self.anchor, + 'note': self.note, + 'created_at': self.created_at, + } + + +class Quote(models.Model): + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey('auth.User', models.CASCADE) + book = models.ForeignKey('catalogue.Book', models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + start_elem = models.CharField(max_length=100, blank=True) + end_elem = models.CharField(max_length=100, blank=True) + start_offset = models.IntegerField(null=True, blank=True) + end_offset = models.IntegerField(null=True, blank=True) + text = models.TextField(blank=True) + + def __str__(self): + return str(self.uuid) + + def get_for_json(self): + return { + 'uuid': self.uuid, + 'startElem': self.start_elem, + 'endElem': self.end_elem, + 'startOffset': self.start_offset, + 'startOffset': self.end_offset, + 'created_at': self.created_at, + } + +def from_path(elem, path): + def child_nodes(e): + if e.text: yield (e, 'text') + for child in e: + if child.attrib.get('id') != 'toc': + yield (child, None) + if child.tail: + yield (child, 'tail') + while len(path) > 1: + n = path.pop(0) + elem = list(child_nodes(elem))[n] + return elem + + + diff --git a/src/bookmarks/templates/bookmarks/quote_detail.html b/src/bookmarks/templates/bookmarks/quote_detail.html new file mode 100644 index 000000000..46181dd22 --- /dev/null +++ b/src/bookmarks/templates/bookmarks/quote_detail.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n static %} + + +{% block breadcrumbs %} + Cytaty użytkowników + +{% endblock %} + +{% block main %} + +{% with book=object.book %} +
+
+ {% with first_text=book.get_first_text %} + +
+ + +
+ {% endwith %} +
+ +
+ + + + + {% endwith %} +{% endblock %} diff --git a/src/bookmarks/tests.py b/src/bookmarks/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/bookmarks/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/bookmarks/urls.py b/src/bookmarks/urls.py new file mode 100644 index 000000000..333afd783 --- /dev/null +++ b/src/bookmarks/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path('zakladki/', views.bookmarks, name='bookmarks'), + path('zakladki//', views.bookmark, name='bookmark'), + path('zakladki//delete/', views.bookmark_delete, name='bookmark_delete'), + + path('cytaty/', views.quotes, name='quotes'), + path('cytaty//', views.quote, name='quote'), +] diff --git a/src/bookmarks/views.py b/src/bookmarks/views.py new file mode 100644 index 000000000..7d83818c1 --- /dev/null +++ b/src/bookmarks/views.py @@ -0,0 +1,137 @@ +from django.http import Http404, JsonResponse +from django.shortcuts import render, get_object_or_404 +from django.views.decorators import cache +import catalogue.models +from wolnelektury.utils import is_ajax +from . import models +from lxml import html +import re + + +# login required + +@cache.never_cache +def bookmarks(request): + try: + slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1] + except: + slug = 'w-80-dni-dookola-swiata' +# raise Http404() + try: + book = catalogue.models.Book.objects.get(slug=slug) + except catalogue.models.Book.DoesNotExist: + raise Http404() + + if request.method == 'POST': + # TODO test + bm, created = models.Bookmark.objects.update_or_create( + user=request.user, + book=book, + anchor=request.POST.get('anchor', ''), + defaults={ + 'note': request.POST.get('note', ''), + } + ) + return JsonResponse(bm.get_for_json()) + else: + return JsonResponse({ + bm.anchor: bm.get_for_json() + for bm in models.Bookmark.objects.filter( + user=request.user, + book=book, + ) + }) + + +def bookmark(request, uuid): + bm = get_object_or_404(models.Bookmark, user=request.user, uuid=uuid) + if request.method == 'POST': + bm.note = request.POST.get('note', '') + bm.save() + return JsonResponse(bm.get_for_json()) + + +def bookmark_delete(request, uuid): + models.Bookmark.objects.filter(user=request.user, uuid=uuid).delete() + return JsonResponse({}) + + + + +@cache.never_cache +def quotes(request): + try: + slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1] + except: + slug = 'w-80-dni-dookola-swiata' +# raise Http404() + try: + book = catalogue.models.Book.objects.get(slug=slug) + except catalogue.models.Book.DoesNotExist: + raise Http404() + + if request.method == 'POST': + # TODO test + # ensure unique? or no? + + text = request.POST.get('text', '') + text = text.strip() + + stext = re.sub(r'\s+', ' ', text) + ## verify + print(text) + + + # find out + with book.html_file.open('r') as f: + ht = f.read() + tree = html.fromstring(ht) + # TODO: clear + for sel in ('.//a[@class="theme-begin"]', + './/a[@class="anchor"]', + ): + for e in tree.xpath(sel): + e.clear(keep_tail=True) + htext = html.tostring(tree, encoding='unicode', method='text') + htext = re.sub(r'\s+', ' ', htext) + + print(htext) + + otext = stext + if stext not in htext: + # raise 401 + raise Http404() + + # paths? + # start elem? + q = models.Quote.objects.create( + user=request.user, + book=book, + start_elem=request.POST.get('startElem', ''), + end_elem=request.POST.get('startElem', ''), + start_offset=request.POST.get('startOffset', None), + end_offset=request.POST.get('startOffset', None), + text=text, + ) + return JsonResponse(q.get_for_json()) + else: + return JsonResponse({ + q.start_elem: q.get_for_json() + for q in models.Quote.objects.filter( + user=request.user, + book=book, + ) + }) + + + +def quote(request, uuid): + q = get_object_or_404(models.Quote, user=request.user, uuid=uuid) + if is_ajax(request): + return JsonResponse(q.get_for_json()) + else: + return render(request, 'bookmarks/quote_detail.html', { + 'object': q, + }) + + diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 86010df1a..ac8c02b9e 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -414,7 +414,7 @@ class Book(models.Model): has_daisy_file.boolean = True def has_sync_file(self): - return self.has_media("sync") + return settings.FEATURE_SYNCHRO and self.has_media("sync") def get_sync(self): with self.get_media('sync').first().file.open('r') as f: diff --git a/src/catalogue/static/player/player.js b/src/catalogue/static/player/player.js index dfac09615..cbf90261b 100644 --- a/src/catalogue/static/player/player.js +++ b/src/catalogue/static/player/player.js @@ -4,10 +4,13 @@ $(".book-right-column").remove(); if ($("#player-bar").length) { - $("h1").first().after($("")); + $("#book-text-buttons").append( + $(" zacznij słuchać") + ).show(); } $(".enable-player-bar").click(function() { + $('body').addClass('with-player-bar'); $('.jp-play').click(); return false; @@ -67,9 +70,11 @@ // TODO: will need class for attach // may be added from sync data - $(".syncable").click(function() { - if (!$('body').hasClass('with-player-bar')) return; - let id = $(this).attr('id'); + + + $(".zakladka-tool_sluchaj").click(function() { + $('body').addClass('with-player-bar'); + let id = $(this).data('sync'); if (!id) return; for (let i=0; i + {#% annoy_banner 'book-start' %#} + +
+
+ -
+
{% with next=book.get_next_text prev=book.get_prev_text %} {% if next %} {{ next.title }} → @@ -178,7 +183,13 @@ {% if prev %} ← {{ prev.title }} {% endif %} - {{ book_text|safe }} +
+ {{ book_text|safe }} +
+ +
+ {% include 'club/donation_step1_form.html' with form=donation_form %} +
{% endwith %} @@ -200,34 +211,102 @@ + - {% if book.other_versions.exists %} -
-

{% trans "Inne wersje utworu" %}

- {% trans "Zamknij drugą wersję" %} - +
+
+ Skopiuj link + Skopiuj cytat + {% if request.user.is_authenticated %} + Zapisz cytat + {% endif %}
- {% endif %} +
+
+
-
- {% annoy_banners 'book-text-intermission' %} +
+
+ Zakładka + Istniejąca zakładka + Notka +
+
+
+ {% if request.user.is_authenticated %} +
+ Usuń zakładkę +
+
+ Dodaj zakładkę +
+
+ Słuchaj od tego miejsca +
+
+ + + + + +
+ + {% else %} + +
+ +
+
+
+
+ + + {% if book.other_versions.exists %} +
+

{% trans "Inne wersje utworu" %}

+ {% trans "Zamknij drugą wersję" %} + +
+ {% endif %} + +
+ {% annoy_banners 'book-text-intermission' %} + + {% for insert in inserts %} + {% include 'annoy/dynamic_insert.html' %} + {% endfor %} +
diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 588cdb073..fc01ad65e 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -550,7 +550,10 @@ def book_text(request, slug): 'book': book, 'extra_info': book.get_extra_info_json(), 'book_text': book_text, - 'inserts': DynamicTextInsert.get_all(request) + 'inserts': DynamicTextInsert.get_all(request), + + 'club': Club.objects.first(), + 'donation_form': DonationStep1Form(), }) diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index 769565d03..d4faa8bfb 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -8,6 +8,7 @@ INSTALLED_APPS_OUR = [ 'ajaxable', 'annoy', 'api', + 'bookmarks', 'catalogue', 'chunks', 'dictionary', diff --git a/src/wolnelektury/settings/custom.py b/src/wolnelektury/settings/custom.py index a9ae731eb..16b5e0a4f 100644 --- a/src/wolnelektury/settings/custom.py +++ b/src/wolnelektury/settings/custom.py @@ -74,3 +74,5 @@ WIDGETS = {} SEARCH_CONFIG = 'english' SEARCH_CONFIG_SIMPLE = 'simple' SEARCH_USE_UNACCENT = False + +FEATURE_SYNCHRO = False diff --git a/src/wolnelektury/settings/static.py b/src/wolnelektury/settings/static.py index b3edb72cb..b83691b24 100644 --- a/src/wolnelektury/settings/static.py +++ b/src/wolnelektury/settings/static.py @@ -31,6 +31,7 @@ PIPELINE = { 'chunks/edit.scss', 'scss/text.scss', + '2022/styles/reader_player.scss', ], 'output_filename': 'css/compressed/main.css', }, @@ -59,6 +60,7 @@ PIPELINE = { 'source_filenames': [ '2022/scripts/vendor.js', 'contrib/jquery-ui-1.13.1.custom/jquery-ui.js', + #'js/contrib/jquery.scrollto.js', 'js/search.js', 'js/header.js', @@ -78,6 +80,9 @@ PIPELINE = { 'js/book_text/toc.js', 'js/book_text/progress.js', + 'js/book_text/pbox.js', + 'js/book_text/pbox-items.js', + 'js/contrib/jquery.countdown.js', 'js/contrib/jquery.countdown-pl.js', 'js/contrib/jquery.countdown-de.js', 'js/contrib/jquery.countdown-uk.js', 'js/contrib/jquery.countdown-es.js', 'js/contrib/jquery.countdown-lt.js', @@ -99,6 +104,9 @@ PIPELINE = { 'source_filenames': [ 'js/contrib/jquery.form.js', 'js/contrib/jquery.jqmodal.js', + + 'js/contrib/jquery.scrollto.js', + 'js/book_text/info.js', 'js/book_text/menu.js', 'js/book_text/note.js', diff --git a/src/wolnelektury/static/2022/images/add-icon.svg b/src/wolnelektury/static/2022/images/add-icon.svg new file mode 100644 index 000000000..006ee63f2 --- /dev/null +++ b/src/wolnelektury/static/2022/images/add-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/wolnelektury/static/2022/images/add-note-icon.svg b/src/wolnelektury/static/2022/images/add-note-icon.svg new file mode 100644 index 000000000..ba6a5ff10 --- /dev/null +++ b/src/wolnelektury/static/2022/images/add-note-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/wolnelektury/static/2022/images/notka-saved.svg b/src/wolnelektury/static/2022/images/notka-saved.svg new file mode 100644 index 000000000..79eebd136 --- /dev/null +++ b/src/wolnelektury/static/2022/images/notka-saved.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/play-now-icon.svg b/src/wolnelektury/static/2022/images/play-now-icon.svg new file mode 100644 index 000000000..4aeace3fb --- /dev/null +++ b/src/wolnelektury/static/2022/images/play-now-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/wolnelektury/static/2022/images/tool-copy.svg b/src/wolnelektury/static/2022/images/tool-copy.svg new file mode 100644 index 000000000..cf26b5ae1 --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-copy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/wolnelektury/static/2022/images/tool-link.svg b/src/wolnelektury/static/2022/images/tool-link.svg new file mode 100644 index 000000000..241bd68c4 --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-link.svg @@ -0,0 +1,49 @@ + + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/tool-quote.svg b/src/wolnelektury/static/2022/images/tool-quote.svg new file mode 100644 index 000000000..0c8670a1e --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-quote.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-full.svg b/src/wolnelektury/static/2022/images/zakladka-full.svg new file mode 100644 index 000000000..4fccefe99 --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-full.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-note.svg b/src/wolnelektury/static/2022/images/zakladka-note.svg new file mode 100644 index 000000000..1c4d3eaef --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-note.svg @@ -0,0 +1,45 @@ + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-usun.svg b/src/wolnelektury/static/2022/images/zakladka-usun.svg new file mode 100644 index 000000000..628efb12c --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-usun.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka.svg b/src/wolnelektury/static/2022/images/zakladka.svg new file mode 100644 index 000000000..55aceaa5a --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/wolnelektury/static/2022/styles/layout/_author.scss b/src/wolnelektury/static/2022/styles/layout/_author.scss index 618b19ac5..30796dd8b 100644 --- a/src/wolnelektury/static/2022/styles/layout/_author.scss +++ b/src/wolnelektury/static/2022/styles/layout/_author.scss @@ -180,7 +180,9 @@ } .l-author__quotes__slider { - display: flex; + display: flex; +} +.l-author__quotes { div { em { font-style: italic; diff --git a/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss b/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss new file mode 100644 index 000000000..229347684 --- /dev/null +++ b/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss @@ -0,0 +1,24 @@ +.bookmarks-quote-box { + padding: 30px ; + background: #E6F0F1; + border-radius: 20px; + margin-bottom: 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + + .bookmarks-quote { + + } + + .bookmarks-quote-content { + padding: 30px; + } + + .bookmarks-quote-book { + img { + width: 170px; + } + } +} diff --git a/src/wolnelektury/static/2022/styles/layout/_module.scss b/src/wolnelektury/static/2022/styles/layout/_module.scss index fcefb2f3e..4e4a35f37 100644 --- a/src/wolnelektury/static/2022/styles/layout/_module.scss +++ b/src/wolnelektury/static/2022/styles/layout/_module.scss @@ -5,6 +5,7 @@ /*!*/ @import "annoy"; +@import "bookmarks"; @import "navigation"; @import "header"; @import "main"; diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index dd48d4f25..1c617c7ee 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -67,13 +67,12 @@ left: 0; top: 0; } - #book-text { - padding: 5px; + .main-text-body { + margin: 16px; @include rwd($break-wide) { width: 717px; - padding: 0; + margin: 0 auto; } - margin: 0 auto; color: #333333; @@ -244,12 +243,13 @@ #main-text #book-text { + body { - font-size: 16px; - font-family: Gelasio, Georgia, "Times New Roman", serif; - line-height: 1.5em; - margin: 0; -} + font-size: 16px; + font-family: Gelasio, Georgia, "Times New Roman", serif; + line-height: 1.5em; + margin: 0; + } a { color: blue; @@ -366,6 +366,7 @@ blockquote { /* ============= */ .verse, .paragraph { position:relative; + padding: 0 48px 0 0; } /*.anchor { position: absolute; @@ -467,7 +468,7 @@ div.kwestia div.stanza { p.paragraph { text-align: justify; - margin: 0; + margin: 0 0 0 44px; text-indent: 1.5em; } @@ -562,8 +563,9 @@ table.border td, table.border th { .anchor { /* Show line numbers. */ float: left; + clear: left; font-size: .8em; - margin-left: -40px; + margin-left: 0; width: 36px; height: auto; padding: 2px; @@ -573,7 +575,6 @@ table.border td, table.border th { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; - } @@ -586,7 +587,6 @@ table.border td, table.border th { clear: both; line-height: 1.5em; text-align: left; - z-index: 60; font-style: normal; font-weight: normal; @@ -652,3 +652,373 @@ a.reference.interesting:after { margin-bottom: 1em; } } + + +#main-text { + #annotation-box { + display: none; + position: fixed; + + .pointer-bottom { + transform: rotate(45deg); + left: 27.5px; + bottom: -6px; + width: 12px; + height: 12px; + position: absolute; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #FFF); + border: 1px solid var(--teal-700, #007880); + display: block; + } + .pointer-top { + left: 27.5px; + bottom: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #fff; + display: block; + } + + #annotation { + max-width: 470px; + position: relative; + z-index: 2; + background: white; + + padding: 20px; + box-shadow: 0px 0px 20px rgba(1, 129, 137, 0.2); + border: 1px solid #007880; + border-radius: 6px; + + @include rwd($break-wide) { + } + } + + + #annotation-content { + max-height: 138px; + overflow-y: scroll; + color: var(--black-900, #333); + leading-trim: both; + text-edge: cap; + font-variant-numeric: oldstyle-nums proportional-nums; + /* Czytnik/Desktop/p read */ + font-family: "Source Serif Pro"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 28px; /* 155.556% */ + } + + #footnote-link { + display: block; + margin-top: 16px; + text-align: right; + } + } + + + #qbox { + display: none; + position: fixed; + padding: 4px; + border: 1px solid #007880; + border-radius: 22px; + background: white; + box-shadow: 6px 6px 10px 0px rgba(0, 120, 128, 0.35); + overflow: hidden; + + &.copied { + .content:after { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: "skopiowane"; + display: flex; + background: white; + color: black; + justify-content: center; + align-items: center; + } + } + + .content { + overflow: hidden; + display: flex; + a { + width: 70px; + line-height: 44px; + text-align: center; + border-left: 1px solid #BBF6FA; + + &:nth-child(1) { + border-left: none; + } + + img { + vertical-align: middle; + height: 22px; + } + } + } + + .pointer-bottom { + transform: rotate(45deg); + left: 128px; + top: -6px; + width: 12px; + height: 12px; + position: absolute; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #FFF); + border: 1px solid var(--teal-700, #007880); + display: block; + } + .pointer-top { + left: 128px; + top: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #fff; + display: block; + } + + } +} + + + +.zakladka { + display: none; + position: absolute; + //z-index: 70; + width: 40px; + height: 40px; + .icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + .icon-exists, .icon-note {display: none;} + img { + } + } + + #zakladka-box { + padding: 20px; + width: 270px; + border: 1px solid #007880; + border-radius: 8px; + box-shadow: 6px 6px 10px 0px rgba(0, 120, 128, 0.35); + background: #E1F1F2; + position: relative; + left: -225px; + z-index: 3; + @include rwd($break-wide) { + left: -12px; + } + + .pointer-bottom { + transform: rotate(45deg); + left: 238px; + top: -7px; + width: 12px; + height: 12px; + position: absolute; + box-sizing: border-box; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #E1F1F2); + border: 1px solid var(--teal-700, #007880); + display: block; + + @include rwd($break-wide) { + left: 25px; + } + } + .pointer-top { + left: 238px; + top: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #E1F1F2; + display: block; + @include rwd($break-wide) { + left: 25px; + } + } + + + .content { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: 12px; + .zakladka-tool { + color: #333; + line-height: 30px; + font-family: "Source Sans Pro"; + font-size: 18px; + font-style: normal; + font-weight: 400; + cursor: pointer; + padding-left: 38px; + + &:before { + height: 30px; + width: 30px; + display: inline-block; + vertical-align: bottom; + margin-left: -38px; + margin-right: 8px; + } + + &.zakladka-tool_zakladka_delete:before { + content: url('/static/2022/images/zakladka-usun.svg'); + } + &.zakladka-tool_zakladka:before { + content: url('/static/2022/images/add-icon.svg'); + } + &.zakladka-tool_sluchaj:before { + content: url('/static/2022/images/play-now-icon.svg'); + } + &.zakladka-tool_notka:before { + content: url('/static/2022/images/add-note-icon.svg'); + } + } + .zakladka-tool_notka_text { + display: none; + width: 100%; + position: relative; + textarea { + width: 100%; + +display: flex; +padding: 16px; +justify-content: center; +align-items: center; +gap: 8px; +flex: 1 0 0; +border-radius: 6px; +border: 1px solid #007880; +background: #fff; + } + #notka-save, #notka-saved { + position: absolute; + top: 4px; + right: 4px; + display: none; + width: 16px; + height: 16px; + } + #notka-saved { + } + } + .zakladka-tool_zakladka_delete { + display: none; +// position: absolute; +// bottom: 40px; +// right: 3px; +// width: 29px; +// height: 29px; +// cursor: pointer; +// align-items: center; +// justify-content: center; + } + } + } + + + + &.zakladka-exists { + .icon-empty {display: none;} + .icon-exists {display: flex;} + #zakladka-box .content { + .zakladka-tool_notka_text {display: block;} + .zakladka-tool_zakladka_delete {display: block;} + } + } + &.zakladka-note { + .icon-empty {display: none;} + .icon-note {display: flex;} + #zakladka-box .content { + .zakladka-tool_notka_text {display: block;} + .zakladka-tool_zakladka_delete {display: block;} + } + } +} + + + +/* +.zakladka-tool_sluchaj {display: none;} +.has-sync .zakladka-tool_sluchaj {display: block;} +*/ + +#book-text-buttons { + display: none; + margin-top: 16px; + border: 1px solid #D5ECED; + border-radius: 6px; + padding: 8px; + max-width: 717px; + + + a { + display: inline-block; + padding: 20px 30px; + font-weight: bold; + cursor: pointer; + border-radius: 6px; + &:hover { + background: #E1F1F2; + } + i { + margin-right: 10px; + &.icon-play { + color: white; + background: #018189; + display: inline-block; + padding: 10px 8px 10px 12px; + border-radius: 100%; + } + } + } +} diff --git a/src/wolnelektury/static/2022/styles/reader_player.scss b/src/wolnelektury/static/2022/styles/reader_player.scss index 28cc2d04b..bf0fb2b29 100644 --- a/src/wolnelektury/static/2022/styles/reader_player.scss +++ b/src/wolnelektury/static/2022/styles/reader_player.scss @@ -3,6 +3,11 @@ @import "components/select"; + + + +#player-bar { + // copied from local, move to base .jp-state-playing .icon-play { &:before { @@ -18,6 +23,8 @@ // * + + .c-media { margin: 0 auto; } @@ -106,12 +113,13 @@ width: 100%; align-items: center; position: relative; - background-color: black; + color: #083F4D; + background-color: #D5ECED; padding: 0 34px 0 14px; } .c-player__btn { - background: white; + background: #083F4D; border: 0; outline: 0; border-radius: 50%; @@ -196,7 +204,7 @@ } .icon { - color: white; + color: #083F4D; font-size: 16px; } } @@ -352,7 +360,7 @@ .icon { font-size: 21px; - color: white; + color: #083F4D; margin-right: 8px; cursor: pointer; } @@ -416,7 +424,7 @@ &.up:after { content: '▲'; - color: white; + color: #083F4D; font-size: .8em; position: absolute; top: 0px; @@ -426,7 +434,7 @@ } &.down:after { content: '▼'; - color: white; + color: #083F4D; font-size: .8em; position: absolute; bottom: 2px; @@ -436,6 +444,7 @@ } } +} #menu { @@ -448,7 +457,7 @@ .with-player-bar .playing-highlight { - background: #D5ECED; + background: #FFE694; } @@ -460,9 +469,6 @@ #player-bar { display: block; } - .syncable { - cursor: pointer; - } } .annoy-banner-on_blackout { diff --git a/src/wolnelektury/static/js/book_text/marker.js b/src/wolnelektury/static/js/book_text/marker.js new file mode 100644 index 000000000..5db4d7e7a --- /dev/null +++ b/src/wolnelektury/static/js/book_text/marker.js @@ -0,0 +1,36 @@ +(function($){$(function(){ + + class PMarker { + putBox(box) { + + let $z = $(this).closest('.zakladka'); + let $box = $("#zakladka-box"); + $z.append($box); + $box.data('z', $z); + + anchor = $z.data('anchor'); + let note = anchor in zakladki ? zakladki[anchor].note : ''; + $('textarea', $box).val(note); + + // TODO update note content here. + // And/or delete buttons. + $box.toggle(); + + + } + + + + showForAnchor(anchor) { + } + + showForP(p) { + } + } + + $.PMarker = PMarker; + + // There can be more than one marker. + // Some markers + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/note.js b/src/wolnelektury/static/js/book_text/note.js index 2f0feaaea..277f3d48e 100644 --- a/src/wolnelektury/static/js/book_text/note.js +++ b/src/wolnelektury/static/js/book_text/note.js @@ -1,9 +1,10 @@ (function($){$(function(){ -if ($('#nota_red li').length > 0) { - $("#menu-nota_red").show(); -} + if ($('#nota_red p').length > 0) { + $("#info").prepend($("
")); + $("#info").prepend($('#nota_red *')); + } diff --git a/src/wolnelektury/static/js/book_text/pbox-items.js b/src/wolnelektury/static/js/book_text/pbox-items.js new file mode 100644 index 000000000..2f9a502e8 --- /dev/null +++ b/src/wolnelektury/static/js/book_text/pbox-items.js @@ -0,0 +1,38 @@ +// i18n for labels? +// maybe move labels to templates after all? + +(function($){$(function(){ + + class PBoxItem { + update(pbox) { + if (this.isAvailable(pbox)) { + pbox.showButton(this, this.label, this.pboxClass); + } + } + } + + + class LoginPBI extends PBoxItem { + label = 'ZALOGUJ'; + pboxClass = 'zakladka-tool_login'; + + isAvailable(pbox) { + return true; + } + + action() { + alert('akcja'); + } + } + + + class BookmarkPBI extends PBoxItem { + label = 'DODAJ ZAKŁADKĘ' + + } + + + $.pbox.addItem(new LoginPBI()); + + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/pbox.js b/src/wolnelektury/static/js/book_text/pbox.js new file mode 100644 index 000000000..816938cd1 --- /dev/null +++ b/src/wolnelektury/static/js/book_text/pbox.js @@ -0,0 +1,46 @@ +(function($){$(function(){ + + class PBox { + items = []; + + constructor(element) { + this.$element = element; + } + + addItem(item) { + this.items.unshift(item); + } + + clear() { + $("div", this.$element).remove(); + } + + showButton(item, text, cls) { + let btn = $("
"); + btn.addClass('zakladka-tool'); + btn.addClass('cls'); + btn.text(text); + btn.on('click', item.action); + this.$element.append(btn); + } + + // What's a p? + // We should open at a *marker*. + // And it's the marker that should know its context. + openAt(marker) { + this.marker = marker; + this.clear(); + $.each(this.items, (i, item) => { + item.update(this); + }); + } + + close() { + } + } + + $.pbox = new PBox($('#zakladka-box')); // TODO: rename id + + + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/progress.js b/src/wolnelektury/static/js/book_text/progress.js index d57a32fbd..ae5b14ac4 100644 --- a/src/wolnelektury/static/js/book_text/progress.js +++ b/src/wolnelektury/static/js/book_text/progress.js @@ -1,6 +1,7 @@ (function($){$(function(){ - t = $('#global-progress').data('t'); + let t = $('#global-progress').data('t'); + function upd_t() { $text = $('#main-text #book-text'); texttop = $text.offset().top; diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index cdfc1f06f..146067523 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -1,4 +1,5 @@ (function($){$(function(){ + let csrf = $("[name='csrfmiddlewaretoken']").val(); var interestingReferences = $("#interesting-references").text(); if (interestingReferences) { @@ -104,4 +105,579 @@ _paq.push(['trackEvent', 'html', 'reference']); }); + + + function putNoteAt($elem, anchor, side) { + $elem.data('anchoredTo', anchor); + updateNote($elem, side); + } + + function updateNote($elem, side) { + anchor = $elem.data('anchoredTo') + if (!anchor) return; + let anchorRect = anchor.getBoundingClientRect(); + + let x = anchorRect.x + anchorRect.width / 2; + let y = anchorRect.y; + if ($elem.data('attach-bottom')) { + y += anchorRect.height; + } + minx = $("#book-text").position().left; + maxx = minx + $("#book-text").width(); + + margin = 20; + minx += margin; + maxx -= margin; + maxx += 10000; + + //boxwidth = 470; + boxwidth = $elem.width(); + + if (maxx - minx <= boxwidth) { + nx = margin; + right = margin; + leftoffset = x - margin; + } else { + right = ''; + + // default position. + leftoffset = 40; + leftoffset = $elem.data('default-leftoffset'); + + nx = x - leftoffset; + + $elem.css({right: ''}); + + // Do we need to move away from the left? + if (nx < minx) { + let d = minx - nx; + nx += d; + leftoffset -= d; + } + + // Do we need to move away from the right? + if (nx + boxwidth > maxx) { + // ACTUALLY CALCULATE STUFF + // if maxx - minx < 470 px -- daj z lewej do prawej i już! + + right = ''; + let d = nx + boxwidth - maxx; + //if (leftoffset + d > $elem.width() - 10) d = $elem.width() - leftoffset - 10; + nx -= d; + leftoffset += d; + } + } + $elem.css({ + left: nx, + right: right + }); + if (!$elem.data('attach-bottom')) { + ny = y - $elem.height() - 10; + } else { + ny = y + 10; + } + $elem.css({ + top: ny + }); + $('.pointer', $elem).css({ + left: leftoffset - 6 + }); + + $elem.css({ + display: "block" + }); + } + + function closeNoteBox() { + $('#annotation-box').data('anchoredTo', null).fadeOut(); + } + $(document).on('click', function(event) { + let t = $(event.target); + if (t.parents('#annotation-box').length && !t.is('#footnote-link')) { + return; + } + closeNoteBox(); + }); + $(window).on('resize', closeNoteBox); + + function getPositionInBookText($e) { + let x = 0, y = 0; + + // Ok dla Y, nie ok dla X + + while ($e.attr('id') != 'book-text') { + let p = $e.position(); + x += p.left; + y += p.top; + $e = $e.offsetParent(); + break; + } + return {"x": x, "y": y} + } + + $('#book-text .annotation').on('click', function(event) { + if ($(this).parents('#footnotes').length) return; + event.preventDefault(); + + + + let x = $(this).width() / 2, y = 0; + let elem = $(this); + while (elem.attr('id') != 'book-text') { + let p = $(elem).position(); + x += p.left; + y += p.top; + elem = elem.parent(); + } + href = $(this).attr('href').substr(1); + content = $("[name='" + href + "']").next().next().html(); + if (!content) return; + $("#annotation-content").html(content); + $("#footnote-link").attr('href', '#' + href) + + + putNoteAt($('#annotation-box'), this); + event.stopPropagation(); + }); + + + + let zakladki = {}; + $.get({ + url: '/zakladki/', + success: function(data) { + zakladki = data; + $.each(zakladki, (i, e) => { + zakladkaUpdateFor( + // TODO: not just paragraphs. + $('[href="#' + e.anchor + '"]').nextAll('.paragraph').first() + ); + }); + } + }); + + // TODO: create bookmarks on init + // We need to do that from anchors. + + function zakladkaUpdateFor($item) { + + let anchor = $item.prevAll('.target').first().attr('name'); + + if (anchor in zakladki) { + let $booktag = $item.data('booktag'); + if (!$booktag) { + + // TODO: only copy without the dialog. + $booktag = $("
"); + $booktag.append($('.icon', $zakladka).clone()); + + $item.data('booktag', $booktag); + $booktag.data('p', $item); + $booktag.data('anchor', anchor); + $zakladka.after($booktag); + + zakladkaSetPosition($booktag); + $booktag.show(); + } + + $z = $booktag; + if (zakladki[anchor].note) { + $z.removeClass('zakladka-exists'); + $z.addClass('zakladka-note'); + } else { + $z.removeClass('zakladka-note'); + $z.addClass('zakladka-exists'); + } + } else { + let $booktag = $item.data('booktag'); + if ($booktag) { + $item.data('booktag', null); + $zakladka.append($("#zakladka-box")); + $booktag.remove(); + } + } + } + + function zakladkaSetPosition($z) { + $item = $z.data('p'); + pos = getPositionInBookText($item); + $z.css({ + display: 'block', + top: pos.y, + right: ($('#main-text').width() - $('#book-text').width()) / 2, + }); + } + + let $zakladka = $('#zakladka'); + $('#book-text .paragraph').on('mouseover', function() {showMarker($(this));}); + $('#book-text .verse').on('mouseover', function() {showMarker($(this));}); + //$.PMarker.showForP(this); + + + function showMarker(p) { + + // Close the currently tag box when moving to another one. + // TBD: Do we want to keep the box open and prevent moving? + $("#zakladka-box").hide(); + + let anchor = p.prevAll('.target').first().attr('name'); + // Don't bother if there's no anchor to use. + if (!anchor) return; + + // Only show tag if there is not already a tag for this p. + if (p.data('booktag')) { + $zakladka.hide(); + } else { + $zakladka.data('p', p); + $zakladka.data('anchor', anchor); + + // (not needed) zakladkaUpdateClass(); + // TODO: UPDATE THIS ON OPEN? + //let note = anchor in zakladki ? zakladki[anchor].note : ''; + //$('textarea', $zakladka).val(note); + + zakladkaSetPosition($zakladka); + $zakladka.show(); + } + } + + $(".zakladka-tool_zakladka").on('click', function() { + let $z = $("#zakladka-box").data('z'); + let anchor = $z.data('anchor'); + let $p = $z.data('p'); + $.post({ + url: '/zakladki/', + data: { + csrfmiddlewaretoken: csrf, + anchor: anchor + }, + success: function(data) { + zakladki[data.anchor] = data; + $("#zakladka-box").hide(); + + // Just hide, and create new .zakladka if not already exists? + // In general no hiding 'classed' .zakladka. + // So the 'cursor' .zakladka doesn't ever need class update. + //zakladkaUpdateClass(); + zakladkaUpdateFor($p); + + } + }); + }); + + $(".zakladka-tool_notka_text textarea").on('input', function() { + // FIXME: no use const $zakladka here, check which .zakladka are we attached to. + let $z = $(this).closest('.zakladka'); + let anchor = $z.data('anchor'); + + $("#notka-saved").hide(); + //$("#notka-save").show(); + $.post({ + url: '/zakladki/' + zakladki[anchor].uuid + '/', + data: { + csrfmiddlewaretoken: csrf, + note: $(this).val() + }, + success: function(data) { + zakladki[anchor] = data; + zakladkaUpdateFor($z.data('p')); + $("#notka-save").hide(); + $("#notka-saved").fadeIn(); + } + }); + }); + + $(".zakladka-tool_zakladka_delete").on('click', function() { + let $z = $(this).closest('.zakladka'); + let anchor = $z.data('anchor'); + $.post({ + url: '/zakladki/' + zakladki[anchor].uuid + '/delete/', + data: { + csrfmiddlewaretoken: csrf, + }, + success: function(data) { + delete zakladki[anchor]; + $("#zakladka-box").hide(); + zakladkaUpdateFor($z.data('p')); + } + }); + }); + + $("#main-text").on("click", ".zakladka .icon", function() { + let $z = $(this).closest('.zakladka'); + let $box = $("#zakladka-box"); + $z.append($box); + $box.data('z', $z); + + let $p = $z.data('p'); + let anchor = $z.data('anchor'); + let note = anchor in zakladki ? zakladki[anchor].note : ''; + + $('.zakladka-tool_zakladka', $box).toggle(!(anchor in zakladki)); + $('.zakladka-tool_sluchaj', $box).toggle($p.hasClass('syncable')).data('sync', $p.attr('id')); + $('textarea', $box).val(note); + + $box.toggle(); + }); + + + class QBox { + constructor(qbox) { + this.qbox = qbox; + } + showForSelection(sel) { + // TODO: only consider ranges inside text.? + this.selection = sel; + + // TODO: multiple ranges. + let range = sel.getRangeAt(0); + let rect = range.getBoundingClientRect(); + + putNoteAt(this.qbox, range) + } + showForBlock(b) { + let rect = b.getBoundingClientRect(); + + putNoteAt(this.qbox, b, 'left') + } + hide() { + this.qbox.data('anchoredTo', null); + this.qbox.fadeOut(); + } + hideCopied() { + this.qbox.data('anchoredTo', null); + this.qbox.addClass('copied').fadeOut(1500, () => { + this.qbox.removeClass('copied'); + }); + } + + copyText() { + // TODO: only consider ranges inside text.? + let range = this.selection.getRangeAt(0); + let e = range.startContainer; + let anchor = getIdForElem(e); + let text = window.location.protocol + '//' + + window.location.host + + window.location.pathname; + + navigator.clipboard.writeText( + this.selection.toString() + + '\n\nCałość czytaj na: ' + text + ); + this.hideCopied(); + } + copyLink() { + // TODO: only consider ranges inside text.? + let range = this.selection.getRangeAt(0); + let e = range.startContainer; + let anchor = getIdForElem(e); + let text = window.location.protocol + '//' + + window.location.host + + window.location.pathname; + if (anchor) text += anchor; + navigator.clipboard.writeText(text); + + this.hideCopied(); + } + quote() { + // What aboot non-contiguous selections? + let sel = this.selection; + let textContent = sel.toString(); + let anchor = getIdForElem(sel.getRangeAt(0).startContainer); + let paths = getSelectionPaths(sel); + $.post({ + url: '/cytaty/', + data: { + csrfmiddlewaretoken: csrf, + text: textContent, + startElem: anchor, + //endElem: endElem, + //startOffset: 0, + //endOffset: 0, + paths: paths, + }, + success: function (data) { + var win = window.open('/cytaty/' + data.uuid + '/', '_blank'); + } + }); + + } + + } + let qbox = new QBox($("#qbox")); + + + function getPathToNode(elem) { + // Need normalize? + let path = []; + while (elem.id != 'book-text') { + let p = elem.parentElement; + path.unshift([...p.childNodes].indexOf(elem)) + elem = p; + } + return path; + } + function getSelectionPaths(selection) { + // does it work? + let range1 = selection.getRangeAt(0); + let range2 = selection.getRangeAt(selection.rangeCount - 1); + let paths = [ + getPathToNode(range1.startContainer) + [range1.startOffset], + getPathToNode(range2.endContainer) + [range2.endOffset] + ] + return paths; + } + + + function getIdForElem(elem) { + // is it used? + let $elem = $(elem); + // check if inside book-text? + + while (true) { + if ($elem.hasClass('target')) { + return $elem.attr('name'); + } + $p = $elem.prev(); + if ($p.length) { + $elem = $p; + } else { + // Gdy wychodzimy w górę -- to jest ten moment, w którym znajdujemy element od którego wychodzimy i zliczamy znaki. + + + $p = $elem.parent() + if ($p.length) { + // is there text? + $elem = $p; + } else { + return undefined; + } + } + } + } + + function getIdForElem(elem) { + // is it used? + // check if inside book-text? + $elem = $(elem); + while (true) { + if ($elem.hasClass('target')) { + return $elem.attr('name'); + } + $p = $elem.prev(); + if ($p.length) { + $elem = $p; + } else { + $p = $elem.parent() + if ($p.length) { + // is there text? + $elem = $p; + } else { + return undefined; + } + } + } + } + + + function positionToIIDOffset(container, offset) { + // Container and offset follow Range rules. + // If container is a text node, offset is text offset. + // If container is an element node, offset is number of child nodes from container start. + // (containers of type Comment, CDATASection - ignore)z + } + + + function updateQBox() { + sel = document.getSelection(); + let goodS = true; + if (sel.isCollapsed || sel.rangeCount < 1) { + goodS = false; + } + + if (!goodS) { + qbox.hide(); + } else { + qbox.showForSelection(sel); + } + }; + $(document).on('selectionchange', updateQBox); + + function updateBoxes() { + updateNote(qbox.qbox); + updateNote($('#annotation-box')); + + } + $(window).on('scroll', updateBoxes); + $(window).on('resize', updateBoxes); + + + $(window).on('resize', function() { + $('.zakladka').each(function() { + zakladkaSetPosition($(this)); + }); + }); + + $('a.anchor').on('click', function(e) { + e.preventDefault(); + + let sel = window.getSelection(); + sel.removeAllRanges(); + let range = document.createRange(); + + let $p = $(this).nextAll('.paragraph').first() + range.selectNode($p[0]); + sel.addRange(range); + + qbox.showForSelection(sel); + + showMarker($p); + }); + + + + $('.qbox-t-copy').on('click', function(e) { + e.preventDefault(); + qbox.copyText(); + }); + $('.qbox-t-link').on('click', function(e) { + e.preventDefault(); + qbox.copyLink(); + }); + $('.qbox-t-quote').on('click', function(e) { + e.preventDefault(); + qbox.quote(); + }); + + + /* + $(".paragraph").on('click', function(e) { + qbox.showForBlock(this); + }); + */ + + + function scrollToAnchor(anchor) { + if (anchor) { + var anchor_name = anchor.slice(1); + var element = $('a[name="' + anchor_name + '"]'); + if (element.length > 0) { + $("html").animate({ + scrollTop: element.offset().top - 55 + }, { + duration: 500, + done: function() { + history.pushState({}, '', anchor); + }, + }); + } + } + } + scrollToAnchor(window.location.hash) + $('#toc, #themes, #book-text, #annotation').on('click', 'a', function(event) { + event.preventDefault(); + scrollToAnchor($(this).attr('href')); + }); + + })})(jQuery); diff --git a/src/wolnelektury/static/js/contrib/jquery.scrollto.js b/src/wolnelektury/static/js/contrib/jquery.scrollto.js index c403ab9df..0717e0aa6 100644 --- a/src/wolnelektury/static/js/contrib/jquery.scrollto.js +++ b/src/wolnelektury/static/js/contrib/jquery.scrollto.js @@ -1,194 +1,215 @@ -/** - * jQuery.ScrollTo - * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com - * Dual licensed under MIT and GPL. - * Date: 9/11/2008 - * - * @projectDescription Easy element scrolling using jQuery. - * http://flesler.blogspot.com/2007/10/jqueryscrollto.html - * Tested with jQuery 1.2.6. On FF 2/3, IE 6/7, Opera 9.2/5 and Safari 3. on Windows. - * +/*! + * jQuery.scrollTo + * Copyright (c) 2007 Ariel Flesler - aflesler ○ gmail • com | https://github.com/flesler + * Licensed under MIT + * https://github.com/flesler/jquery.scrollTo + * @projectDescription Lightweight, cross-browser and highly customizable animated scrolling with jQuery * @author Ariel Flesler - * @version 1.4 - * - * @id jQuery.scrollTo - * @id jQuery.fn.scrollTo - * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. - * The different options for target are: - * - A number position (will be applied to all axes). - * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes - * - A jQuery/DOM element ( logically, child of the element to scroll ) - * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) - * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. - * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead. - * @param {Object,Function} settings Optional set of settings or the onAfter callback. - * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. - * @option {Number} duration The OVERALL length of the animation. - * @option {String} easing The easing method for the animation. - * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. - * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. - * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. - * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. - * @option {Function} onAfter Function to be called after the scrolling ends. - * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. - * @return {jQuery} Returns the same jQuery object, for chaining. - * - * @desc Scroll to a fixed position - * @example $('div').scrollTo( 340 ); - * - * @desc Scroll relatively to the actual position - * @example $('div').scrollTo( '+=340px', { axis:'y' } ); - * - * @dec Scroll using a selector (relative to the scrolled element) - * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); - * - * @ Scroll to a DOM element (same for jQuery object) - * @example var second_child = document.getElementById('container').firstChild.nextSibling; - * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ - * alert('scrolled!!'); - * }}); - * - * @desc Scroll on both axes, to different values - * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); + * @version 2.1.3 */ -;(function( $ ){ - - var $scrollTo = $.scrollTo = function( target, duration, settings ){ - $(window).scrollTo( target, duration, settings ); +;(function(factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module !== 'undefined' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Global + factory(jQuery); + } +})(function($) { + 'use strict'; + + var $scrollTo = $.scrollTo = function(target, duration, settings) { + return $(window).scrollTo(target, duration, settings); }; $scrollTo.defaults = { - axis:'y', - duration:1 + axis:'xy', + duration: 0, + limit:true }; - // Returns the element that needs to be animated to scroll the window. - // Kept for backwards compatibility (specially for localScroll & serialScroll) - $scrollTo.window = function( scope ){ - return $(window).scrollable(); - }; + function isWin(elem) { + return !elem.nodeName || + $.inArray(elem.nodeName.toLowerCase(), ['iframe','#document','html','body']) !== -1; + } - // Hack, hack, hack... stay away! - // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) - $.fn.scrollable = function(){ - return this.map(function(){ - // Just store it, we might need it - var win = this.parentWindow || this.defaultView, - // If it's a document, get its iframe or the window if it's THE document - elem = this.nodeName == '#document' ? win.frameElement || win : this, - // Get the corresponding document - doc = elem.contentDocument || (elem.contentWindow || elem).document, - isWin = elem.setInterval; - - return elem.nodeName == 'IFRAME' || isWin && $.browser.safari ? doc.body - : isWin ? doc.documentElement - : this; - }); - }; + function isFunction(obj) { + // Brought from jQuery since it's deprecated + return typeof obj === 'function' + } - $.fn.scrollTo = function( target, duration, settings ){ - if( typeof duration == 'object' ){ + $.fn.scrollTo = function(target, duration, settings) { + if (typeof duration === 'object') { settings = duration; duration = 0; } - if( typeof settings == 'function' ) + if (typeof settings === 'function') { settings = { onAfter:settings }; + } + if (target === 'max') { + target = 9e9; + } - settings = $.extend( {}, $scrollTo.defaults, settings ); + settings = $.extend({}, $scrollTo.defaults, settings); // Speed is still recognized for backwards compatibility - duration = duration || settings.speed || settings.duration; + duration = duration || settings.duration; // Make sure the settings are given right - settings.queue = settings.queue && settings.axis.length > 1; - - if( settings.queue ) + var queue = settings.queue && settings.axis.length > 1; + if (queue) { // Let's keep the overall duration duration /= 2; - settings.offset = both( settings.offset ); - settings.over = both( settings.over ); + } + settings.offset = both(settings.offset); + settings.over = both(settings.over); + + return this.each(function() { + // Null target yields nothing, just like jQuery does + if (target === null) return; - return this.scrollable().each(function(){ - var elem = this, + var win = isWin(this), + elem = win ? this.contentWindow || window : this, $elem = $(elem), - targ = target, toff, attr = {}, - win = $elem.is('html,body'); + targ = target, + attr = {}, + toff; - switch( typeof targ ){ + switch (typeof targ) { // A number will pass the regex case 'number': case 'string': - if( /^([+-]=)?\d+(px)?$/.test(targ) ){ - targ = both( targ ); + if (/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)) { + targ = both(targ); // We are done break; } - // Relative selector, no break! - targ = $(targ,this); + // Relative/Absolute selector + targ = win ? $(targ) : $(targ, elem); + /* falls through */ case 'object': + if (targ.length === 0) return; // DOMElement / jQuery - if( targ.is || targ.style ) + if (targ.is || targ.style) { // Get the real position of the target toff = (targ = $(targ)).offset(); + } } - $.each( settings.axis.split(''), function( i, axis ){ - var Pos = axis == 'x' ? 'Left' : 'Top', + + var offset = isFunction(settings.offset) && settings.offset(elem, targ) || settings.offset; + + $.each(settings.axis.split(''), function(i, axis) { + var Pos = axis === 'x' ? 'Left' : 'Top', pos = Pos.toLowerCase(), key = 'scroll' + Pos, - old = elem[key], - Dim = axis == 'x' ? 'Width' : 'Height', - dim = Dim.toLowerCase(); + prev = $elem[key](), + max = $scrollTo.max(elem, axis); - if( toff ){// jQuery / DOMElement - attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); + if (toff) {// jQuery / DOMElement + attr[key] = toff[pos] + (win ? 0 : prev - $elem.offset()[pos]); // If it's a dom element, reduce the margin - if( settings.margin ){ - attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; - attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; + if (settings.margin) { + attr[key] -= parseInt(targ.css('margin'+Pos), 10) || 0; + attr[key] -= parseInt(targ.css('border'+Pos+'Width'), 10) || 0; } - attr[key] += settings.offset[pos] || 0; + attr[key] += offset[pos] || 0; - if( settings.over[pos] ) + if (settings.over[pos]) { // Scroll to a fraction of its width/height - attr[key] += targ[dim]() * settings.over[pos]; - }else - attr[key] = targ[pos]; + attr[key] += targ[axis === 'x'?'width':'height']() * settings.over[pos]; + } + } else { + var val = targ[pos]; + // Handle percentage values + attr[key] = val.slice && val.slice(-1) === '%' ? + parseFloat(val) / 100 * max + : val; + } // Number or 'number' - if( /^\d+$/.test(attr[key]) ) + if (settings.limit && /^\d+$/.test(attr[key])) { // Check the limits - attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max(Dim) ); + attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max); + } - // Queueing axes - if( !i && settings.queue ){ - // Don't waste time animating, if there's no need. - if( old != attr[key] ) + // Don't waste time animating, if there's no need. + if (!i && settings.axis.length > 1) { + if (prev === attr[key]) { + // No animation needed + attr = {}; + } else if (queue) { // Intermediate animation - animate( settings.onAfterFirst ); - // Don't animate this axis again in the next iteration. - delete attr[key]; + animate(settings.onAfterFirst); + // Don't animate this axis again in the next iteration. + attr = {}; + } } }); - animate( settings.onAfter ); - function animate( callback ){ - $elem.animate( attr, duration, settings.easing, callback && function(){ - callback.call(this, target, settings); + animate(settings.onAfter); + + function animate(callback) { + var opts = $.extend({}, settings, { + // The queue setting conflicts with animate() + // Force it to always be true + queue: true, + duration: duration, + complete: callback && function() { + callback.call(elem, targ, settings); + } }); - }; - function max( Dim ){ - var attr ='scroll'+Dim, - doc = elem.ownerDocument; - - return win - ? Math.max( doc.documentElement[attr], doc.body[attr] ) - : elem[attr]; - }; - }).end(); + $elem.animate(attr, opts); + } + }); + }; + + // Max scrolling position, works on quirks mode + // It only fails (not too badly) on IE, quirks mode. + $scrollTo.max = function(elem, axis) { + var Dim = axis === 'x' ? 'Width' : 'Height', + scroll = 'scroll'+Dim; + + if (!isWin(elem)) + return elem[scroll] - $(elem)[Dim.toLowerCase()](); + + var size = 'client' + Dim, + doc = elem.ownerDocument || elem.document, + html = doc.documentElement, + body = doc.body; + + return Math.max(html[scroll], body[scroll]) - Math.min(html[size], body[size]); }; - function both( val ){ - return typeof val == 'object' ? val : { top:val, left:val }; + function both(val) { + return isFunction(val) || $.isPlainObject(val) ? val : { top:val, left:val }; + } + + // Add special hooks so that window scroll properties can be animated + $.Tween.propHooks.scrollLeft = + $.Tween.propHooks.scrollTop = { + get: function(t) { + return $(t.elem)[t.prop](); + }, + set: function(t) { + var curr = this.get(t); + // If interrupt is true and user scrolled, stop animating + if (t.options.interrupt && t._last && t._last !== curr) { + return $(t.elem).stop(); + } + var next = Math.round(t.now); + // Don't waste CPU + // Browsers don't render floating point scroll + if (curr !== next) { + $(t.elem)[t.prop](next); + t._last = this.get(t); + } + } }; -})( jQuery ); \ No newline at end of file + // AMD requirement + return $scrollTo; +}); diff --git a/src/wolnelektury/urls.py b/src/wolnelektury/urls.py index afee7f1f9..cfecb5078 100644 --- a/src/wolnelektury/urls.py +++ b/src/wolnelektury/urls.py @@ -57,6 +57,7 @@ urlpatterns += [ path('towarzystwo/', RedirectView.as_view(url='/pomagam/', permanent=False, query_string=True)), path('towarzystwo/', RedirectView.as_view( url='/pomagam/%(path)s', permanent=False)), + path('', include('bookmarks.urls')), path('chunks/', include('chunks.urls')),