Repository: comments-ink/django-comments-ink (language: python)

Issue:
Allow decreasing `max_thread_level`
If I had a site with a `COMMENTS_INK_MAX_THREAD_LEVEL` of say, 3, and wanted to switch to a value of 1, the list of comments is displayed somehow broken, because the QuerySet contains all the comments, even those with a thread_level beyond 1.

This issue is about allowing the user to change the COMMENTS_INK_MAX_THREAD_LEVEL so that when decreasing the value the list of comments still looks good. Increasing the value is handled correctly. Also, when decreasing it, the value of `nested_count` has to be recomputed for every comment in the tree. Use the management command, `initialized_nested_count` of django-comments-xtd for this purpose.


Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index c5594a1..84296ba 100644
--- a/Makefile
+++ b/Makefile
@@ -12,10 +12,10 @@ coverage:  ## Run tests with coverage.
 	@sh ./ccsvg.sh ||:
 
 syntax-check:  ## Check syntax code (isort and black).
-	black --check django_comments_ink demos
+	black --check django_comments_ink
 
 syntax-format:  ## Format syntax code (isort and black).
-	black django_comments_ink demos
+	black django_comments_ink
 
 tox:  ## Run tox.
 	python -m tox
diff --git a/coverage.svg b/coverage.svg
index 0fa9649..b6c4e36 100644
--- a/coverage.svg
+++ b/coverage.svg
@@ -15,7 +15,7 @@
     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
         <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
         <text x="31.5" y="14">coverage</text>
-        <text x="80" y="15" fill="#010101" fill-opacity=".3">98%</text>
-        <text x="80" y="14">98%</text>
+        <text x="80" y="15" fill="#010101" fill-opacity=".3">95%</text>
+        <text x="80" y="14">95%</text>
     </g>
 </svg>
diff --git a/django_comments_ink/api/frontend.py b/django_comments_ink/api/frontend.py
index e07e50b..1dd0f47 100644
--- a/django_comments_ink/api/frontend.py
+++ b/django_comments_ink/api/frontend.py
@@ -61,8 +61,7 @@ def comments_api_props(obj, user, request=None):
         is_public=True,
     )
     ctype_slug = "%s-%s" % (ctype.app_label, ctype.model)
-    app_model = "%s.%s" % (ctype.app_label, ctype.model)
-    options = get_app_model_options(content_type=app_model)
+    options = get_app_model_options(content_type=ctype)
     check_input_allowed_str = options.pop("check_input_allowed")
     check_func = import_string(check_input_allowed_str)
     d = {
diff --git a/django_comments_ink/api/serializers.py b/django_comments_ink/api/serializers.py
index acb100c..5f79308 100644
--- a/django_comments_ink/api/serializers.py
+++ b/django_comments_ink/api/serializers.py
@@ -1,4 +1,5 @@
 from django.apps import apps
+from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.shortcuts import get_current_site
 from django.utils import formats, timezone
 from django.utils.html import escape
@@ -92,26 +93,31 @@ def validate_reply_to(self, value):
         return value
 
     def validate(self, data):
-        ctype = data.get("content_type")
+        content_type = data.get("content_type")
         object_pk = data.get("object_pk")
         try:
-            model = apps.get_model(*ctype.split(".", 1))
+            model = apps.get_model(*content_type.split(".", 1))
             target = model._default_manager.get(pk=object_pk)
+            ctype = ContentType.objects.get_for_model(model)
             whocan = get_app_model_options(content_type=ctype)["who_can_post"]
         except (AttributeError, TypeError, LookupError):
             raise serializers.ValidationError(
-                "Invalid content_type value: %r" % escape(ctype)
+                "Invalid content_type value: %r" % escape(content_type)
             )
         except model.DoesNotExist:
             raise serializers.ValidationError(
                 "No object matching content-type %r and object PK %r."
-                % (escape(ctype), escape(object_pk))
+                % (escape(content_type), escape(object_pk))
             )
         except (ValueError, serializers.ValidationError) as e:
             raise serializers.ValidationError(
                 "Attempting to get content-type %r and object PK %r "
                 "raised %s"
-                % (escape(ctype), escape(object_pk), e.__class__.__name__)
+                % (
+                    escape(content_type),
+                    escape(object_pk),
+                    e.__class__.__name__,
+                )
             )
         else:
             if whocan == "users" and not self.request.user.is_authenticated:
diff --git a/django_comments_ink/conf/defaults.py b/django_comments_ink/conf/defaults.py
index 1633a54..784b19f 100644
--- a/django_comments_ink/conf/defaults.py
+++ b/django_comments_ink/conf/defaults.py
@@ -52,6 +52,8 @@
         # Function to determine whether new comments,
         # reactions, etc. should be allowed for a given object.
         "check_input_allowed": "django_comments_ink.utils.check_input_allowed",
+        # Whether to allow users to vote for comments.
+        "comment_votes_enabled": False,
         # Whether to display a link to flag a comment as inappropriate.
         "comment_flagging_enabled": False,
         # Whether to allow users to submit reactions on comments.
diff --git a/django_comments_ink/management/commands/update_nested_count.py b/django_comments_ink/management/commands/update_nested_count.py
new file mode 100644
index 0000000..d142522
--- /dev/null
+++ b/django_comments_ink/management/commands/update_nested_count.py
@@ -0,0 +1,113 @@
+import logging
+
+from django.contrib.contenttypes.models import ContentType
+from django.core.management.base import BaseCommand
+from django.db.models import Q
+from django.db.utils import ConnectionDoesNotExist
+
+from django_comments_ink.conf import settings
+from django_comments_ink.models import InkComment
+
+
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+    help = "Initialize the nested_count field for all the comments in the DB."
+
+    def add_arguments(self, parser):
+        parser.add_argument("using", nargs="*", type=str)
+
+    def update_nested_count(self, using):
+        total = 0
+        ctype_list = []
+
+        # Check if the COMMENTS_INK_MAX_THREAD_LEVEL_BY_APP_MODEL is defined
+        # and if so, iterate over each group of app_label.model, and apply
+        # the update of the nested_count on per app_label.model group.
+        # If that setting only contains the default, then do the update to all
+        # the InkComments. Otherwise use the COMMENTS_INK_MAX_THREAD_LEVEL.
+
+        # 1st: Process comments on per app_model basis.
+        MTLs = settings.COMMENTS_INK_MAX_THREAD_LEVEL_BY_APP_MODEL
+        for app_model, mtl in MTLs.items():
+            bits = app_model.split(".")
+            app, model = ".".join(bits[:-1]), bits[-1]
+            try:
+                ctype = ContentType.objects.get(app_label=app, model=model)
+                ctype_list.append(ctype)
+            except ContentType.DoesNotExist as exc:
+                logger.warn("app.model '%s' does not exist", app_model)
+            else:
+                qs = (
+                    InkComment.objects.using(using)
+                    .filter(content_type=ctype, level__lte=mtl)
+                    .order_by("thread_id", "-order")
+                )
+                count = self.process_queryset(qs)
+                total += count
+                self.stdout.write(
+                    f"Updated {count} InkComments for {app_model}."
+                )
+
+        if not len(MTLs):
+            self.stdout.write(f"")
+
+        # 2nd: Process the rest of the comments.
+        MTL = settings.COMMENTS_INK_MAX_THREAD_LEVEL
+        if len(ctype_list):
+            qs = (
+                InkComment.objects.using(using)
+                .filter(~Q(content_type__in=ctype_list), level__lte=MTL)
+                .order_by("thread_id", "-order")
+            )
+            count = self.process_queryset(qs)
+            total += count
+            self.stdout.write(f"Updated additional {count} InkComments.")
+        else:
+            qs = (
+                InkComment.objects.using(using)
+                .filter(level__lte=MTL)
+                .order_by("thread_id", "-order")
+            )
+            total = self.process_queryset(qs)
+            self.stdout.write(f"Updated {total} InkComments.")
+
+        return total
+
+    def process_queryset(self, qs):
+        active_thread_id = -1
+        parents = {}
+
+        for comment in qs:
+            # Clean up parents when there is a control break.
+            if comment.thread_id != active_thread_id:
+                parents = {}
+                active_thread_id = comment.thread_id
+
+            nested_count = parents.get(comment.comment_ptr_id, 0)
+            parents.setdefault(comment.parent_id, 0)
+            if nested_count > 0:
+                parents[comment.parent_id] += 1 + nested_count
+            else:
+                parents[comment.parent_id] += 1
+            comment.nested_count = nested_count
+            comment.save()
+
+        return qs.count()
+
+    def handle(self, *args, **options):
+        total = 0
+        using = options["using"] or ["default"]
+
+        for db_conn in using:
+            try:
+                total += self.update_nested_count(db_conn)
+            except ConnectionDoesNotExist:
+                self.stdout.write(
+                    "DB connection '%s' does not exist." % db_conn
+                )
+                continue
+        self.stdout.write(
+            "%d InkComment object(s) processed in all DBs." % total
+        )
diff --git a/django_comments_ink/static/django_comments_ink/css/comments.css b/django_comments_ink/static/django_comments_ink/css/comments.css
index 78caf6f..d49f0fe 100644
--- a/django_comments_ink/static/django_comments_ink/css/comments.css
+++ b/django_comments_ink/static/django_comments_ink/css/comments.css
@@ -547,6 +547,10 @@
     font-size: 0.8rem;
 }
 
+.dci .comment-box .body, .dci .comment-box-img .body {
+    font-size: 0.9rem;
+}
+
 .body-bordered {
     padding: 4px;
     border: 1px solid rgba(0, 0, 0, .1);
@@ -558,7 +562,11 @@
 }
 
 .dci .comment-box     .body > P,
-.dci .comment-box-img .body > P  { margin: .6rem; }
+.dci .comment-box-img .body > P  {
+    margin: .6rem;
+    line-height: 1.4;
+    font-size: 0.9rem;
+}
 
 .dci .comment-box     .body > P + P,
 .dci .comment-box-img .body > P + P { margin-top: 0; }
diff --git a/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js b/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js
index 48a00be..385ae2e 100644
--- a/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js
+++ b/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js
@@ -382,7 +382,7 @@ class ReactionsHandler {
                 "Cannot initialize reactions panel => There are " +
                 "no elements with [data-dci=reactions-panel].");
         }
-        this.active_visible_panel = 0;
+        this.active_visible_panel = "0";
         this.panels_visibility = new Map(); // Keys are 'comment_id'.
         this.event_handlers = [];
         this.add_event_listeners();
@@ -434,9 +434,9 @@ class ReactionsHandler {
         const data_attr = event.target.getAttribute("data-dci");
         if (!data_attr || data_attr !== "reactions-panel") {
             this.reactions_panel.hide();
-            if (this.active_visible_panel) {
+            if (this.active_visible_panel !== "0") {
                 this.panels_visibility.set(this.active_visible_panel, false);
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
             }
         }
     }
@@ -444,9 +444,9 @@ class ReactionsHandler {
     on_document_key_up(event) {
         if (event.key === "Escape") {
             this.reactions_panel.hide();
-            if (this.active_visible_panel) {
+            if (this.active_visible_panel !== "0") {
                 this.panels_visibility.set(this.active_visible_panel, false);
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
             }
         }
     }
@@ -501,7 +501,7 @@ class ReactionsHandler {
                 this.active_visible_panel = comment_id;
                 this.reactions_panel.show(event.target, comment_id);
             } else {
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
                 this.reactions_panel.hide();
             }
             this.panels_visibility.set(comment_id, !is_visible);
@@ -575,7 +575,7 @@ class ReactionsPanel {
     }
 
     add_event_listeners() {
-        const buttons = this.panel_el.querySelectorAll("BUTTON");
+        const buttons = this.panel_el.querySelectorAll("button");
         for (const btn of Array.from(buttons)) {
             btn.addEventListener("click", this.on_react_btn_click);
             btn.addEventListener("mouseover", this.on_react_btn_mouseover);
diff --git a/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js.map b/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js.map
index ae79fb4..525c00f 100644
--- a/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js.map
+++ b/django_comments_ink/static/django_comments_ink/dist/dci-0.2.0.js.map
@@ -1,1 +1,1 @@
-{"version":3,"file":"dci-0.2.0.js","mappings":";;;;;;;;;;;;;;AAAe;AACf;AACA;AACA;AACA;;AAEA,uBAAuB;;AAEvB,0BAA0B;;AAE1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,SAAS;AACT;AACA;AACA,cAAc;AACd;AACA;AACA,SAAS;;AAET;AACA,sBAAsB;AACtB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,iBAAiB;AACpE;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,mDAAmD,iBAAiB;AACpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC/G4C;AACK;AACP;;AAE1C;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gDAAgD,wDAAW;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAuD,uDAAiB;AACxE;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,mDAAc;AACjE,qDAAqD,mDAAc;AACnE;AACA;;AAEyB;;;;;;;;;;;;;;;AC1DV;AACf;AACA,8BAA8B;AAC9B;AACA;;AAEA,uCAAuC,SAAS;AAChD;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA,sCAAsC,GAAG;AACzC;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,2BAA2B,SAAS,GAAG,MAAM;AAC7C;AACA;AACA;AACA;AACA,wEAAwE,MAAM;AAC9E;AACA;AACA;AACA;AACA;;AAEA;AACA,+BAA+B,+BAA+B,GAAG;AACjE;;AAEA;AACA;AACA;;AAEA,mDAAmD,GAAG;AACtD;AACA,iCAAiC,GAAG;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,6CAA6C,MAAM;AACnD;AACA;AACA;AACA;;AAEA,wCAAwC;AACxC,wCAAwC;;AAExC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACjGmD;;AAEnD;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA,qDAAqD,0DAAgB;AACrE;AACA;AACA,SAAS;;AAET;AACA;;AAE0B;;;;;;;;;;;;;;;;ACvBqB;;AAEhC;AACf;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4CAA4C;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA,wDAAwD,SAAS;AACjE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mCAAmC,wDAAc;AACjD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb,2DAA2D;AAC3D;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AC7IA;AACA;;AAEA;AACA;AACA;AACA,gDAAgD;AAChD,wBAAwB,oBAAoB;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEe;AACf,iBAAiB,2CAA2C;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,6BAA6B;AAC7B,4BAA4B;;AAE5B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;AACA,aAAa;AACb,UAAU;AACV,sCAAsC,eAAe,QAAQ,cAAc;AAC3E;AACA;;AAEA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,kDAAkD,WAAW;AAC7D;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,sDAAsD,KAAK,MAAM,IAAI;;AAErE;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,kDAAkD,KAAK,MAAM,IAAI;;AAEjE;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AC9Me;AACf;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,aAAa;AAChE;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,gDAAgD,SAAS;;AAEzD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,mDAAmD,SAAS;AAC5D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,SAAS;AACT;AACA;AACA,cAAc;AACd;AACA;AACA,SAAS;AACT;AACA,sBAAsB;AACtB;;AAEA;AACA;AACA,mCAAmC;AACnC;AACA,wCAAwC,iBAAiB;AACzD;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;UCnOA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;ACN8C;AACE;;AAEhD;AACA,mBAAmB,uDAAa;AAChC,oBAAoB,yDAAc;AAClC;;AAEA;AACA,IAAI,2DAAa;AACjB,IAAI,6DAAc;AAClB,CAAC","sources":["webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/comment_form.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/comments.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/folding.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions_handler.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions_panel.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reply_forms.js","webpack://django-comments-ink/webpack/bootstrap","webpack://django-comments-ink/webpack/runtime/define property getters","webpack://django-comments-ink/webpack/runtime/hasOwnProperty shorthand","webpack://django-comments-ink/webpack/runtime/make namespace object","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/index.js"],"sourcesContent":["export default class CommentForm {\n    constructor(formWrapper) {\n        this.formWrapper = formWrapper;\n        this.init();\n    }\n\n    click_on_post(_) { return this.post(\"post\"); }\n\n    click_on_preview(_) { return this.post(\"preview\"); }\n\n    init() {\n        this.formWrapperEl = document.querySelector(this.formWrapper);\n        this.formEl = this.formWrapperEl.querySelector(\"form\");\n        const post_btn = this.formEl.elements.post;\n        const preview_btn = this.formEl.elements.preview;\n        post_btn.addEventListener(\"click\", (_) => this.post(\"post\"));\n        preview_btn.addEventListener(\"click\", (_) => this.post(\"preview\"));\n        // Change the type of the buttons, otherwise the form is submitted.\n        post_btn.type = \"button\";\n        preview_btn.type = \"button\";\n    }\n\n    disable_btns(value) {\n        this.formEl.elements.post.disabled = value;\n        this.formEl.elements.preview.disabled = value;\n    }\n\n    is_valid() {\n        for (const el of this.formEl.querySelectorAll(\"[required]\")) {\n            if (!el.reportValidity()) {\n                el.focus();\n                return false;\n            }\n        }\n        return true;\n    }\n\n    post(submit_button_name) {\n        if (!this.is_valid()) {\n            return;\n        }\n        this.disable_btns(true);\n\n        // If the <section data-dci=\"preview\">...</section> does exist,\n        // delete it. If the user clicks again in the \"preview\" button\n        // it will be displayed again.\n        const preview = this.formWrapperEl.querySelector(\"[data-dci=preview]\");\n        if (preview) {\n            preview.remove();\n        }\n\n        const formData = new FormData(this.formEl);\n        if (submit_button_name !== undefined) {\n            formData.append(submit_button_name, 1);\n        }\n\n        fetch(this.formEl.action, {\n            method: 'POST',\n            headers: {\n                \"X-Requested-With\": \"XMLHttpRequest\",\n            },\n            body: formData\n        }).then(response => {\n            if (submit_button_name === \"preview\") {\n                this.handle_preview_comment_response(response);\n            } else if (submit_button_name === \"post\") {\n                this.handle_post_comment_response(response);\n            }\n        });\n\n        this.disable_btns(false);\n        return false; // To prevent calling the action attribute.\n    }\n\n    async handle_preview_comment_response(response) {\n        const data = await response.json();\n        if (response.status === 200) {\n            this.formWrapperEl.innerHTML = data.html;\n            this.init();\n            if (data.field_focus) {\n                this.formEl.querySelector(`[name=${data.field_focus}]`).focus();\n            }\n        } else if (response.status === 400) {\n            this.formEl.innerHTML = data.html;\n        }\n    }\n\n    async handle_post_comment_response(response) {\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.formWrapperEl.innerHTML = data.html;\n            this.init();\n            if (data.field_focus) {\n                this.formEl.querySelector(`[name=${data.field_focus}]`).focus();\n            }\n        }\n        else if (\n            response.status === 201 ||\n            response.status === 202 ||\n            response.status === 400\n        ) {\n            this.formEl.innerHTML = data.html;\n        }\n        else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment could not be \" +\n                \"processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n}\n","import CommentForm from \"./comment_form.js\";\nimport ReplyFormsHandler from \"./reply_forms.js\";\nimport FoldingHandler from \"./folding.js\";\n\nfunction init_comments() {\n    if (window.djCommentsInk === null) {\n        return;\n    }\n\n    if (window.djCommentsInk.page_param === undefined) {\n        const rroot = document.querySelector(\"[data-dci=config]\");\n        if (rroot) {\n            window.djCommentsInk.page_param = rroot.getAttribute(\n                \"data-page-qs-param\"\n            );\n        }\n    }\n\n    window.djCommentsInk.comment_form = null;\n    window.djCommentsInk.reply_forms_handler = null;\n    window.djCommentsInk.folding_handler = null;\n    window.djCommentsInk.unfolding_handler = null;\n\n    /* ----------------------------------------------\n     * Initialize main comment form.\n     */\n    const qs_cform = \"[data-dci=comment-form]\";\n    if (window.djCommentsInk.comment_form === null &&\n        document.querySelector(qs_cform)\n    ) {\n        window.djCommentsInk.comment_form = new CommentForm(qs_cform);\n    }\n\n    /* ----------------------------------------------\n     * Initialize reply forms.\n     */\n    const qs_rform_base = \"[data-dci=reply-form-template]\";\n    const qs_rforms = \"[data-dci=reply-form]\";\n    if (window.djCommentsInk.reply_forms_handler === null &&\n        document.querySelector(qs_rform_base) &&\n        document.querySelectorAll(qs_rforms)\n    ) {\n        window.djCommentsInk.reply_forms_handler = new ReplyFormsHandler(\n            qs_rform_base, qs_rforms\n        );\n    }\n\n    /* ----------------------------------------------\n     * Initialize fold/unfold of comments with level > 0.\n     */\n    if (window.djCommentsInk.folding_handler === null &&\n        window.djCommentsInk.unfolding_handler === null\n    ) {\n        window.djCommentsInk.folding_handler = new FoldingHandler(\"fold\");\n        window.djCommentsInk.unfolding_handler = new FoldingHandler(\"unfold\");\n    }\n}\n\nexport { init_comments };\n","export default class FoldingHandler {\n    constructor(direction) {\n        this.dir = direction; // Direction can be either 'fold' or 'unfold'.\n        this.target_comment = undefined;\n        this.comment_replies = -1;\n\n        const qs = `[data-dci-action=${this.dir}]`;\n        const qs_links = document.querySelectorAll(qs);\n\n        const onLinkClickHandler = this.on_click.bind(this);\n\n        qs_links.forEach(elem => {\n            if (direction === \"fold\") {\n                // By default, when loading the page all comments are\n                // unfolded, so the 'fold' link has to be visible.\n                elem.classList.remove(\"hide\");\n            }\n            // If the fold link is clicked, it should trigger\n            // the folding of the comments underneath.\n            elem.addEventListener(\"click\", onLinkClickHandler);\n        });\n    }\n\n    on_click(event) {\n        event.preventDefault();\n        // Find the <div id=\"comment-{id}\"> of the clicked element.\n        this.target_comment = event.target.closest(\".comment\").parentNode;\n        // Read the number of sibling elements that have to be hidden.\n        this.comment_replies = parseInt(event.target.dataset.dciReplies);\n\n        this.fold_or_unfold_siblings();\n\n        // Hide the link clicked, and make visible the inverse link.\n        const bits = this.target_comment.getAttribute(\"id\").split(\"-\");\n        console.assert(bits.length === 2 && bits[0] === \"comment\");\n        const cm_id = parseInt(bits[1]);\n        this.turn_links(cm_id);\n    }\n\n    turn_links(cm_id) {\n        const dir_lid = `${this.dir}-${cm_id}`;\n        const direct_link = document.getElementById(dir_lid);\n        if (direct_link !== null) {\n            direct_link.classList.add(\"hide\");\n        }\n        const inv_lid = (this.dir === 'fold' ? 'unfold' : 'fold') + `-${cm_id}`;\n        const inverse_link = document.getElementById(inv_lid);\n        if (inverse_link !== null) {\n            inverse_link.classList.remove(\"hide\");\n        }\n    }\n\n    fold_or_unfold_siblings() {\n        let num_processed = 0; // Number of <div id=\"comment-{id}\"> processed.\n        let next_node = this.target_comment.nextElementSibling;\n\n        while(next_node && (num_processed < this.comment_replies)) {\n            const cm_attr_id = next_node.getAttribute(\"id\");\n            const bits = cm_attr_id.split(\"-\");\n\n            // If the node is a <div id=\"reply-to-{id}\"> element, skip it.\n            // They are hidden explicitly when processing the\n            // <div id=\"comment-{id}\"> counterpart.\n\n            if (\n                (bits.length === 3) &&\n                (bits[0] === \"reply\") && (bits[1] === \"to\")\n            ) {\n                next_node = next_node.nextElementSibling;\n                continue;\n            }\n            else if ((bits.length === 2) && (bits[0] === \"comment\")) {\n                const cm_id = parseInt(bits[1]);\n\n                // If the comment has a reply node, toggle it.\n                const reply_id = `reply-to-${cm_id}`;\n                const reply_node = document.getElementById(reply_id);\n                if(reply_node) {\n                    this.toggle(reply_node);\n                }\n\n                this.turn_links(cm_id); // Turn the fold/unfold links.\n                this.toggle(next_node); // Toggle the comment too.\n\n                next_node = next_node.nextElementSibling;\n                num_processed++;\n            }\n        }\n    }\n\n    toggle(node) {\n        if (this.dir === \"fold\") {\n            node.classList.add(\"hide\");\n        } else if (this.dir === \"unfold\") {\n            node.classList.remove(\"hide\");\n        }\n    }\n}\n","import ReactionsHandler from \"./reactions_handler\";\n\nfunction init_reactions() {\n    const rroot = document.querySelector(\"[data-dci=config]\");\n    if (rroot === null || window.djCommentsInk === null) {\n        return;\n    }\n\n    window.djCommentsInk.reactions_handler = null;\n\n    /* ----------------------------------------------\n     * Initialize reactions_handler, in charge\n     * of all reactions popover components.\n     */\n    if (window.djCommentsInk.reactions_handler === null) {\n        window.djCommentsInk.reactions_handler = new ReactionsHandler(rroot);\n        window.addEventListener(\"beforeunload\", (_) => {\n            window.djCommentsInk.reactions_handler.remove_event_listeners();\n        });\n\n    }\n}\n\nexport { init_reactions };\n","import ReactionsPanel from \"./reactions_panel\";\n\nexport default class ReactionsHandler {\n    constructor(configEl) {\n        this.cfg_el = configEl;\n        this.is_guest = this.cfg_el.getAttribute(\"data-guest-user\") === \"1\";\n        this.login_url = this.init_login_url();\n        this.react_url = this.init_react_url();\n\n        // Initialize the buttons panels and their components.\n        this.links = document.querySelectorAll(\"[data-dci=reactions-panel]\");\n        if (this.links.length === 0) {\n            throw new Error(\n                \"Cannot initialize reactions panel => There are \" +\n                \"no elements with [data-dci=reactions-panel].\");\n        }\n        this.active_visible_panel = 0;\n        this.panels_visibility = new Map(); // Keys are 'comment_id'.\n        this.event_handlers = [];\n        this.add_event_listeners();\n        this.listen_to_click_on_links();\n        const qs_panel = \"[data-dci=reactions-panel-template]\";\n        this.panel_el = document.querySelector(qs_panel);\n        if (this.panel_el === undefined) {\n            throw new Error(\"Cannot find element with ${qs_panel}.\");\n        }\n\n        // Create object of class ReactionsPanel in charge of showing and\n        // hiding the reactions panel around the clicked 'react' link.\n        const opts = {\n            panel_el: this.panel_el,\n            is_guest: this.is_guest,\n            login_url: this.login_url,\n            react_url: this.react_url\n        };\n        this.reactions_panel = new ReactionsPanel(opts);\n    }\n\n    init_login_url() {\n        const url = this.cfg_el.getAttribute(\"data-login-url\");\n        if (url === null || url.length === 0) {\n            if (this.is_guest) {\n                throw new Error(\"Cannot initialize reactions panel => The \" +\n                    \"[data-login-url] attribute does not exist or is empty.\");\n            }\n        }\n        return url;\n    }\n\n    init_react_url() {\n        const url = this.cfg_el.getAttribute(\"data-react-url\");\n        if (url === null || url.length === 0) {\n            if (!this.is_guest) {\n                throw new Error(\"Cannot initialize reactions panel => The \" +\n                    \"[data-react-url] attribute does not exist or is empty.\");\n            } else {\n                console.info(\"Couldn't find the data-react-url attribute, \" +\n                    \"but the user is anonymous. She has to login first in \" +\n                    \"order to post comment reactions.\");\n            }\n        }\n        return url;\n    }\n\n    on_document_click(event) {\n        const data_attr = event.target.getAttribute(\"data-dci\");\n        if (!data_attr || data_attr !== \"reactions-panel\") {\n            this.reactions_panel.hide();\n            if (this.active_visible_panel) {\n                this.panels_visibility.set(this.active_visible_panel, false);\n                this.active_visible_panel = 0;\n            }\n        }\n    }\n\n    on_document_key_up(event) {\n        if (event.key === \"Escape\") {\n            this.reactions_panel.hide();\n            if (this.active_visible_panel) {\n                this.panels_visibility.set(this.active_visible_panel, false);\n                this.active_visible_panel = 0;\n            }\n        }\n    }\n\n    add_event_listeners() {\n        const onDocumentClickHandler = this.on_document_click.bind(this);\n        const onDocumentKeyUpHandler = this.on_document_key_up.bind(this);\n\n        window.document.addEventListener('click', onDocumentClickHandler);\n        window.document.addEventListener('keyup', onDocumentKeyUpHandler);\n\n        this.event_handlers.push({\n            elem: window.document,\n            event: 'click',\n            handler: this.on_document_click,\n        });\n        this.event_handlers.push({\n            elem: window.document,\n            event: 'keyup',\n            handler: this.on_document_key_up,\n        });\n    }\n\n    remove_event_listeners() {\n        for (const item of this.event_handlers) {\n            item.elem.removeEventListener(item.event, item.handler);\n        }\n    }\n\n    listen_to_click_on_links() {\n        for (const elem of Array.from(this.links)) {\n            const comment_id = elem.getAttribute(\"data-comment\");\n            if (comment_id === null) {\n                continue;\n            }\n            const click_handler = this.toggle_reactions_panel(comment_id);\n            elem.addEventListener(\"click\", click_handler);\n            this.event_handlers.push({\n                'elem': elem,\n                'event': 'click',\n                'handler': click_handler\n            });\n            this.panels_visibility.set(comment_id, false); // Not visible yet.\n        }\n    }\n\n    toggle_reactions_panel(comment_id) {\n        return (event) => {\n            event.preventDefault();\n            const is_visible = this.panels_visibility.get(comment_id);\n            if (!is_visible) {\n                this.active_visible_panel = comment_id;\n                this.reactions_panel.show(event.target, comment_id);\n            } else {\n                this.active_visible_panel = 0;\n                this.reactions_panel.hide();\n            }\n            this.panels_visibility.set(comment_id, !is_visible);\n        };\n    }\n}\n","const enter_delay = 0;\nconst exit_delay = 0;\n\nfunction get_cookie(name) {\n    let cookieValue = null;\n    if (document.cookie && document.cookie !== '') {\n        const cookies = document.cookie.split(';');\n        for (let i = 0; i < cookies.length; i++) {\n            const cookie = cookies[i].trim();\n            // Does this cookie string begin with the name we want?\n            if (cookie.substring(0, name.length + 1) === (name + '=')) {\n                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\n                break;\n            }\n        }\n    }\n    return cookieValue;\n}\n\nexport default class ReactionsPanel {\n    constructor({panel_el, is_guest, login_url, react_url } = opts) {\n        this.panel_el = panel_el;\n        // this.panel_el.style.zIndex = 1;\n        // this.panel_el.style.display = \"block\";\n        this.arrow_el = panel_el.querySelector(\".arrow\");\n        this.is_guest = is_guest;\n        this.login_url = login_url;\n        this.react_url = react_url;\n\n        // -----------------------------------------------------\n        // The panel_title_elem and its content panel_title will\n        // change when the user hover the buttons of the panel.\n\n        this.panel_title = \"\";\n        this.panel_title_elem = this.panel_el.querySelector(\".title\");\n        if (this.panel_title_elem) {\n            this.panel_title = this.panel_title_elem.textContent;\n        }\n\n        // -----------------------------------------\n        // The comment_id is necessary to know which\n        // comment will receive the reaction code.\n\n        this.comment_id = 0; // Valid comment_id must be > 0.\n        this.next_url = \"\"; // Comment URL to come back after log in.\n\n        this.on_react_btn_click = this.on_react_btn_click.bind(this);\n        this.on_react_btn_mouseover = this.on_react_btn_mouseover.bind(this);\n        this.on_react_btn_mouseout = this.on_react_btn_mouseout.bind(this);\n        this.add_event_listeners();\n    }\n\n    add_event_listeners() {\n        const buttons = this.panel_el.querySelectorAll(\"BUTTON\");\n        for (const btn of Array.from(buttons)) {\n            btn.addEventListener(\"click\", this.on_react_btn_click);\n            btn.addEventListener(\"mouseover\", this.on_react_btn_mouseover);\n            btn.addEventListener(\"mouseout\", this.on_react_btn_mouseout);\n        }\n    }\n\n    on_react_btn_click(event) {\n        if (!this.is_guest) {\n            const code = event.target.dataset.code;\n            const react_url = this.react_url.replace(\"0\", this.comment_id);\n            const formData = new FormData();\n            formData.append(\"reaction\", code);\n            formData.append(\"csrfmiddlewaretoken\", get_cookie(\"csrftoken\"));\n\n            fetch(react_url, {\n                method: \"POST\",\n                cache: \"no-cache\",\n                credentials: \"same-origin\",\n                headers: {\n                    \"X-Requested-With\": \"XMLHttpRequest\",\n                },\n                body: formData\n            }).then(response => this.handle_reactions_response(response));\n        } else {\n            window.location.href = `${this.login_url}?next=${this.next_url}`;\n        }\n    }\n\n    async handle_reactions_response(response) {\n        const data = await response.json();\n        if (response.status === 200 || response.status === 201) {\n            const cm_reactions_qs = `#cm-reactions-${this.comment_id}`;\n            const cm_reactions_el = document.querySelector(cm_reactions_qs);\n            if (cm_reactions_el) {\n                cm_reactions_el.innerHTML = data.html;\n            }\n        } else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment reaction could not \" +\n                \"be processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n\n    on_react_btn_mouseover(event) {\n        if (this.panel_title_elem) {\n            this.panel_title_elem.textContent = event.target.dataset.title;\n        }\n    }\n\n    on_react_btn_mouseout(_) {\n        this.panel_title_elem.textContent = this.panel_title;\n    }\n\n    set_position(trigger_elem) {\n        this.panel_el.style.display = \"block\";\n\n        const panel_elem_coords = this.get_absolute_coords(this.panel_el);\n        const trigger_elem_coords = this.get_absolute_coords(trigger_elem);\n\n        const panel_elem_width = panel_elem_coords.width;\n        const panel_elem_height = panel_elem_coords.height;\n        const panel_elem_top = panel_elem_coords.top;\n        const panel_elem_left = panel_elem_coords.left;\n\n        const trigger_elem_width = trigger_elem_coords.width;\n        const trigger_elem_top = trigger_elem_coords.top;\n        const trigger_elem_left = trigger_elem_coords.left;\n\n        const top_diff = trigger_elem_top - panel_elem_top;\n        const left_diff = trigger_elem_left - panel_elem_left;\n\n        // This group of const values can be hardcoded somewhere else.\n        // const position = \"auto\";\n        const margin = 8;\n\n        const width_center = trigger_elem_width / 2 - panel_elem_width / 2;\n\n        const left = left_diff + width_center;\n        const top = top_diff - panel_elem_height - margin;\n        const from_top = top + 10;\n\n        this.panel_el.dataset.fromLeft = left;\n        this.panel_el.dataset.fromTop = from_top;\n        this.panel_el.dataset.left = left;\n        this.panel_el.dataset.top = top;\n\n        // Arrow.\n        if (this.arrow_el) {\n            let arrow_left = 0;\n            const full_left = left + panel_elem_left;\n            const t_width_center = trigger_elem_width / 2 + trigger_elem_left;\n            arrow_left = t_width_center - full_left;\n            const transform_text = `translate3d(${arrow_left}px, 0px, 0)`;\n            this.arrow_el.style.transform = transform_text;\n        }\n    }\n\n    hide() {\n        clearTimeout(this.enter_delay_timeout);\n\n        this.exit_delat_timeout = setTimeout(() => {\n            if (this.panel_el) {\n                const left = this.panel_el.dataset.fromLeft;\n                const top = this.panel_el.dataset.fromTop;\n                const transform_text = `translate3d(${left}px, ${top}px, 0)`;\n\n                this.panel_el.style.transform = transform_text;\n                this.panel_el.style.opacity = 0;\n                this.panel_el.style.display = \"none\";\n                this.panel_el.style.zIndex = 0;\n            }\n        }, exit_delay);\n    }\n\n    show(trigger_elem, comment_id) {\n        this.comment_id = comment_id;\n        this.next_url = trigger_elem.dataset.loginNext || \"\";\n        this.panel_el.style.transform = \"none\";\n        this.set_position(trigger_elem);\n\n        this.enter_delay_timeout = setTimeout(() => {\n            const left = this.panel_el.dataset.left;\n            const top = this.panel_el.dataset.top;\n            const transform_text = `translate3d(${left}px, ${top}px, 0)`;\n\n            this.panel_el.style.zIndex = 1;\n            this.panel_el.style.display = \"block\";\n            this.panel_el.style.transform = transform_text;\n            this.panel_el.style.opacity = 1;\n        }, enter_delay);\n    }\n\n    get_absolute_coords(elem) {\n        if (!elem) {\n            return;\n        }\n\n        const box = elem.getBoundingClientRect();\n        const page_x = window.pageXOffset;\n        const page_y = window.pageYOffset;\n\n        return {\n            width: box.width,\n            height: box.height,\n            top: box.top + page_y,\n            right: box.right + page_x,\n            bottom: box.bottom + page_y,\n            left: box.left + page_x,\n        };\n    }\n}\n","export default class ReplyFormsHandler {\n    constructor(qsReplyFormBase, qsReplyForms) {\n        this.replyFormBase = document.querySelector(qsReplyFormBase);\n        this.replyMap = new Map();\n\n        const cpage_field = window.djCommentsInk.page_param || \"cpage\";\n\n        for (const elem of document.querySelectorAll(qsReplyForms)) {\n            // Extract the reply_to value from the current reply_form.\n            // Also, it if does exist, extract the comment's page number too.\n            // Then replace the content of elem with a copy of\n            // this.replyFormBase and update the fields reply_to\n            // and comment's page number.\n            const rFormEl = elem.querySelector(\"form\");\n            if (rFormEl === null) {\n                console.error(\n                    `Could not find a reply form within one of ` +\n                    `the elements retrieved with ${qsReplyForms}.`\n                );\n                return;\n            }\n\n            const reply_to = rFormEl.elements.reply_to.value;\n            const cpage = rFormEl.elements[cpage_field]\n                ? rFormEl.elements[cpage_field].value\n                : null;\n\n            const section = this.replyFormBase.cloneNode(true);\n            section.dataset.dci = `reply-form-${reply_to}`;\n\n            // Update fields reply_to and cpage..\n            const newForm = section.querySelector(\"form\");\n            newForm.elements.reply_to.value = reply_to;\n            if (cpage) {\n                newForm.elements[cpage_field].value = cpage;\n            }\n\n            const elemParent = elem.parentNode;\n            elem.replaceWith(section);\n            this.init(reply_to);\n            this.replyMap.set(reply_to, elemParent);\n        }\n    }\n\n    init(reply_to, is_active) {\n        const qs_section = `[data-dci=reply-form-${reply_to}]`;\n        const section = document.querySelector(qs_section);\n\n        // Modify the form (update fields, add event listeners).\n        const newForm = section.querySelector(\"form\");\n        const post_btn = newForm.elements.post;\n        post_btn.addEventListener(\"click\", this.send_clicked(reply_to));\n        const preview_btn = newForm.elements.preview;\n        preview_btn.addEventListener(\"click\", this.preview_clicked(reply_to));\n        const cancel_btn = newForm.elements.cancel;\n        cancel_btn.addEventListener(\"click\", this.cancel_clicked(reply_to));\n        newForm.style.display = \"none\";\n\n        // Attach event listener to textarea.\n        const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n        const ta = divta.querySelector(\"textarea\");\n        ta.addEventListener(\"focus\", this.textarea_focus(reply_to));\n\n        // If is_active is true, hide the textarea and display the form.\n        if (is_active === true) {\n            section.classList.add(\"active\");\n            divta.style.display = \"none\";\n            newForm.style = \"\";\n            newForm.elements.comment.focus();\n        }\n    }\n\n    get_map_item(reply_to) {\n        const item = this.replyMap.get(reply_to);\n        if (item === undefined) {\n            const msg = `replyMap doesn't have a key ${reply_to}`;\n            console.error(msg);\n            throw msg;\n        }\n        return item;\n    }\n\n    disable_buttons(formEl, value) {\n        formEl.elements.post.disabled = value;\n        formEl.elements.preview.disabled = value;\n    }\n\n    is_valid(formEl) {\n        for (const el of formEl.querySelectorAll(\"[required]\")) {\n            if (!el.reportValidity()) {\n                el.focus();\n                return false;\n            }\n        }\n        return true;\n    }\n\n    textarea_focus(reply_to) {\n        // Display the comment form and hide the text area.\n        return (_) => {\n            const item = this.get_map_item(reply_to);\n            const qs_section = `[data-dci=reply-form-${reply_to}`;\n            const section = item.querySelector(qs_section);\n            const form = section.querySelector(\"form\");\n            const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n            section.classList.toggle(\"active\");\n            divta.style.display = \"none\";\n            form.style = \"\";\n            form.elements.comment.focus();\n        };\n    }\n\n    cancel_clicked(reply_to) {\n    // Display the text area and hide the comment form.\n        return (_) => {\n            const item = this.get_map_item(reply_to);\n            const qs_section = `[data-dci=reply-form-${reply_to}`;\n            const section = item.querySelector(qs_section);\n            const form = section.querySelector(\"form\");\n            const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n            const comment_value = form.elements.comment.value;\n            divta.querySelector(\"textarea\").value = comment_value;\n            section.classList.toggle(\"active\");\n            form.style.display = \"none\";\n            divta.style = \"\";\n\n            const previewEl = item.querySelector(\"[data-dci=preview]\");\n            if (previewEl) {\n                previewEl.remove();\n            }\n        };\n    }\n\n    preview_clicked(reply_to) {\n        return (_) => {\n            this.post(\"preview\", reply_to);\n        };\n    }\n\n    send_clicked(reply_to) {\n        return (_) => {\n            this.post(\"post\", reply_to);\n        };\n    }\n\n    post(submit_button_name, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const formEl = item.querySelector(\"form\");\n\n        if (!this.is_valid(formEl)) {\n            return;\n        }\n\n        this.disable_buttons(formEl, true);\n\n        const previewEl = item.querySelector(\"[data-dci=preview]\");\n        if (previewEl) {\n            previewEl.remove();\n        }\n\n        const formData = new FormData(formEl);\n        if (submit_button_name !== undefined) {\n            formData.append(submit_button_name, 1);\n        }\n\n        fetch(formEl.action, {\n            method: \"POST\",\n            headers: {\n                \"X-Requested-With\": \"XMLHttpRequest\",\n            },\n            body: formData\n        }).then(response => {\n            if (submit_button_name === \"preview\") {\n                this.handle_preview_comment_response(response, reply_to);\n            } else if (submit_button_name === \"post\") {\n                this.handle_post_comment_response(response, reply_to);\n            }\n        });\n        this.disable_buttons(formEl, false);\n        return false; // To prevent calling the action attribute.\n    }\n\n    handle_http_200(item, data, reply_to) {\n        item.innerHTML = data.html;\n        this.init(reply_to, true); // 2nd param: is_active = true.\n        if (data.field_focus) {\n            item.querySelector(`[name=${data.field_focus}]`).focus();\n        }\n    }\n\n    handle_http_201_202_400(item, data) {\n        const form = item.querySelector(\"form\");\n        form.innerHTML = data.html;\n    }\n\n    async handle_preview_comment_response(response, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.handle_http_200(item, data, reply_to);\n        } else if (response.status === 400) {\n            this.handle_http_201_202_400(item, data);\n        }\n    }\n\n    async handle_post_comment_response(response, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.handle_http_200(item, data, reply_to);\n        }\n        else if (\n            response.status === 201 ||\n            response.status === 202 ||\n            response.status === 400\n        ) {\n            this.handle_http_201_202_400(item, data);\n        }\n        else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment could not be \" +\n                \"processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { init_comments } from \"./comments.js\";\nimport { init_reactions } from \"./reactions.js\";\n\nwindow.djCommentsInk = {\n    init_comments: init_comments,\n    init_reactions: init_reactions\n};\n\nwindow.addEventListener(\"DOMContentLoaded\", (_) => {\n    init_comments();\n    init_reactions();\n});\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
+{"version":3,"file":"dci-0.2.0.js","mappings":";;;;;;;;;;;;;;AAAe;AACf;AACA;AACA;AACA;;AAEA,uBAAuB;;AAEvB,0BAA0B;;AAE1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,SAAS;AACT;AACA;AACA,cAAc;AACd;AACA;AACA,SAAS;;AAET;AACA,sBAAsB;AACtB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,iBAAiB;AACpE;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,mDAAmD,iBAAiB;AACpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC/G4C;AACK;AACP;;AAE1C;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gDAAgD,wDAAW;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAuD,uDAAiB;AACxE;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,mDAAc;AACjE,qDAAqD,mDAAc;AACnE;AACA;;AAEyB;;;;;;;;;;;;;;;AC1DV;AACf;AACA,8BAA8B;AAC9B;AACA;;AAEA,uCAAuC,SAAS;AAChD;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA,sCAAsC,GAAG;AACzC;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,2BAA2B,SAAS,GAAG,MAAM;AAC7C;AACA;AACA;AACA;AACA,wEAAwE,MAAM;AAC9E;AACA;AACA;AACA;AACA;;AAEA;AACA,+BAA+B,+BAA+B,GAAG;AACjE;;AAEA;AACA;AACA;;AAEA,mDAAmD,GAAG;AACtD;AACA,iCAAiC,GAAG;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,6CAA6C,MAAM;AACnD;AACA;AACA;AACA;;AAEA,wCAAwC;AACxC,wCAAwC;;AAExC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACjGmD;;AAEnD;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA,qDAAqD,0DAAgB;AACrE;AACA;AACA,SAAS;;AAET;AACA;;AAE0B;;;;;;;;;;;;;;;;ACvBqB;;AAEhC;AACf;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4CAA4C;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA,wDAAwD,SAAS;AACjE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mCAAmC,wDAAc;AACjD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb,2DAA2D;AAC3D;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AC7IA;AACA;;AAEA;AACA;AACA;AACA,gDAAgD;AAChD,wBAAwB,oBAAoB;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEe;AACf,iBAAiB,2CAA2C;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,6BAA6B;AAC7B,4BAA4B;;AAE5B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB;AACjB;AACA,aAAa;AACb,UAAU;AACV,sCAAsC,eAAe,QAAQ,cAAc;AAC3E;AACA;;AAEA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE;AACA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,kDAAkD,WAAW;AAC7D;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,sDAAsD,KAAK,MAAM,IAAI;;AAErE;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,kDAAkD,KAAK,MAAM,IAAI;;AAEjE;AACA;AACA;AACA;AACA,SAAS;AACT;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AC9Me;AACf;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,aAAa;AAChE;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,gDAAgD,SAAS;;AAEzD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,mDAAmD,SAAS;AAC5D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,uDAAuD,SAAS;AAChE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,SAAS;AACT;AACA;AACA,cAAc;AACd;AACA;AACA,SAAS;AACT;AACA,sBAAsB;AACtB;;AAEA;AACA;AACA,mCAAmC;AACnC;AACA,wCAAwC,iBAAiB;AACzD;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,UAAU;AACV;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;UCnOA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;ACN8C;AACE;;AAEhD;AACA,mBAAmB,uDAAa;AAChC,oBAAoB,yDAAc;AAClC;;AAEA;AACA,IAAI,2DAAa;AACjB,IAAI,6DAAc;AAClB,CAAC","sources":["webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/comment_form.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/comments.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/folding.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions_handler.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reactions_panel.js","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/reply_forms.js","webpack://django-comments-ink/webpack/bootstrap","webpack://django-comments-ink/webpack/runtime/define property getters","webpack://django-comments-ink/webpack/runtime/hasOwnProperty shorthand","webpack://django-comments-ink/webpack/runtime/make namespace object","webpack://django-comments-ink/./django_comments_ink/static/django_comments_ink/js/index.js"],"sourcesContent":["export default class CommentForm {\n    constructor(formWrapper) {\n        this.formWrapper = formWrapper;\n        this.init();\n    }\n\n    click_on_post(_) { return this.post(\"post\"); }\n\n    click_on_preview(_) { return this.post(\"preview\"); }\n\n    init() {\n        this.formWrapperEl = document.querySelector(this.formWrapper);\n        this.formEl = this.formWrapperEl.querySelector(\"form\");\n        const post_btn = this.formEl.elements.post;\n        const preview_btn = this.formEl.elements.preview;\n        post_btn.addEventListener(\"click\", (_) => this.post(\"post\"));\n        preview_btn.addEventListener(\"click\", (_) => this.post(\"preview\"));\n        // Change the type of the buttons, otherwise the form is submitted.\n        post_btn.type = \"button\";\n        preview_btn.type = \"button\";\n    }\n\n    disable_btns(value) {\n        this.formEl.elements.post.disabled = value;\n        this.formEl.elements.preview.disabled = value;\n    }\n\n    is_valid() {\n        for (const el of this.formEl.querySelectorAll(\"[required]\")) {\n            if (!el.reportValidity()) {\n                el.focus();\n                return false;\n            }\n        }\n        return true;\n    }\n\n    post(submit_button_name) {\n        if (!this.is_valid()) {\n            return;\n        }\n        this.disable_btns(true);\n\n        // If the <section data-dci=\"preview\">...</section> does exist,\n        // delete it. If the user clicks again in the \"preview\" button\n        // it will be displayed again.\n        const preview = this.formWrapperEl.querySelector(\"[data-dci=preview]\");\n        if (preview) {\n            preview.remove();\n        }\n\n        const formData = new FormData(this.formEl);\n        if (submit_button_name !== undefined) {\n            formData.append(submit_button_name, 1);\n        }\n\n        fetch(this.formEl.action, {\n            method: 'POST',\n            headers: {\n                \"X-Requested-With\": \"XMLHttpRequest\",\n            },\n            body: formData\n        }).then(response => {\n            if (submit_button_name === \"preview\") {\n                this.handle_preview_comment_response(response);\n            } else if (submit_button_name === \"post\") {\n                this.handle_post_comment_response(response);\n            }\n        });\n\n        this.disable_btns(false);\n        return false; // To prevent calling the action attribute.\n    }\n\n    async handle_preview_comment_response(response) {\n        const data = await response.json();\n        if (response.status === 200) {\n            this.formWrapperEl.innerHTML = data.html;\n            this.init();\n            if (data.field_focus) {\n                this.formEl.querySelector(`[name=${data.field_focus}]`).focus();\n            }\n        } else if (response.status === 400) {\n            this.formEl.innerHTML = data.html;\n        }\n    }\n\n    async handle_post_comment_response(response) {\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.formWrapperEl.innerHTML = data.html;\n            this.init();\n            if (data.field_focus) {\n                this.formEl.querySelector(`[name=${data.field_focus}]`).focus();\n            }\n        }\n        else if (\n            response.status === 201 ||\n            response.status === 202 ||\n            response.status === 400\n        ) {\n            this.formEl.innerHTML = data.html;\n        }\n        else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment could not be \" +\n                \"processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n}\n","import CommentForm from \"./comment_form.js\";\nimport ReplyFormsHandler from \"./reply_forms.js\";\nimport FoldingHandler from \"./folding.js\";\n\nfunction init_comments() {\n    if (window.djCommentsInk === null) {\n        return;\n    }\n\n    if (window.djCommentsInk.page_param === undefined) {\n        const rroot = document.querySelector(\"[data-dci=config]\");\n        if (rroot) {\n            window.djCommentsInk.page_param = rroot.getAttribute(\n                \"data-page-qs-param\"\n            );\n        }\n    }\n\n    window.djCommentsInk.comment_form = null;\n    window.djCommentsInk.reply_forms_handler = null;\n    window.djCommentsInk.folding_handler = null;\n    window.djCommentsInk.unfolding_handler = null;\n\n    /* ----------------------------------------------\n     * Initialize main comment form.\n     */\n    const qs_cform = \"[data-dci=comment-form]\";\n    if (window.djCommentsInk.comment_form === null &&\n        document.querySelector(qs_cform)\n    ) {\n        window.djCommentsInk.comment_form = new CommentForm(qs_cform);\n    }\n\n    /* ----------------------------------------------\n     * Initialize reply forms.\n     */\n    const qs_rform_base = \"[data-dci=reply-form-template]\";\n    const qs_rforms = \"[data-dci=reply-form]\";\n    if (window.djCommentsInk.reply_forms_handler === null &&\n        document.querySelector(qs_rform_base) &&\n        document.querySelectorAll(qs_rforms)\n    ) {\n        window.djCommentsInk.reply_forms_handler = new ReplyFormsHandler(\n            qs_rform_base, qs_rforms\n        );\n    }\n\n    /* ----------------------------------------------\n     * Initialize fold/unfold of comments with level > 0.\n     */\n    if (window.djCommentsInk.folding_handler === null &&\n        window.djCommentsInk.unfolding_handler === null\n    ) {\n        window.djCommentsInk.folding_handler = new FoldingHandler(\"fold\");\n        window.djCommentsInk.unfolding_handler = new FoldingHandler(\"unfold\");\n    }\n}\n\nexport { init_comments };\n","export default class FoldingHandler {\n    constructor(direction) {\n        this.dir = direction; // Direction can be either 'fold' or 'unfold'.\n        this.target_comment = undefined;\n        this.comment_replies = -1;\n\n        const qs = `[data-dci-action=${this.dir}]`;\n        const qs_links = document.querySelectorAll(qs);\n\n        const onLinkClickHandler = this.on_click.bind(this);\n\n        qs_links.forEach(elem => {\n            if (direction === \"fold\") {\n                // By default, when loading the page all comments are\n                // unfolded, so the 'fold' link has to be visible.\n                elem.classList.remove(\"hide\");\n            }\n            // If the fold link is clicked, it should trigger\n            // the folding of the comments underneath.\n            elem.addEventListener(\"click\", onLinkClickHandler);\n        });\n    }\n\n    on_click(event) {\n        event.preventDefault();\n        // Find the <div id=\"comment-{id}\"> of the clicked element.\n        this.target_comment = event.target.closest(\".comment\").parentNode;\n        // Read the number of sibling elements that have to be hidden.\n        this.comment_replies = parseInt(event.target.dataset.dciReplies);\n\n        this.fold_or_unfold_siblings();\n\n        // Hide the link clicked, and make visible the inverse link.\n        const bits = this.target_comment.getAttribute(\"id\").split(\"-\");\n        console.assert(bits.length === 2 && bits[0] === \"comment\");\n        const cm_id = parseInt(bits[1]);\n        this.turn_links(cm_id);\n    }\n\n    turn_links(cm_id) {\n        const dir_lid = `${this.dir}-${cm_id}`;\n        const direct_link = document.getElementById(dir_lid);\n        if (direct_link !== null) {\n            direct_link.classList.add(\"hide\");\n        }\n        const inv_lid = (this.dir === 'fold' ? 'unfold' : 'fold') + `-${cm_id}`;\n        const inverse_link = document.getElementById(inv_lid);\n        if (inverse_link !== null) {\n            inverse_link.classList.remove(\"hide\");\n        }\n    }\n\n    fold_or_unfold_siblings() {\n        let num_processed = 0; // Number of <div id=\"comment-{id}\"> processed.\n        let next_node = this.target_comment.nextElementSibling;\n\n        while(next_node && (num_processed < this.comment_replies)) {\n            const cm_attr_id = next_node.getAttribute(\"id\");\n            const bits = cm_attr_id.split(\"-\");\n\n            // If the node is a <div id=\"reply-to-{id}\"> element, skip it.\n            // They are hidden explicitly when processing the\n            // <div id=\"comment-{id}\"> counterpart.\n\n            if (\n                (bits.length === 3) &&\n                (bits[0] === \"reply\") && (bits[1] === \"to\")\n            ) {\n                next_node = next_node.nextElementSibling;\n                continue;\n            }\n            else if ((bits.length === 2) && (bits[0] === \"comment\")) {\n                const cm_id = parseInt(bits[1]);\n\n                // If the comment has a reply node, toggle it.\n                const reply_id = `reply-to-${cm_id}`;\n                const reply_node = document.getElementById(reply_id);\n                if(reply_node) {\n                    this.toggle(reply_node);\n                }\n\n                this.turn_links(cm_id); // Turn the fold/unfold links.\n                this.toggle(next_node); // Toggle the comment too.\n\n                next_node = next_node.nextElementSibling;\n                num_processed++;\n            }\n        }\n    }\n\n    toggle(node) {\n        if (this.dir === \"fold\") {\n            node.classList.add(\"hide\");\n        } else if (this.dir === \"unfold\") {\n            node.classList.remove(\"hide\");\n        }\n    }\n}\n","import ReactionsHandler from \"./reactions_handler\";\n\nfunction init_reactions() {\n    const rroot = document.querySelector(\"[data-dci=config]\");\n    if (rroot === null || window.djCommentsInk === null) {\n        return;\n    }\n\n    window.djCommentsInk.reactions_handler = null;\n\n    /* ----------------------------------------------\n     * Initialize reactions_handler, in charge\n     * of all reactions popover components.\n     */\n    if (window.djCommentsInk.reactions_handler === null) {\n        window.djCommentsInk.reactions_handler = new ReactionsHandler(rroot);\n        window.addEventListener(\"beforeunload\", (_) => {\n            window.djCommentsInk.reactions_handler.remove_event_listeners();\n        });\n\n    }\n}\n\nexport { init_reactions };\n","import ReactionsPanel from \"./reactions_panel\";\n\nexport default class ReactionsHandler {\n    constructor(configEl) {\n        this.cfg_el = configEl;\n        this.is_guest = this.cfg_el.getAttribute(\"data-guest-user\") === \"1\";\n        this.login_url = this.init_login_url();\n        this.react_url = this.init_react_url();\n\n        // Initialize the buttons panels and their components.\n        this.links = document.querySelectorAll(\"[data-dci=reactions-panel]\");\n        if (this.links.length === 0) {\n            throw new Error(\n                \"Cannot initialize reactions panel => There are \" +\n                \"no elements with [data-dci=reactions-panel].\");\n        }\n        this.active_visible_panel = \"0\";\n        this.panels_visibility = new Map(); // Keys are 'comment_id'.\n        this.event_handlers = [];\n        this.add_event_listeners();\n        this.listen_to_click_on_links();\n        const qs_panel = \"[data-dci=reactions-panel-template]\";\n        this.panel_el = document.querySelector(qs_panel);\n        if (this.panel_el === undefined) {\n            throw new Error(\"Cannot find element with ${qs_panel}.\");\n        }\n\n        // Create object of class ReactionsPanel in charge of showing and\n        // hiding the reactions panel around the clicked 'react' link.\n        const opts = {\n            panel_el: this.panel_el,\n            is_guest: this.is_guest,\n            login_url: this.login_url,\n            react_url: this.react_url\n        };\n        this.reactions_panel = new ReactionsPanel(opts);\n    }\n\n    init_login_url() {\n        const url = this.cfg_el.getAttribute(\"data-login-url\");\n        if (url === null || url.length === 0) {\n            if (this.is_guest) {\n                throw new Error(\"Cannot initialize reactions panel => The \" +\n                    \"[data-login-url] attribute does not exist or is empty.\");\n            }\n        }\n        return url;\n    }\n\n    init_react_url() {\n        const url = this.cfg_el.getAttribute(\"data-react-url\");\n        if (url === null || url.length === 0) {\n            if (!this.is_guest) {\n                throw new Error(\"Cannot initialize reactions panel => The \" +\n                    \"[data-react-url] attribute does not exist or is empty.\");\n            } else {\n                console.info(\"Couldn't find the data-react-url attribute, \" +\n                    \"but the user is anonymous. She has to login first in \" +\n                    \"order to post comment reactions.\");\n            }\n        }\n        return url;\n    }\n\n    on_document_click(event) {\n        const data_attr = event.target.getAttribute(\"data-dci\");\n        if (!data_attr || data_attr !== \"reactions-panel\") {\n            this.reactions_panel.hide();\n            if (this.active_visible_panel !== \"0\") {\n                this.panels_visibility.set(this.active_visible_panel, false);\n                this.active_visible_panel = \"0\";\n            }\n        }\n    }\n\n    on_document_key_up(event) {\n        if (event.key === \"Escape\") {\n            this.reactions_panel.hide();\n            if (this.active_visible_panel !== \"0\") {\n                this.panels_visibility.set(this.active_visible_panel, false);\n                this.active_visible_panel = \"0\";\n            }\n        }\n    }\n\n    add_event_listeners() {\n        const onDocumentClickHandler = this.on_document_click.bind(this);\n        const onDocumentKeyUpHandler = this.on_document_key_up.bind(this);\n\n        window.document.addEventListener('click', onDocumentClickHandler);\n        window.document.addEventListener('keyup', onDocumentKeyUpHandler);\n\n        this.event_handlers.push({\n            elem: window.document,\n            event: 'click',\n            handler: this.on_document_click,\n        });\n        this.event_handlers.push({\n            elem: window.document,\n            event: 'keyup',\n            handler: this.on_document_key_up,\n        });\n    }\n\n    remove_event_listeners() {\n        for (const item of this.event_handlers) {\n            item.elem.removeEventListener(item.event, item.handler);\n        }\n    }\n\n    listen_to_click_on_links() {\n        for (const elem of Array.from(this.links)) {\n            const comment_id = elem.getAttribute(\"data-comment\");\n            if (comment_id === null) {\n                continue;\n            }\n            const click_handler = this.toggle_reactions_panel(comment_id);\n            elem.addEventListener(\"click\", click_handler);\n            this.event_handlers.push({\n                'elem': elem,\n                'event': 'click',\n                'handler': click_handler\n            });\n            this.panels_visibility.set(comment_id, false); // Not visible yet.\n        }\n    }\n\n    toggle_reactions_panel(comment_id) {\n        return (event) => {\n            event.preventDefault();\n            const is_visible = this.panels_visibility.get(comment_id);\n            if (!is_visible) {\n                this.active_visible_panel = comment_id;\n                this.reactions_panel.show(event.target, comment_id);\n            } else {\n                this.active_visible_panel = \"0\";\n                this.reactions_panel.hide();\n            }\n            this.panels_visibility.set(comment_id, !is_visible);\n        };\n    }\n}\n","const enter_delay = 0;\nconst exit_delay = 0;\n\nfunction get_cookie(name) {\n    let cookieValue = null;\n    if (document.cookie && document.cookie !== '') {\n        const cookies = document.cookie.split(';');\n        for (let i = 0; i < cookies.length; i++) {\n            const cookie = cookies[i].trim();\n            // Does this cookie string begin with the name we want?\n            if (cookie.substring(0, name.length + 1) === (name + '=')) {\n                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\n                break;\n            }\n        }\n    }\n    return cookieValue;\n}\n\nexport default class ReactionsPanel {\n    constructor({panel_el, is_guest, login_url, react_url } = opts) {\n        this.panel_el = panel_el;\n        // this.panel_el.style.zIndex = 1;\n        // this.panel_el.style.display = \"block\";\n        this.arrow_el = panel_el.querySelector(\".arrow\");\n        this.is_guest = is_guest;\n        this.login_url = login_url;\n        this.react_url = react_url;\n\n        // -----------------------------------------------------\n        // The panel_title_elem and its content panel_title will\n        // change when the user hover the buttons of the panel.\n\n        this.panel_title = \"\";\n        this.panel_title_elem = this.panel_el.querySelector(\".title\");\n        if (this.panel_title_elem) {\n            this.panel_title = this.panel_title_elem.textContent;\n        }\n\n        // -----------------------------------------\n        // The comment_id is necessary to know which\n        // comment will receive the reaction code.\n\n        this.comment_id = 0; // Valid comment_id must be > 0.\n        this.next_url = \"\"; // Comment URL to come back after log in.\n\n        this.on_react_btn_click = this.on_react_btn_click.bind(this);\n        this.on_react_btn_mouseover = this.on_react_btn_mouseover.bind(this);\n        this.on_react_btn_mouseout = this.on_react_btn_mouseout.bind(this);\n        this.add_event_listeners();\n    }\n\n    add_event_listeners() {\n        const buttons = this.panel_el.querySelectorAll(\"button\");\n        for (const btn of Array.from(buttons)) {\n            btn.addEventListener(\"click\", this.on_react_btn_click);\n            btn.addEventListener(\"mouseover\", this.on_react_btn_mouseover);\n            btn.addEventListener(\"mouseout\", this.on_react_btn_mouseout);\n        }\n    }\n\n    on_react_btn_click(event) {\n        if (!this.is_guest) {\n            const code = event.target.dataset.code;\n            const react_url = this.react_url.replace(\"0\", this.comment_id);\n            const formData = new FormData();\n            formData.append(\"reaction\", code);\n            formData.append(\"csrfmiddlewaretoken\", get_cookie(\"csrftoken\"));\n\n            fetch(react_url, {\n                method: \"POST\",\n                cache: \"no-cache\",\n                credentials: \"same-origin\",\n                headers: {\n                    \"X-Requested-With\": \"XMLHttpRequest\",\n                },\n                body: formData\n            }).then(response => this.handle_reactions_response(response));\n        } else {\n            window.location.href = `${this.login_url}?next=${this.next_url}`;\n        }\n    }\n\n    async handle_reactions_response(response) {\n        const data = await response.json();\n        if (response.status === 200 || response.status === 201) {\n            const cm_reactions_qs = `#cm-reactions-${this.comment_id}`;\n            const cm_reactions_el = document.querySelector(cm_reactions_qs);\n            if (cm_reactions_el) {\n                cm_reactions_el.innerHTML = data.html;\n            }\n        } else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment reaction could not \" +\n                \"be processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n\n    on_react_btn_mouseover(event) {\n        if (this.panel_title_elem) {\n            this.panel_title_elem.textContent = event.target.dataset.title;\n        }\n    }\n\n    on_react_btn_mouseout(_) {\n        this.panel_title_elem.textContent = this.panel_title;\n    }\n\n    set_position(trigger_elem) {\n        this.panel_el.style.display = \"block\";\n\n        const panel_elem_coords = this.get_absolute_coords(this.panel_el);\n        const trigger_elem_coords = this.get_absolute_coords(trigger_elem);\n\n        const panel_elem_width = panel_elem_coords.width;\n        const panel_elem_height = panel_elem_coords.height;\n        const panel_elem_top = panel_elem_coords.top;\n        const panel_elem_left = panel_elem_coords.left;\n\n        const trigger_elem_width = trigger_elem_coords.width;\n        const trigger_elem_top = trigger_elem_coords.top;\n        const trigger_elem_left = trigger_elem_coords.left;\n\n        const top_diff = trigger_elem_top - panel_elem_top;\n        const left_diff = trigger_elem_left - panel_elem_left;\n\n        // This group of const values can be hardcoded somewhere else.\n        // const position = \"auto\";\n        const margin = 8;\n\n        const width_center = trigger_elem_width / 2 - panel_elem_width / 2;\n\n        const left = left_diff + width_center;\n        const top = top_diff - panel_elem_height - margin;\n        const from_top = top + 10;\n\n        this.panel_el.dataset.fromLeft = left;\n        this.panel_el.dataset.fromTop = from_top;\n        this.panel_el.dataset.left = left;\n        this.panel_el.dataset.top = top;\n\n        // Arrow.\n        if (this.arrow_el) {\n            let arrow_left = 0;\n            const full_left = left + panel_elem_left;\n            const t_width_center = trigger_elem_width / 2 + trigger_elem_left;\n            arrow_left = t_width_center - full_left;\n            const transform_text = `translate3d(${arrow_left}px, 0px, 0)`;\n            this.arrow_el.style.transform = transform_text;\n        }\n    }\n\n    hide() {\n        clearTimeout(this.enter_delay_timeout);\n\n        this.exit_delat_timeout = setTimeout(() => {\n            if (this.panel_el) {\n                const left = this.panel_el.dataset.fromLeft;\n                const top = this.panel_el.dataset.fromTop;\n                const transform_text = `translate3d(${left}px, ${top}px, 0)`;\n\n                this.panel_el.style.transform = transform_text;\n                this.panel_el.style.opacity = 0;\n                this.panel_el.style.display = \"none\";\n                this.panel_el.style.zIndex = 0;\n            }\n        }, exit_delay);\n    }\n\n    show(trigger_elem, comment_id) {\n        this.comment_id = comment_id;\n        this.next_url = trigger_elem.dataset.loginNext || \"\";\n        this.panel_el.style.transform = \"none\";\n        this.set_position(trigger_elem);\n\n        this.enter_delay_timeout = setTimeout(() => {\n            const left = this.panel_el.dataset.left;\n            const top = this.panel_el.dataset.top;\n            const transform_text = `translate3d(${left}px, ${top}px, 0)`;\n\n            this.panel_el.style.zIndex = 1;\n            this.panel_el.style.display = \"block\";\n            this.panel_el.style.transform = transform_text;\n            this.panel_el.style.opacity = 1;\n        }, enter_delay);\n    }\n\n    get_absolute_coords(elem) {\n        if (!elem) {\n            return;\n        }\n\n        const box = elem.getBoundingClientRect();\n        const page_x = window.pageXOffset;\n        const page_y = window.pageYOffset;\n\n        return {\n            width: box.width,\n            height: box.height,\n            top: box.top + page_y,\n            right: box.right + page_x,\n            bottom: box.bottom + page_y,\n            left: box.left + page_x,\n        };\n    }\n}\n","export default class ReplyFormsHandler {\n    constructor(qsReplyFormBase, qsReplyForms) {\n        this.replyFormBase = document.querySelector(qsReplyFormBase);\n        this.replyMap = new Map();\n\n        const cpage_field = window.djCommentsInk.page_param || \"cpage\";\n\n        for (const elem of document.querySelectorAll(qsReplyForms)) {\n            // Extract the reply_to value from the current reply_form.\n            // Also, it if does exist, extract the comment's page number too.\n            // Then replace the content of elem with a copy of\n            // this.replyFormBase and update the fields reply_to\n            // and comment's page number.\n            const rFormEl = elem.querySelector(\"form\");\n            if (rFormEl === null) {\n                console.error(\n                    `Could not find a reply form within one of ` +\n                    `the elements retrieved with ${qsReplyForms}.`\n                );\n                return;\n            }\n\n            const reply_to = rFormEl.elements.reply_to.value;\n            const cpage = rFormEl.elements[cpage_field]\n                ? rFormEl.elements[cpage_field].value\n                : null;\n\n            const section = this.replyFormBase.cloneNode(true);\n            section.dataset.dci = `reply-form-${reply_to}`;\n\n            // Update fields reply_to and cpage..\n            const newForm = section.querySelector(\"form\");\n            newForm.elements.reply_to.value = reply_to;\n            if (cpage) {\n                newForm.elements[cpage_field].value = cpage;\n            }\n\n            const elemParent = elem.parentNode;\n            elem.replaceWith(section);\n            this.init(reply_to);\n            this.replyMap.set(reply_to, elemParent);\n        }\n    }\n\n    init(reply_to, is_active) {\n        const qs_section = `[data-dci=reply-form-${reply_to}]`;\n        const section = document.querySelector(qs_section);\n\n        // Modify the form (update fields, add event listeners).\n        const newForm = section.querySelector(\"form\");\n        const post_btn = newForm.elements.post;\n        post_btn.addEventListener(\"click\", this.send_clicked(reply_to));\n        const preview_btn = newForm.elements.preview;\n        preview_btn.addEventListener(\"click\", this.preview_clicked(reply_to));\n        const cancel_btn = newForm.elements.cancel;\n        cancel_btn.addEventListener(\"click\", this.cancel_clicked(reply_to));\n        newForm.style.display = \"none\";\n\n        // Attach event listener to textarea.\n        const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n        const ta = divta.querySelector(\"textarea\");\n        ta.addEventListener(\"focus\", this.textarea_focus(reply_to));\n\n        // If is_active is true, hide the textarea and display the form.\n        if (is_active === true) {\n            section.classList.add(\"active\");\n            divta.style.display = \"none\";\n            newForm.style = \"\";\n            newForm.elements.comment.focus();\n        }\n    }\n\n    get_map_item(reply_to) {\n        const item = this.replyMap.get(reply_to);\n        if (item === undefined) {\n            const msg = `replyMap doesn't have a key ${reply_to}`;\n            console.error(msg);\n            throw msg;\n        }\n        return item;\n    }\n\n    disable_buttons(formEl, value) {\n        formEl.elements.post.disabled = value;\n        formEl.elements.preview.disabled = value;\n    }\n\n    is_valid(formEl) {\n        for (const el of formEl.querySelectorAll(\"[required]\")) {\n            if (!el.reportValidity()) {\n                el.focus();\n                return false;\n            }\n        }\n        return true;\n    }\n\n    textarea_focus(reply_to) {\n        // Display the comment form and hide the text area.\n        return (_) => {\n            const item = this.get_map_item(reply_to);\n            const qs_section = `[data-dci=reply-form-${reply_to}`;\n            const section = item.querySelector(qs_section);\n            const form = section.querySelector(\"form\");\n            const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n            section.classList.toggle(\"active\");\n            divta.style.display = \"none\";\n            form.style = \"\";\n            form.elements.comment.focus();\n        };\n    }\n\n    cancel_clicked(reply_to) {\n    // Display the text area and hide the comment form.\n        return (_) => {\n            const item = this.get_map_item(reply_to);\n            const qs_section = `[data-dci=reply-form-${reply_to}`;\n            const section = item.querySelector(qs_section);\n            const form = section.querySelector(\"form\");\n            const divta = section.querySelector(\"[data-dci=reply-textarea]\");\n            const comment_value = form.elements.comment.value;\n            divta.querySelector(\"textarea\").value = comment_value;\n            section.classList.toggle(\"active\");\n            form.style.display = \"none\";\n            divta.style = \"\";\n\n            const previewEl = item.querySelector(\"[data-dci=preview]\");\n            if (previewEl) {\n                previewEl.remove();\n            }\n        };\n    }\n\n    preview_clicked(reply_to) {\n        return (_) => {\n            this.post(\"preview\", reply_to);\n        };\n    }\n\n    send_clicked(reply_to) {\n        return (_) => {\n            this.post(\"post\", reply_to);\n        };\n    }\n\n    post(submit_button_name, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const formEl = item.querySelector(\"form\");\n\n        if (!this.is_valid(formEl)) {\n            return;\n        }\n\n        this.disable_buttons(formEl, true);\n\n        const previewEl = item.querySelector(\"[data-dci=preview]\");\n        if (previewEl) {\n            previewEl.remove();\n        }\n\n        const formData = new FormData(formEl);\n        if (submit_button_name !== undefined) {\n            formData.append(submit_button_name, 1);\n        }\n\n        fetch(formEl.action, {\n            method: \"POST\",\n            headers: {\n                \"X-Requested-With\": \"XMLHttpRequest\",\n            },\n            body: formData\n        }).then(response => {\n            if (submit_button_name === \"preview\") {\n                this.handle_preview_comment_response(response, reply_to);\n            } else if (submit_button_name === \"post\") {\n                this.handle_post_comment_response(response, reply_to);\n            }\n        });\n        this.disable_buttons(formEl, false);\n        return false; // To prevent calling the action attribute.\n    }\n\n    handle_http_200(item, data, reply_to) {\n        item.innerHTML = data.html;\n        this.init(reply_to, true); // 2nd param: is_active = true.\n        if (data.field_focus) {\n            item.querySelector(`[name=${data.field_focus}]`).focus();\n        }\n    }\n\n    handle_http_201_202_400(item, data) {\n        const form = item.querySelector(\"form\");\n        form.innerHTML = data.html;\n    }\n\n    async handle_preview_comment_response(response, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.handle_http_200(item, data, reply_to);\n        } else if (response.status === 400) {\n            this.handle_http_201_202_400(item, data);\n        }\n    }\n\n    async handle_post_comment_response(response, reply_to) {\n        const item = this.get_map_item(reply_to);\n        const data = await response.json();\n\n        if (response.status === 200) {\n            this.handle_http_200(item, data, reply_to);\n        }\n        else if (\n            response.status === 201 ||\n            response.status === 202 ||\n            response.status === 400\n        ) {\n            this.handle_http_201_202_400(item, data);\n        }\n        else if (response.status > 400) {\n            alert(\n                \"Something went wrong and your comment could not be \" +\n                \"processed. Please, reload the page and try again.\"\n            );\n        }\n    }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { init_comments } from \"./comments.js\";\nimport { init_reactions } from \"./reactions.js\";\n\nwindow.djCommentsInk = {\n    init_comments: init_comments,\n    init_reactions: init_reactions\n};\n\nwindow.addEventListener(\"DOMContentLoaded\", (_) => {\n    init_comments();\n    init_reactions();\n});\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/django_comments_ink/static/django_comments_ink/js/reactions_handler.js b/django_comments_ink/static/django_comments_ink/js/reactions_handler.js
index 6895826..cbb79df 100644
--- a/django_comments_ink/static/django_comments_ink/js/reactions_handler.js
+++ b/django_comments_ink/static/django_comments_ink/js/reactions_handler.js
@@ -14,7 +14,7 @@ export default class ReactionsHandler {
                 "Cannot initialize reactions panel => There are " +
                 "no elements with [data-dci=reactions-panel].");
         }
-        this.active_visible_panel = 0;
+        this.active_visible_panel = "0";
         this.panels_visibility = new Map(); // Keys are 'comment_id'.
         this.event_handlers = [];
         this.add_event_listeners();
@@ -66,9 +66,9 @@ export default class ReactionsHandler {
         const data_attr = event.target.getAttribute("data-dci");
         if (!data_attr || data_attr !== "reactions-panel") {
             this.reactions_panel.hide();
-            if (this.active_visible_panel) {
+            if (this.active_visible_panel !== "0") {
                 this.panels_visibility.set(this.active_visible_panel, false);
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
             }
         }
     }
@@ -76,9 +76,9 @@ export default class ReactionsHandler {
     on_document_key_up(event) {
         if (event.key === "Escape") {
             this.reactions_panel.hide();
-            if (this.active_visible_panel) {
+            if (this.active_visible_panel !== "0") {
                 this.panels_visibility.set(this.active_visible_panel, false);
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
             }
         }
     }
@@ -133,7 +133,7 @@ export default class ReactionsHandler {
                 this.active_visible_panel = comment_id;
                 this.reactions_panel.show(event.target, comment_id);
             } else {
-                this.active_visible_panel = 0;
+                this.active_visible_panel = "0";
                 this.reactions_panel.hide();
             }
             this.panels_visibility.set(comment_id, !is_visible);
diff --git a/django_comments_ink/static/django_comments_ink/js/reactions_panel.js b/django_comments_ink/static/django_comments_ink/js/reactions_panel.js
index 71e8522..b522239 100644
--- a/django_comments_ink/static/django_comments_ink/js/reactions_panel.js
+++ b/django_comments_ink/static/django_comments_ink/js/reactions_panel.js
@@ -51,7 +51,7 @@ export default class ReactionsPanel {
     }
 
     add_event_listeners() {
-        const buttons = this.panel_el.querySelectorAll("BUTTON");
+        const buttons = this.panel_el.querySelectorAll("button");
         for (const btn of Array.from(buttons)) {
             btn.addEventListener("click", this.on_react_btn_click);
             btn.addEventListener("mouseover", this.on_react_btn_mouseover);
diff --git a/django_comments_ink/templates/comments/comment.html b/django_comments_ink/templates/comments/comment.html
index 4892097..76d9f49 100644
--- a/django_comments_ink/templates/comments/comment.html
+++ b/django_comments_ink/templates/comments/comment.html
@@ -28,7 +28,7 @@
         {% endif %}
         {% if comment_reactions_enabled and is_input_allowed %}
           <div class="inline-block sep">
-            <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="small" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{% render_qs_params page page_obj.number %}#{{ anchor }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}{% endif %}">{% trans "React" %}</a>
+            <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="small" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{% render_qs_params page page_obj.number %}#{{ anchor }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}"{% endif %}>{% trans "React" %}</a>
           </div>
         {% endif %}
       {% endif %}
diff --git a/django_comments_ink/templates/comments/moderated.html b/django_comments_ink/templates/comments/moderated.html
index 8c7e1ae..b91bdc2 100644
--- a/django_comments_ink/templates/comments/moderated.html
+++ b/django_comments_ink/templates/comments/moderated.html
@@ -1,29 +1,28 @@
 {% extends "comments/base.html" %}
 {% load i18n %}
 
-{% block title %}{% trans "Comment requires approval" %}.{% endblock %}
+{% block title %}{% trans "Comment requires approval" %}{% endblock %}
 
 {% block content %}
-<div class="mb-4">
-  <h3 class="text-center">{% trans "Comment in moderation" %}</h3>
-  <hr/>
-</div>
-<div class="row">
-  <div class="col-md-12">
-    <p class="lead text-center">
-      {% blocktrans %}
-      Your comment is in moderation.<br/>
-      It has to be reviewed before being published.<br/>
-      Thank you for your patience and understanding.
-      {% endblocktrans %}
-    </p>
-    <p class="pt-5 text-center">
-      {% with content_object_url=comment.content_object.get_absolute_url content_object_str=comment.content_object %}
-      {% blocktrans %}
-      Go back to: <a href="{{ content_object_url }}">{{ content_object_str }}</a>
-      {% endblocktrans %}
-      {% endwith %}
-    </p>
+<main>
+  <div class="container">
+    <div class="intro">
+      <h2>{% trans "Comment in moderation" %}</h2>
+      <p class="big">
+        {% blocktrans %}
+        Your comment is in moderation.<br/>
+        It has to be reviewed before being published.<br/>
+        Thank you for your patience and understanding.
+        {% endblocktrans %}
+      </p>
+      <p>
+        {% with content_object_url=comment.content_object.get_absolute_url content_object_str=comment.content_object %}
+          {% blocktrans %}
+          Go back to: <a href="{{ content_object_url }}">{{ content_object_str }}</a>
+          {% endblocktrans %}
+        {% endwith %}
+      </p>
+    </div>
   </div>
-</div>
+</main>
 {% endblock %}
diff --git a/django_comments_ink/templates/comments/posted.html b/django_comments_ink/templates/comments/posted.html
index 128db4b..88cd106 100644
--- a/django_comments_ink/templates/comments/posted.html
+++ b/django_comments_ink/templates/comments/posted.html
@@ -9,8 +9,12 @@ <h2>{% trans "Comment confirmation requested" %}</h2>
       <p class="big">
         {% blocktrans %}A confirmation message has been sent to your email address. Please, click on the link in the message to confirm your comment.{% endblocktrans %}
       </p>
-      <p class="small">
-        Go back to: <a href="{{ target.get_absolute_url }}">{{ target }}</a>
+      <p>
+        {% with object_url=target.get_absolute_url %}
+          {% blocktrans %}
+            Go back to: <a href="{{ get_absolute_url }}">{{ target }}</a>
+          {% endblocktrans %}
+        {% endwith %}
       </p>
     </div>
   </div>
diff --git a/django_comments_ink/templates/comments/themes/avatar_in_header/comment.html b/django_comments_ink/templates/comments/themes/avatar_in_header/comment.html
index 9a84a03..487250a 100644
--- a/django_comments_ink/templates/comments/themes/avatar_in_header/comment.html
+++ b/django_comments_ink/templates/comments/themes/avatar_in_header/comment.html
@@ -20,7 +20,7 @@
             {% include "comments/themes/avatar_in_header/comment_reactions.html" %}
             {% if is_input_allowed %}
               <div>
-                <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="smaller" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}{% endif %}">{% trans "React" %}</a>
+                <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="smaller" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}"{% endif %}>{% trans "React" %}</a>
               </div>
             {% endif %}
           </div>
diff --git a/django_comments_ink/templates/comments/themes/avatar_in_thread/comment.html b/django_comments_ink/templates/comments/themes/avatar_in_thread/comment.html
index 398befe..c4a1083 100644
--- a/django_comments_ink/templates/comments/themes/avatar_in_thread/comment.html
+++ b/django_comments_ink/templates/comments/themes/avatar_in_thread/comment.html
@@ -31,7 +31,7 @@
         {% endif %}
         {% if comment_reactions_enabled and is_input_allowed %}
           <div class="inline-block sep">
-            <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="small" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}{% endif %}">{% trans "React" %}</a>
+            <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="small" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}"{% endif %}>{% trans "React" %}</a>
           </div>
         {% endif %}
       {% endif %}
diff --git a/django_comments_ink/templates/comments/themes/feedback_in_header/comment.html b/django_comments_ink/templates/comments/themes/feedback_in_header/comment.html
index e7f560b..3e9a715 100644
--- a/django_comments_ink/templates/comments/themes/feedback_in_header/comment.html
+++ b/django_comments_ink/templates/comments/themes/feedback_in_header/comment.html
@@ -17,7 +17,7 @@
             {% include "comments/themes/feedback_in_header/comment_reactions.html" %}
             {% if is_input_allowed %}
               <div>
-                <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="smaller" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}{% endif %}">{% trans "React" %}</a>
+                <a data-dci="reactions-panel" data-comment="{{ comment.id }}" class="smaller" data-user-auth="{{ user.is_authenticated }}" href="{% url 'comments-ink-react' comment.id %}?{{ comments_page_qs_param }}={{ page_obj.number }}" {% if not user.is_authenticated %}data-login-next="{% get_inkcomment_permalink comment page_number %}"{% endif %}>{% trans "React" %}</a>
               </div>
             {% endif %}
           </div>
diff --git a/django_comments_ink/templatetags/comments_ink.py b/django_comments_ink/templatetags/comments_ink.py
index eae9c05..14f1900 100644
--- a/django_comments_ink/templatetags/comments_ink.py
+++ b/django_comments_ink/templatetags/comments_ink.py
@@ -287,7 +287,9 @@ def get_queryset(self, context):
                 qs = cached
 
         if not qs:
-            qs = super().get_queryset(context)
+            mtl = utils.get_max_thread_level(ctype)
+            qs = super().get_queryset(context).filter(level__lte=mtl)
+
             if dci_cache != None and self.ckey_comments_qs != "":
                 logger.debug("Adding %s to the cache", self.ckey_comments_qs)
                 dci_cache.set(self.ckey_comments_qs, qs, timeout=None)
@@ -334,6 +336,8 @@ def render(self, context):
             # {% render_inkcomment_list for var_not_in_context %}
             return ""
 
+        mtl = utils.get_max_thread_level(ctype)
+
         template_list = [
             pth.format(
                 theme_dir=theme_dir,
@@ -342,7 +346,6 @@ def render(self, context):
             )
             for pth in _list_html_tmpl
         ]
-
         qs = self.get_queryset(context)
         qs = self.get_context_value_from_queryset(context, qs)
         context_dict = context.flatten()
@@ -352,10 +355,7 @@ def render(self, context):
             paginate_queryset(qs, context, self.ckey_comments_paged)
         )
 
-        # Pass max_thread_level in the context.
-        app_model = "%s.%s" % (ctype.app_label, ctype.model)
-        MTL = settings.COMMENTS_INK_MAX_THREAD_LEVEL_BY_APP_MODEL
-        mtl = MTL.get(app_model, settings.COMMENTS_INK_MAX_THREAD_LEVEL)
+        # Pass max_users_in_tooltip from the settings.
         max_users_in_tooltip = settings.COMMENTS_INK_MAX_USERS_IN_TOOLTIP
         context_dict.update(
             {
@@ -373,7 +373,7 @@ def render(self, context):
         #     'comment_reactions_enabled': <boolean>,
         #     'object_reactions_enabled': <boolean>
         # }
-        options = utils.get_app_model_options(content_type=app_model)
+        options = utils.get_app_model_options(content_type=ctype)
         check_input_allowed_str = options.pop("check_input_allowed")
         check_func = import_string(check_input_allowed_str)
         target_obj = ctype.get_object_for_this_type(pk=object_pk)
@@ -498,8 +498,8 @@ def render_inkcomment_form(parser, token):
 
     Syntax::
 
-        {% get_inkcomment_form for [object] as [varname] %}
-        {% get_inkcomment_form for [app].[model] [object_id] as [varname] %}
+        {% render_inkcomment_form for [object] as [varname] %}
+        {% render_inkcomment_form for [app].[model] [object_id] as [varname] %}
     """
     return RenderInkCommentFormNode.handle_token(parser, token)
 
@@ -591,11 +591,7 @@ def render(self, context):
             if self.page_expr == None:
                 request = context.get("request", None)
                 page = utils.get_comment_page_number(
-                    request,
-                    cobj.content_type.id,
-                    cobj.object_pk,
-                    cobj.id,
-                    comments_folded=fold,
+                    request, cobj, comments_folded=fold
                 )
                 qs_params.append(f"{cpage_qs_param}={page}")
 
@@ -1081,13 +1077,12 @@ def resolve_ctype_and_object_pk(self, context):
                 context, ignore_failures=True
             )
 
-        app_model = "%s.%s" % (self.ctype.app_label, self.ctype.model)
-        options = utils.get_app_model_options(content_type=app_model)
+        options = utils.get_app_model_options(content_type=self.ctype)
         if not options["object_reactions_enabled"]:
             raise Exception(
                 "Object reactions for '%s' is not enabled in the setting "
                 "COMMENTS_INK_APP_MODEL_OPTIONS in the settings module"
-                % app_model
+                % self.ctype
             )
 
         self.object = self.ctype.get_object_for_this_type(pk=self.object_pk)
diff --git a/django_comments_ink/utils.py b/django_comments_ink/utils.py
index 93fae23..6418fe1 100644
--- a/django_comments_ink/utils.py
+++ b/django_comments_ink/utils.py
@@ -71,6 +71,14 @@ def send_mail(
         )
 
 
+# --------------------------------------------------------------------
+def get_max_thread_level(content_type):
+    """Get the max_thread_level for a given content_type."""
+    app_model = "%s.%s" % (content_type.app_label, content_type.model)
+    MTL = settings.COMMENTS_INK_MAX_THREAD_LEVEL_BY_APP_MODEL
+    return MTL.get(app_model, settings.COMMENTS_INK_MAX_THREAD_LEVEL)
+
+
 # --------------------------------------------------------------------
 def get_app_model_options(comment=None, content_type=None):
     """
@@ -101,7 +109,7 @@ def get_app_model_options(comment=None, content_type=None):
         content_type = ContentType.objects.get_for_model(comment.content_object)
         key = "%s.%s" % (content_type.app_label, content_type.model)
     elif content_type:
-        key = content_type
+        key = "%s.%s" % (content_type.app_label, content_type.model)
     else:
         return defaults_options["default"]
     try:
@@ -113,19 +121,25 @@ def get_app_model_options(comment=None, content_type=None):
 
 option_msgs = {
     "comment_flagging_enabled": {
-        "PROD": "This type of comments are not allowed to be flagged.",
+        "PROD": "This type of comment is not allowed to be flagged.",
         "DEBUG": "Comments posted to instances of '%s.%s' are not "
         "explicitly allowed to be flagged. Check the "
         "COMMENTS_INK_APP_MODEL_OPTIONS setting.",
     },
+    "comment_votes_enabled": {
+        "PROD": "This type of comment is not allowed to receive votes.",
+        "DEBUG": "Comments posted to instances of '%s.%s' are not "
+        "explicitly allowed to receive votes. Check the "
+        "COMMENTS_INK_APP_MODEL_OPTIONS setting.",
+    },
     "comment_reactions_enabled": {
-        "PROD": "This type of comment are not allowed to receive reactions.",
+        "PROD": "This type of comment is not allowed to receive reactions.",
         "DEBUG": "Comments posted to instances of '%s.%s' are not "
         "explicitly allowed to receive reactions. Check the "
         "COMMENTS_INK_APP_MODEL_OPTIONS setting.",
     },
     "object_reactions_enabled": {
-        "PROD": "This type of object are not allowed to receive reactions.",
+        "PROD": "This type of object is not allowed to receive reactions.",
         "DEBUG": "Instances of '%s.%s' are not explicitly allowed to receive "
         "reactions. Check the COMMENTS_INK_APP_MODEL_OPTIONS setting.",
     },
@@ -144,7 +158,7 @@ def check_option(option, comment=None, content_type=None):
                 ct = ContentType.objects.get_for_model(comment.content_object)
                 app_label, model = ct.app_label, ct.model
             if content_type:
-                app_label, model = content_type.rsplit(".", 1)
+                app_label, model = content_type.app_label, content_type.model
             message = option_msgs[option]["DEBUG"] % (app_label, model)
         raise PermissionDenied(detail=message, code=status.HTTP_403_FORBIDDEN)
 
@@ -201,11 +215,9 @@ def redirect_to(comment, request=None, page_number=None):
     return HttpResponseRedirect(url)
 
 
-def get_comment_page_number(
-    request, content_type_id, object_id, comment_id, comments_folded=None
-):
+def get_comment_page_number(request, comment, comments_folded=None):
     """
-    Returns the page number in which the `comment_id` is listed.
+    Returns the page number in which the `comment.pk` is listed.
     """
     num_orphans = settings.COMMENTS_INK_MAX_LAST_PAGE_ORPHANS
     page_size = settings.COMMENTS_INK_COMMENTS_PER_PAGE
@@ -217,8 +229,12 @@ def get_comment_page_number(
 
     site_id = get_current_site_id(request)
 
+    mtl = get_max_thread_level(comment.content_type)
     qs = get_model().objects.filter(
-        content_type_id=content_type_id, object_pk=object_id, site__pk=site_id
+        content_type_id=comment.content_type.pk,
+        object_pk=comment.object_pk,
+        site__pk=site_id,
+        level__lte=mtl,
     )
 
     # The is_public and is_removed fields are implementation details of the
@@ -238,13 +254,13 @@ def get_comment_page_number(
     if "user" in field_names:
         qs = qs.select_related("user")
 
-    comment_id = int(comment_id)
-
     if comments_folded:
         qs = qs.filter(~Q(level__gt=0, thread_id__in=comments_folded))
 
     ckey_prefix = settings.COMMENTS_INK_CACHE_KEYS["comments_paged"].format(
-        ctype_pk=content_type_id, object_pk=object_id, site_id=site_id
+        ctype_pk=comment.content_type.pk,
+        object_pk=comment.object_pk,
+        site_id=site_id,
     )
 
     paginator = CommentsPaginator(
@@ -257,9 +273,9 @@ def get_comment_page_number(
 
     for page_number in range(1, paginator.num_pages + 1):
         page = paginator.page(page_number)
-        if comment_id in [cm.id for cm in page.object_list]:
+        if comment.pk in [cm.id for cm in page.object_list]:
             return page_number
-    raise Exception("Comment %d not listed in any page." % comment_id)
+    raise Exception("Comment %d not listed in any page." % comment.pk)
 
 
 def does_theme_dir_exist(theme_dir):
diff --git a/django_comments_ink/views.py b/django_comments_ink/views.py
index 11baed7..8fffded 100644
--- a/django_comments_ink/views.py
+++ b/django_comments_ink/views.py
@@ -668,8 +668,7 @@ def on_comment_will_be_posted(sender, comment, request, **kwargs):
         user_is_authenticated = False
 
     ct = comment.get("content_type")
-    ct_str = "%s.%s" % (ct.app_label, ct.model)
-    options = utils.get_app_model_options(content_type=ct_str)
+    options = utils.get_app_model_options(content_type=ct)
     if not user_is_authenticated and options["who_can_post"] == "users":
         # Reject comment.
         return False
@@ -782,9 +781,7 @@ def sent_js(request, comment, using=None):
                 )
                 for pth in _published_js_tmpl
             ]
-            page_number = utils.get_comment_page_number(
-                request, comment.content_type.id, comment.object_pk, comment.id
-            )
+            page_number = utils.get_comment_page_number(request, comment)
             comment_url = utils.get_comment_url(comment, None, page_number)
             return json_res(
                 request,
@@ -929,11 +926,7 @@ def reply(request, cid):
     except InkComment.DoesNotExist as exc:
         raise Http404(exc)
 
-    ct_str = "%s.%s" % (
-        comment.content_type.app_label,
-        comment.content_type.model,
-    )
-    options = utils.get_app_model_options(content_type=ct_str)
+    options = utils.get_app_model_options(content_type=comment.content_type)
 
     if not request.user.is_authenticated and options["who_can_post"] == "users":
         path = request.build_absolute_uri()
@@ -1214,8 +1207,7 @@ def react_to_object(request, content_type_id, object_pk, next=None):
             "does not exist" % (content_type_id, object_pk)
         )
 
-    ctype_str = ctype.app_label + "." + ctype.model
-    utils.check_option("object_reactions_enabled", content_type=ctype_str)
+    utils.check_option("object_reactions_enabled", content_type=ctype)
     cpage_qs_param = settings.COMMENTS_INK_PAGE_QUERY_STRING_PARAM
     cfold_qs_param = settings.COMMENTS_INK_FOLD_QUERY_STRING_PARAM
 
@@ -1382,9 +1374,8 @@ def get_inkcomment_url(request, content_type_id, object_id, comment_id):
 
     if not cpage:
         # Create a CommentsPaginator and get the page of the comment.
-        cpage = utils.get_comment_page_number(
-            request, content_type_id, object_id, comment_id
-        )
+        comment = InkComment.norel_objects.get(pk=comment_id)
+        cpage = utils.get_comment_page_number(request, comment)
 
     qs_params = []
 


Test patch:
diff --git a/django_comments_ink/tests/test_utils.py b/django_comments_ink/tests/test_utils.py
index 133fdae..55ce4ae 100644
--- a/django_comments_ink/tests/test_utils.py
+++ b/django_comments_ink/tests/test_utils.py
@@ -8,6 +8,7 @@
 from django_comments_ink.conf import settings
 from django_comments_ink.conf.defaults import COMMENTS_INK_APP_MODEL_OPTIONS
 from django_comments_ink.paginator import CommentsPaginator
+from django_comments_ink.tests import models
 
 InkComment = get_model()
 
@@ -60,6 +61,7 @@ def test_get_app_model_options_without_args():
     "default": {
         "who_can_post": "all",
         "check_input_allowed": "",
+        "comment_votes_enabled": True,
         "comment_flagging_enabled": True,
         "comment_reactions_enabled": True,
         "object_reactions_enabled": True,
@@ -81,12 +83,14 @@ def test_get_app_model_options_without_args_returns_defaults(monkeypatch):
     "default": {
         "who_can_post": "all",
         "check_input_allowed": "",
+        "comment_votes_enabled": True,
         "comment_flagging_enabled": True,
         "comment_reactions_enabled": True,
         "object_reactions_enabled": True,
     },
     "tests.article": {
         "comment_flagging_enabled": False,
+        "comment_votes_enabled": False,
         "comment_reactions_enabled": False,
         "object_reactions_enabled": False,
     },
@@ -100,7 +104,7 @@ def test_get_app_model_options_with_comment(an_articles_comment, monkeypatch):
         "COMMENTS_INK_APP_MODEL_OPTIONS",
         default_and_article_options,
     )
-    _options = utils.get_app_model_options(an_articles_comment)
+    _options = utils.get_app_model_options(comment=an_articles_comment)
     expected_options = default_and_article_options["default"]
     expected_options.update(default_and_article_options["tests.article"])
     assert _options == expected_options
@@ -113,20 +117,21 @@ def test_get_app_model_options_with_content_type(monkeypatch):
         "COMMENTS_INK_APP_MODEL_OPTIONS",
         default_and_article_options,
     )
-    _options = utils.get_app_model_options(content_type="tests.article")
+    ctype = ContentType.objects.get_for_model(models.Article)
+    _options = utils.get_app_model_options(content_type=ctype)
     expected_options = default_and_article_options["default"]
     expected_options.update(default_and_article_options["tests.article"])
     assert _options == expected_options
 
 
 @pytest.mark.django_db
-def test_get_app_model_options_with_non_existing_content_type(monkeypatch):
+def test_get_app_model_options_with_content_type_None(monkeypatch):
     monkeypatch.setattr(
         utils.settings,
         "COMMENTS_INK_APP_MODEL_OPTIONS",
         default_and_article_options,
     )
-    _options = utils.get_app_model_options(content_type="tralari.tralara")
+    _options = utils.get_app_model_options(content_type=None)
     expected_options = default_and_article_options["default"]
     assert _options == expected_options
 
@@ -202,9 +207,7 @@ def test_get_comment_page_number_when_page_size_is_0(an_article, monkeypatch):
     article_ct = create_scenario_1(an_article)
     monkeypatch.setattr(utils.settings, "COMMENTS_INK_COMMENTS_PER_PAGE", 0)
     for comment in InkComment.objects.all():
-        page_number = utils.get_comment_page_number(
-            None, article_ct, an_article.pk, comment.id
-        )
+        page_number = utils.get_comment_page_number(None, comment)
         assert page_number == 1
 
 
@@ -220,23 +223,17 @@ def test_get_comment_page_number(an_article):
 
     page = paginator.page(1)
     for comment in page.object_list:
-        page_number = utils.get_comment_page_number(
-            None, article_ct.pk, an_article.pk, comment.id
-        )
+        page_number = utils.get_comment_page_number(None, comment)
         assert page_number == 1
 
     page = paginator.page(2)
     for comment in page.object_list:
-        page_number = utils.get_comment_page_number(
-            None, article_ct.pk, an_article.pk, comment.id
-        )
+        page_number = utils.get_comment_page_number(None, comment)
         assert page_number == 2
 
     page = paginator.page(3)
     for comment in page.object_list:
-        page_number = utils.get_comment_page_number(
-            None, article_ct.pk, an_article.pk, comment.id
-        )
+        page_number = utils.get_comment_page_number(None, comment)
         assert page_number == 3
 
 
@@ -261,9 +258,7 @@ def test_get_comment_page_number_raises_Exception(an_article, monkeypatch):
     page = paginator.page(2)
     comment = page.object_list[0]
     with pytest.raises(Exception):
-        utils.get_comment_page_number(
-            None, article_ct.id, an_article.id, comment.id
-        )
+        utils.get_comment_page_number(None, comment)
 
 
 @pytest.mark.parametrize(
diff --git a/django_comments_ink/tests/test_views.py b/django_comments_ink/tests/test_views.py
index 48b5c97..0bc4efe 100644
--- a/django_comments_ink/tests/test_views.py
+++ b/django_comments_ink/tests/test_views.py
@@ -224,7 +224,6 @@ def test_confirm_url_is_short_enough(self):
         # Tests that the length of the confirm url's length isn't
         # dependent on the article length.
         l = len(reverse("comments-ink-confirm", kwargs={"key": self.key}))
-        # print("\nXXXXXXXXXXX:", l)
         self.assertLessEqual(l, 4096, "Urls can only be a max of 4096")
 
     def test_400_on_bad_signature(self):
@@ -1692,7 +1691,6 @@ def test_GET_react_without_reactions_enabled_raises(
     monkeypatch, rf, an_user, an_articles_comment
 ):
     def raise_PermissionDenied(*args, **kwargs):
-        print("in raise_PermissionDenied")
         raise PermissionDenied(detail="Mee", code=status.HTTP_403_FORBIDDEN)
 
     monkeypatch.setattr(views.utils, "check_option", raise_PermissionDenied)
diff --git a/js_tests/tests/scene3/index.html b/js_tests/tests/scene3/index.html
index 107cebd..67f939e 100644
--- a/js_tests/tests/scene3/index.html
+++ b/js_tests/tests/scene3/index.html
@@ -43,6 +43,7 @@ <h4 class="text-center">There is 1 comment</h4>
               data-react-url="/comments/react/0/"
               data-guest-user="0"
               data-page-qs-param="cpage"
+              data-fold-qs-param="cfold"
             >
               <div id="comment-29" class="comment-box l0-ini">
                 <div class="comment">
@@ -53,11 +54,11 @@ <h4 class="text-center">There is 1 comment</h4>
                   <div class="body">
                     <p>Many people claim for their rights. The right to have a job, to have a house, the right to have access to health care and education for their children. Many of them however treat trans people as a a kind of sub-human being, and with that attitude they withhold trans peoples' rights to be human beings, and the right to have their own existence.</p>
                   </div>
-                  <div id="cm-feedback-29" class="footer">
+                  <div id="cm-feedback-29" class="feedback feedback-borderer">
                     <div class="reactions" id="cm-reactions-29">
                     </div>
-                    <div>
-                      <a data-dci="reactions-panel" data-comment="29" class="small" data-user-auth="False" href="/comments/react/29/?cpage=1" data-login-next="/comments/cr/15/3/29/#comment-29">React</a>
+                    <div class="inline-block sep">
+                      <a data-dci="reactions-panel" data-comment="29" class="small" data-user-auth="True" href="/comments/react/29/?cpage=1">React</a>
                     </div>
                   </div>
                 </div>
@@ -143,9 +144,6 @@ <h5 class="text-center">Post your comment</h5>
       </div>
     </footer>
 
-    <script>
-        self.dci = {}
-    </script>
     <script> // From https://unpkg.com/unfetch/polyfill.
       self.fetch||(self.fetch=function(e,n){return n=n||{},new Promise(function(t,s){var r=new XMLHttpRequest,o=[],u=[],i={},a=function(){return{ok:2==(r.status/100|0),statusText:r.statusText,status:r.status,url:r.responseURL,text:function(){return Promise.resolve(r.responseText)},json:function(){return Promise.resolve(r.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([r.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var c in r.open(n.method||"get",e,!0),r.onload=function(){r.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t}),t(a())},r.onerror=s,r.withCredentials="include"==n.credentials,n.headers)r.setRequestHeader(c,n.headers[c]);r.send(n.body||null)})});
     </script>
diff --git a/js_tests/tests/scene3/reactions.test.js b/js_tests/tests/scene3/reactions.test.js
index 5404f9e..b52565b 100644
--- a/js_tests/tests/scene3/reactions.test.js
+++ b/js_tests/tests/scene3/reactions.test.js
@@ -22,7 +22,6 @@ describe("scene 3 - reactions.test.js module", () => {
         dom = await JSDOM.fromFile(html_path, opts);
         await new Promise(resolve => {
             dom.window.addEventListener("DOMContentLoaded", () => {
-                // dom.window.djCommentsInk.init_reactions();
                 container = dom.window.document.body;
                 resolve();
             });
@@ -33,82 +32,70 @@ describe("scene 3 - reactions.test.js module", () => {
         expect(
             dom.window.djCommentsInk !== null &&
             dom.window.djCommentsInk !== undefined
-        );
-        expect(dom.window.djCommentsInk.guest === "0");
-        expect(dom.window.djCommentsInk.login_url === null);
-        expect(dom.window.djCommentsInk.react_url === "/comments/api/react/");
+        ).toBe(true);
+        expect(dom.window.djCommentsInk.reactions_handler).not.toBe(null);
     });
 
     it("has a div with [data-dci=config] with more data attributes", () => {
         const qs_config = "[data-dci=config]";
         const config_el = container.querySelector(qs_config);
-        expect(config_el !== null);
-        expect(config_el.dataset.react_url === "/comments/api/react/");
-        expect(config_el.dataset.guest_user === "0");
-        expect(config_el.dataset.popover_pos_bottom === "30");
-        expect(config_el.dataset.popover_pos_left === "10");
-        expect(config_el.dataset.tooltip_pos_bottom === "30");
-        expect(config_el.dataset.tooltip_pos_left === "76");
-        expect(config_el.dataset.page_qs_param === "cpage");
-    });
-
-    it("has a div#comment-29 with a [data-crpanel=29]", () => {
-        const panel_qs = '[data-crpanel="29"]';
-        const react_anchor_qs = "A[data-dci=reactions-panel]";
-
-        const comment_el = container.querySelector("#comment-29");
-        expect(comment_el !== null);
-
-        const reactions_panel_el = comment_el.querySelector(panel_qs);
-        expect(reactions_panel_el !== null);
+        const react_url = config_el.getAttribute("data-react-url");
+        const guest_user = config_el.getAttribute("data-guest-user");
+        const page_qs_param = config_el.getAttribute("data-page-qs-param");
+        const fold_qs_param = config_el.getAttribute("data-fold-qs-param");
+        expect(config_el).not.toBe(null);
+        expect(react_url).toBe("/comments/react/0/");
+        expect(guest_user).toBe("0");
+        expect(page_qs_param).toBe("cpage");
+        expect(fold_qs_param).toBe("cfold");
     });
 
     it("opens/closes reactions panel by clicking on 'react' link", async() => {
-        const anchor_qs = "A[data-dci=reactions-panel]";
+        const anchor_qs = '[data-dci="reactions-panel"]';
 
         const comment_el = container.querySelector("#comment-29");
-        expect(comment_el != null);
+        expect(comment_el).not.toBe(null);
 
         const react_anchor_el = comment_el.querySelector(anchor_qs);
-        expect(react_anchor_el != null);
+        expect(react_anchor_el).not.toBe(null);
 
         fireEvent.click(react_anchor_el);
         await waitFor(() => {
             const hdler = dom.window.djCommentsInk.reactions_handler;
-            expect(hdler.active_visible_panel === '29');
-            expect(hdler.reactions_panel.comment_id === '29');
-            expect(hdler.reactions_panel.panel_el.style.opacity == '1')
+            expect(hdler.active_visible_panel).toBe('29');
+            expect(hdler.reactions_panel.comment_id).toBe('29');
+            expect(hdler.reactions_panel.panel_el.style.opacity).toBe('1');
         });
 
         fireEvent.click(react_anchor_el);
         await waitFor(() => {
             const hdler = dom.window.djCommentsInk.reactions_handler;
-            expect(hdler.active_visible_panel === '0');
-            expect(hdler.reactions_panel.comment_id === '29');
-            expect(hdler.reactions_panel.panel_el.style.opacity == '0');
+            expect(hdler.active_visible_panel).toBe('0');
+            expect(hdler.reactions_panel.comment_id).toBe('29');
+            expect(hdler.reactions_panel.panel_el.style.opacity).toBe('0');
         });
     });
 
     it("clicks on the 'Like' and displays the 'Like'", async () => {
-        const panel_qs = "[data-dci=reactions-panel-template]";
-        const anchor_qs = "A[data-dci=reactions-panel]";
+        const panel_qs = '[data-dci="reactions-panel-template"]';
+        const anchor_qs = '[data-dci="reactions-panel"]';
 
         const comment_el = container.querySelector("#comment-29");
-        expect(comment_el != null);
+        expect(comment_el).not.toBe(null);
 
         const react_anchor_el = comment_el.querySelector(anchor_qs);
-        expect(react_anchor_el != null);
+        expect(react_anchor_el).not.toBe(null);
 
         const reactions_panel_el = container.querySelector(panel_qs);
-        expect(reactions_panel_el != null);
+        expect(reactions_panel_el).not.toBe(null);
 
         // Click on the 'react' link to open the reactions panel.
         fireEvent.click(react_anchor_el);
         await waitFor(() => {
             const hdler = dom.window.djCommentsInk.reactions_handler;
-            expect(hdler.active_visible_panel === '29');
-            expect(hdler.reactions_panel.comment_id === '29');
-            expect(hdler.reactions_panel.panel_el.style.opacity == '1')
+            expect(hdler.active_visible_panel).toBe('29');
+            expect(hdler.reactions_panel.comment_id).toBe('29');
+            expect(hdler.reactions_panel.panel_el.style.opacity).toBe('1');
         });
 
         dom.window.fetch = jest.fn(() => Promise.resolve({
@@ -121,169 +108,176 @@ describe("scene 3 - reactions.test.js module", () => {
         // Get the like button element, and click on it.
         const like_btn_qs = "button[data-code='+']";
         const like_btn_el = reactions_panel_el.querySelector(like_btn_qs);
-        expect(like_btn_el != undefined);
+        expect(like_btn_el).not.toBe(undefined);
         fireEvent.click(like_btn_el);
 
         const formData = new dom.window.FormData();
         formData.append("reaction", "+");
         formData.append("csrfmiddlewaretoken", null);
 
-        expect(dom.window.fetch.mock.calls.length).toEqual(1);
-        expect(dom.window.fetch).toHaveBeenCalledWith(
-            "/comments/react/29/",
-            {
-                method: "POST",
-                cache: "no-cache",
-                credentials: "same-origin",
-                headers: {
-                    "X-Requested-With": "XMLHttpRequest",
-                },
-                body: formData
-            }
-        );
-
         await waitFor(() => {
+            expect(dom.window.fetch.mock.calls.length).toEqual(1);
+            expect(dom.window.fetch).toHaveBeenCalledWith(
+                "/comments/react/29/",
+                {
+                    method: "POST",
+                    cache: "no-cache",
+                    credentials: "same-origin",
+                    headers: {
+                        "X-Requested-With": "XMLHttpRequest",
+                    },
+                    body: formData
+                }
+            );
+
             const feedback_qs = '#cm-feedback-29';
             const reaction_qs = '[data-reaction="+"]';
 
             // The feedback element does exist and has two direct children.
             const feedback_el = comment_el.querySelector(feedback_qs);
-            expect(feedback_el != null);
-            expect(feedback_el.children.length === 2);
+            expect(feedback_el).not.toBe(null);
+            expect(feedback_el.children.length).toBe(2);
 
             // The like reaction element does exist too.
             const like_reaction_el = comment_el.querySelector(reaction_qs);
-            expect(like_reaction_el != null);
-            expect(like_reaction_el.children[0].textContent === '1');
-            expect(like_reaction_el.children[1].textContent === '👍');
-        });
+            expect(like_reaction_el).not.toBe(null);
+            expect(like_reaction_el.children[0].textContent).toBe('1');
+            expect(like_reaction_el.children[1].textContent).toBe('👍');
+        }, {interval: 100, timeout: 2000});
 
         dom.window.fetch.mockClear();
     });
 
     it("clicks on the 'Like' to add and remove the 'Like'", async () => {
-        const panel_qs = '[data-dci=reactions-panel-template]';
-        const anchor_qs = "A[data-dci=reactions-panel]";
+        const panel_qs = '[data-dci="reactions-panel-template"]';
+        const anchor_qs = '[data-dci="reactions-panel"]';
 
         const comment_el = container.querySelector("#comment-29");
-        expect(comment_el != null);
+        expect(comment_el).not.toBe(null);
 
         const react_anchor_el = comment_el.querySelector(anchor_qs);
-        expect(react_anchor_el != null);
+        expect(react_anchor_el).not.toBe(null);
 
         const reactions_panel_el = container.querySelector(panel_qs);
-        expect(reactions_panel_el != null);
+        expect(reactions_panel_el).not.toBe(null);
 
         // Click on the 'react' link to open the reactions panel.
         fireEvent.click(react_anchor_el);
         await waitFor(() => {
             const hdler = dom.window.djCommentsInk.reactions_handler;
-            expect(hdler.active_visible_panel === '29');
-            expect(hdler.reactions_panel.comment_id === '29');
-            expect(hdler.reactions_panel.panel_el.style.opacity == '1')
+            expect(hdler.active_visible_panel).toBe('29');
+            expect(hdler.reactions_panel.comment_id).toBe('29');
+            expect(hdler.reactions_panel.panel_el.style.opacity).toBe('1');
         });
 
-        dom.window.fetch = jest.fn(() => Promise.resolve({
-            status: 201,
-            json: () => Promise.resolve({
-                html: "<div class=\"active-reactions\"><div class=\"reaction\" data-reaction=\"+\"><span class=\"smaller\">1</span><span class=\"emoji\">&#128077;</span><div class=\"reaction_tooltip\">Daniela Rushmore reacted with Like</div></div></div>",
-                reply_to: "0"
-            })
-        }));
+        dom.window.fetch = jest.fn(() => {
+            return Promise.resolve({
+                status: 201,
+                json: () => Promise.resolve({
+                    html: "<div class=\"active-reactions\"><div class=\"reaction\" data-reaction=\"+\"><span class=\"smaller\">1</span><span class=\"emoji\">&#128077;</span><div class=\"reaction_tooltip\">Daniela Rushmore reacted with Like</div></div></div>",
+                    reply_to: "0"
+                })
+            });
+        });
 
         // Get the like button element, and click on it to add the Like.
         const like_btn_qs = "button[data-code='+']";
         const like_btn_el = reactions_panel_el.querySelector(like_btn_qs);
-        expect(like_btn_el != undefined);
+        expect(like_btn_el).not.toBe(undefined);
+        expect(dom.window.fetch.mock.calls.length).toEqual(0);
         fireEvent.click(like_btn_el);
 
         const formData = new dom.window.FormData();
         formData.append("reaction", "+");
         formData.append("csrfmiddlewaretoken", null);
 
-        expect(dom.window.fetch.mock.calls.length).toEqual(1);
-        expect(dom.window.fetch).toHaveBeenCalledWith(
-            "/comments/react/29/",
-            {
-                method: "POST",
-                cache: "no-cache",
-                credentials: "same-origin",
-                headers: {
-                    "X-Requested-With": "XMLHttpRequest",
-                },
-                body: formData
-            }
-        );
-
         await waitFor(() => {
-            const feedback_qs = '#cm-feedback-29';
-            const reaction_qs = '[data-reaction="+"]';
+            expect(dom.window.fetch.mock.calls.length).toEqual(1);
+            expect(dom.window.fetch).toHaveBeenCalledWith(
+                "/comments/react/29/",
+                {
+                    method: "POST",
+                    cache: "no-cache",
+                    credentials: "same-origin",
+                    headers: {
+                        "X-Requested-With": "XMLHttpRequest",
+                    },
+                    body: formData
+                }
+            );
+
+            const reactions_qs = '#cm-reactions-29';
+            const like_qs = '[data-reaction="+"]';
 
             // The feedback element does exist and has two direct children.
-            const feedback_el = comment_el.querySelector(feedback_qs);
-            expect(feedback_el !== null);
-            expect(feedback_el.children.length === 2);
+            const reactions_el = comment_el.querySelector(reactions_qs);
+            expect(reactions_el).not.toBe(null);
+            expect(reactions_el.children.length).toBe(1);
 
             // The like reaction element does exist too.
-            const like_reaction_el = comment_el.querySelector(reaction_qs);
-            expect(like_reaction_el != null);
-            expect(like_reaction_el.children[0].textContent === '1');
-            expect(like_reaction_el.children[1].textContent === '👍');
-        });
+            const like_reaction_el = comment_el.querySelector(like_qs);
+            expect(like_reaction_el).not.toBe(null);
+            expect(like_reaction_el.children[0].textContent).toBe('1');
+            expect(like_reaction_el.children[1].textContent).toBe('👍');
+        }, {interval: 100, timeout: 2000});
         dom.window.fetch.mockClear();
 
         // --------------------------------------
         // Now test that clicking again on the
         // Like button removes the Like reaction.
 
-        dom.window.fetch = jest.fn(() => Promise.resolve({
-            status: 201,
-            json: () => Promise.resolve({
-                html: "<div class=\"active-reactions\"><div class=\"reaction\" data-reaction=\"+\"></div></div>",
-                reply_to: "0"
-            })
-        }));
+        dom.window.fetch = jest.fn(() => {
+            return Promise.resolve({
+                status: 201,
+                json: () => Promise.resolve({
+                    html: "<div class=\"active-reactions\"><div class=\"reaction\" data-reaction=\"+\"></div></div>",
+                    reply_to: "0"
+                })
+            });
+        });
 
         // Click on the Like button.
-        expect(like_btn_el != undefined);
+        expect(like_btn_el).not.toBe(undefined);
+        expect(dom.window.fetch.mock.calls.length).toEqual(0);
         fireEvent.click(like_btn_el);
 
-        expect(dom.window.fetch.mock.calls.length).toEqual(1);
-        expect(dom.window.fetch).toHaveBeenCalledWith(
-            "/comments/react/29/",
-            {
-                method: "POST",
-                cache: "no-cache",
-                credentials: "same-origin",
-                headers: {
-                    "X-Requested-With": "XMLHttpRequest",
-                },
-                body: formData
-            }
-        );
-
         await waitFor(() => {
-            const feedback_qs = '#cm-feedback-29';
-            const reaction_qs = '[data-reaction="+"]';
+            expect(dom.window.fetch.mock.calls.length).toEqual(1);
+            expect(dom.window.fetch).toHaveBeenCalledWith(
+                "/comments/react/29/",
+                {
+                    method: "POST",
+                    cache: "no-cache",
+                    credentials: "same-origin",
+                    headers: {
+                        "X-Requested-With": "XMLHttpRequest",
+                    },
+                    body: formData
+                }
+            );
+
+            const reactions_qs = '#cm-reactions-29';
+            const like_qs = '[data-reaction="+"]';
 
             // The feedback element does exist and has only one direct child.
-            const feedback_el = comment_el.querySelector(feedback_qs);
-            expect(feedback_el != null);
-            expect(feedback_el.children.length === 1);
+            const reactions_el = comment_el.querySelector(reactions_qs);
+            expect(reactions_el).not.toBe(null);
+            expect(reactions_el.children.length).toBe(1);
 
             // Every feedback element's text child node is empty. They would
             // contain &nbsp; to add padding when there are reactions. But
             // when the reactions are removed, the plugin shall remove the
             // explicit paddings too.
-            for (const child of feedback_el.childNodes) {
+            for (const child of reactions_el.childNodes) {
                 if (child.nodeType === Node.TEXT_NODE) {
                     expect(child.textContent === "");
                 }
             }
 
             // The reaction data-code="+" does not exist.
-            const like_reaction_el = comment_el.querySelector(reaction_qs);
-            expect(like_reaction_el == null);
+            const like_reaction_el = comment_el.querySelector(like_qs);
+            expect(like_reaction_el).not.toBe(null);
+            expect(like_reaction_el.children.length).toBe(0);
         });
         dom.window.fetch.mockClear();
     });