diff --git a/.babelrc b/.babelrc index 7bd6e395..243cbdcb 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,12 @@ { - "presets": ["@babel/preset-env", "@babel/preset-react"] -} \ No newline at end of file + "presets": ["@babel/preset-env"], + "plugins": [ + ["@babel/plugin-transform-runtime"] + ], + + "env": { + "test": { + "plugins": ["transform-es2015-modules-commonjs"] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..bce56d17 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +# https://editorconfig.org/ + +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 + +# Docstrings and comments use max_line_length = 79 +[*.py] +max_line_length = 80 + +# Use 2 spaces for the HTML files +[*.html] +indent_size = 2 + +# The JSON files contain newlines inconsistently +[*.json] +indent_size = 2 +insert_final_newline = ignore + +# Minified JavaScript files shouldn't be changed +[**.min.js] +indent_style = ignore +insert_final_newline = ignore + +# Makefiles always use tabs for indentation +[Makefile] +indent_style = tab + +[docs/**.rst] +max_line_length = 79 + +[*.yml] +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..bd862c90 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +**/*.min.js +django_comments_xtd/static/django_comments_xtd/dist/* +docs/_build/**/*.js +node_modules/**.js +js_tests/**/*.js +venv/**/*.js +demos/project-stories/psenv/**/*.js +webpack.config.js +**/webpack.config.js +Gruntfile.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..5f1663ba --- /dev/null +++ b/.eslintrc @@ -0,0 +1,41 @@ +{ + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 8 + }, + "rules": { + "camelcase": ["off", {"properties": "always"}], + "comma-spacing": ["error", {"before": false, "after": true}], + "curly": ["error", "all"], + "dot-notation": ["error", {"allowKeywords": true}], + "eqeqeq": ["error"], + "indent": ["error", 4], + "key-spacing": ["error", {"beforeColon": false, "afterColon": true}], + "linebreak-style": ["error", "unix"], + "new-cap": ["off", {"newIsCap": true, "capIsNew": true}], + "no-alert": ["off"], + "no-eval": ["error"], + "no-extend-native": ["error", {"exceptions": ["Date", "String"]}], + "no-multi-spaces": ["error"], + "no-octal-escape": ["error"], + "no-script-url": ["error"], + "no-shadow": ["error", {"hoist": "functions"}], + "no-underscore-dangle": ["error"], + "no-unused-vars": ["error", {"vars": "local", "args": "none"}], + "no-var": ["error"], + "prefer-const": ["error"], + "quotes": ["off", "single"], + "semi": ["error", "always"], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}], + "space-infix-ops": ["error", {"int32Hint": false}], + "strict": ["error", "global"] + }, + "env": { + "browser": true, + "es6": true + }, + "globals": { + "django": false + } +} diff --git a/demo/project/users/static/users/js/index.js b/demo/project/users/static/users/js/index.js index 65c35728..236715a3 100644 --- a/demo/project/users/static/users/js/index.js +++ b/demo/project/users/static/users/js/index.js @@ -6,16 +6,16 @@ let email_form = null; let pdata_form = null; window.addEventListener("DOMContentLoaded", (_) => { - if (document.getElementById("email_form")) { - email_form = new EmailForm('/user/account/edit/email', - 'email_form', 'email_form_msg'); - } + if (document.getElementById("email_form")) { + email_form = new EmailForm('/user/account/edit/email', + 'email_form', 'email_form_msg'); + } - if (document.getElementById("pdata_form")) { - pdata_form = new PersonalDataForm('/user/account/edit/pdata', - 'pdata_form', 'pdata_form_msg'); - } + if (document.getElementById("pdata_form")) { + pdata_form = new PersonalDataForm('/user/account/edit/pdata', + 'pdata_form', 'pdata_form_msg'); + } }); window.submit_email_form = () => email_form ? email_form.post() : false; -window.submit_pdata_form = () => pdata_form ? pdata_form.post() : false; \ No newline at end of file +window.submit_pdata_form = () => pdata_form ? pdata_form.post() : false; diff --git a/demos/project-stories/project_stories/templates/base.html.py b/demos/project-stories/project_stories/templates/base.html.py deleted file mode 100644 index 503f463e..00000000 --- a/demos/project-stories/project_stories/templates/base.html.py +++ /dev/null @@ -1,84 +0,0 @@ -BBBB BBBB -BBBB BBBBBB -BBBB BBBBBBBBBBBBB - -BBBBBBBBBBBBBBBBBBBBBBB BB BBBBBBBBB -BBBBBBBBBBBBBBBBBBBB BB BBBBBBBBBBBBBBBB - -XXXXXXXXX XXXXX -XXXXXX - XXXXXX - XXXXX XXXXXXXXXXXXXXXX - XXXXX XXXXXXXXXX XXXXXXBBBBBB BBBBBBBBBBBBBBBBBX XX - XXXXXXXBBBBB BBBBBXXXXXXXXBBBBBBBBXXXXXXXX - XXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX - XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXX - XXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX - XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXBBBBBB BBBBBBBBBBXX - XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXBBBBBB BBBBBBBBBBBBBBXX - - BBBBB BBBBBBBBBBBBBBBBB - XXXXXXX - XXXXXX - XXXXXXXX - XXXX XXXXXXXXXXXXXXXXXX - XXXX XXXXXXXXXXXXXX - XXXXXX XXXXXXBBB BBBBBBBBBBXXXXXXXXX XXXXXXXXXXXXXXXX - XXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXX - XXXXXX - XXXX XXXXXXXXXXXXX - BB BBBBBBBBBBBBBBBBBBBBB - BB BBBBBBBBBBBBB - XX XXXXXXBBB BBBBBBBBBBBBBXXXXXXXXXXX - BBBBB - XX XXXXXXXBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBX XXXXXXBBB BBBBBBBBBXXXXXXXXXXXXX - BBBB - XX XXXXXXXBBBBB BBBBBBBBBBBBBBBBBBBBBBBX XXXXXXBBB BBBBBBBXXXXXXXXXXX - BBBBB - XX XXXXXXXBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBX XXXXXXBBB BBBBBBBBBXXXXXXXXXXXXX - BB BBBBBBBBB BB BBBBBBBBBBBBBBBBBBBBBBB - XXXXX XXXXXXXXXXXXX XXXXXXX - BBBBBBBBBBXXXXXX XXXXXXXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXX XXXXXXXXXXX XXXXXXXXXXX XX - XXXXXXX - XXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXX - XXXXXXX XXXXXXXXXXXXXXXXXXXXXXX - BBB BBBBBBBB BB BBBBBBBBB - BBBBBBBBBB BBBBBBBBBBBBBBBB BBBBBBBBBB - XXXXXXX XXXXXXXXXFFFFFFFFFFFFFFXXXXXXXXX - BBBB - XXXXXXX XXXXXXXX XXXXXXXXXFFFFFFFFFFFFFXXXXXXXXX - BBBBBBBBBBBBB - BBBBBB - XXXXXXXXX - XXXXXX - BBBBB - XXXXXX - XXXXXX - XXXXXXXXX - - BBBBB BBBBBBB - BBBBBBBB - - XXXXXXXX - XXXX XXXXXXXXXXXXXXXXXX - XXX - XX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX gettext(u'documentation') XXXX - XXXXXXXXXXXXXXXXXX - XX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXX - XXXXXXXXXXXXXXXXXX - XX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXX - XX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - XXXX - XXXXXXXXXXXXXXXXXXXXXXXXXX - XXXXXX - XXXXXXXXX - - XXXXXXX XXXXXBBB BBBBBBBBBBBBBBBBBBBBXXXXXXXXXXX - XXXXXXX XXXXXBBBBBB BBBBBBBBBBBBBXXXXXXXXXXX - XXXXXXX XXXXXBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBXXXXXXXXXXX - - BBBBB BBBBBBBBBBBBBBBB - XXXXXXX -XXXXXXX \ No newline at end of file diff --git a/demos/project-stories/project_stories/templates/comments/comment.html b/demos/project-stories/project_stories/templates/comments/comment.html index 882d8715..2d0c0525 100644 --- a/demos/project-stories/project_stories/templates/comments/comment.html +++ b/demos/project-stories/project_stories/templates/comments/comment.html @@ -33,13 +33,13 @@ {% endif %} {% if comment_reactions_enabled and is_input_allowed %}
- {% trans "React" %} + {% trans "React" %}
{% endif %} {% endif %} {% if comment.level == 0 and comment.nested_count and not comment.is_removed %}
- {% blocktrans count replies=comment.nested_count%}One reply{% plural %}{{ replies }} replies{% endblocktrans %} + {% blocktrans count replies=comment.nested_count%}One reply{% plural %}{{ replies }} replies{% endblocktrans %}
{% endif %} diff --git a/demos/project-stories/project_stories/templates/comments/preview.html b/demos/project-stories/project_stories/templates/comments/preview.html index 6c7c37d6..4dd72e92 100644 --- a/demos/project-stories/project_stories/templates/comments/preview.html +++ b/demos/project-stories/project_stories/templates/comments/preview.html @@ -35,7 +35,7 @@
-
+
{% else %} {% with parent=form.reply_to.value|get_comment %}
diff --git a/demos/project-stories/project_stories/templates/homepage.html.py b/demos/project-stories/project_stories/templates/homepage.html.py deleted file mode 100644 index c29cfedc..00000000 --- a/demos/project-stories/project_stories/templates/homepage.html.py +++ /dev/null @@ -1,46 +0,0 @@ -BBBBBBB BBBBBBBBBBB - -BBBB BBBB -BBBB BBBBBB - -BBBBB BBBBBBB -XXXXXX - XXXX XXXXXXXXXXXXXXXXXX - XXXX XXXXXXXXXXXX XXX XXXXXXX - XXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXX XXXX XXX XXXXXXX XXX XXX XXXXXXXX XX XXXXXXXXXXXXXXXXXXXX XXXXXXXX XXXXXXX XXX XXXXXX XXXXXXX XXX XXXXXXX XXXXXXXXXXXX XXX XXXXXXX XXXX XXXXXXXXXXX XXXXX XXX XXXX XXXXXXXX XXX XXX XXXX XXX XXXXX XX XXXXXXXX XXXXXXX XXXXXXXXX XXXX XXXX XXXXXXXXXX XX XXXXXXXX XXXXXXX XXXXXXXXX XX XXXXXXX - XXXXXX - XXXXXX - XXXX XXXXXXXXXXXXXXXXXX - XXXX XXXXXXXXXXXXXXXXX - XXXXXXXX XXXXXXXXXXXXXXX - XXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXX - XXX - XXXXXXXX XXX XX XXXXXXX XXX XXXXXXX XXXXXX XXXXX XX XXXXXXXXXXXX XXX XXX XXXXX XXXX XXX XXXXXXXXXX XXX XXXX XXXXXXXXX XXXXX - XXXX - XXXXXXXXXX - XXXXXXXX XXXXXXXXXXXXXXX - XXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX XXX XXXXXXXXXXXXXXX - XXX - XXXXXXXXX XXXXXXX XXX XXXXXXX XXXXXXXXXXXXX XX XXXXXXXXX XXXXXXXXX XXXXXXXXXXXXX XXX XX XXXXXXXXX XX XXXXXXXX XX XXX XXXXXXXXXX - XXXX - XXXXXXXXXX - XXXXXX - XXXXXX - XXXX XXXXXXXXXXXXXXXXXX - XXXX XXXXXXXXXXXXXXXXX - XXXXXXXX XXXXXXXXXXXXXXX - XXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX - XXX - XXXXXXX XXXXXXXXXXXX XXX XXXXX XXXX XXXXXXXXX XXXXXXX XXX XXX XXXXXXXXXXXXXX XXXXXXXX XXX XXX XXXXXXXX XXXX XXXX XXX XXXXXXXXXX - XXXX - XXXXXXXXXX - XXXXXXXX XXXXXXXXXXXXXXX - XXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX - XXX - XXXXXXXX XXXX XXXX XXX XXXXXXX XXXXXXXXXX XXXX XXX XX XXXXXXXX XX XXX XXX XX XXXXXXXXX XX XXXX XXX XXXXXXXX XX XXXX XXXXXXXX - XXXX - XXXXXXXXXX - XXXXXX - XXXXXX -XXXXXXX -BBBBBBBB \ No newline at end of file diff --git a/demos/project-stories/project_stories/templates/stories/story_detail.html b/demos/project-stories/project_stories/templates/stories/story_detail.html index 536ee8af..2a6207bb 100644 --- a/demos/project-stories/project_stories/templates/stories/story_detail.html +++ b/demos/project-stories/project_stories/templates/stories/story_detail.html @@ -58,10 +58,9 @@

{% trans "New comments are not allowed." %}

{% block extra_js %} - - +
{% render_comment_reply_template for object %}
diff --git a/demos/project-stories/project_stories/templates/users/account.html.py b/demos/project-stories/project_stories/templates/users/account.html.py deleted file mode 100644 index 922ee90a..00000000 --- a/demos/project-stories/project_stories/templates/users/account.html.py +++ /dev/null @@ -1,67 +0,0 @@ -BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBB - -BBBB BBBB -BBBB BBBBBB -BBBB BBBBBBBBBBBB - -BBBBB BBBBBXXXXXXXXXXXXXXXXXXXXXXXXXBBBBBBBB - -BBBBB BBBBBBBBB -XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXX -XXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBXX -BBBBBBBB - -BBBBB BBBBBBBBBBBBBBBBBXXXXXXBBBBBBBB - -BBBBB BBBBBBBBBBB -XXXX XXXXXXXXXXXXXXXX XXXXXX - XXXX XXXXXXXXXXXX - XXXX XXXXXXXXXXXXXXX - XXXX gettext(u'Your comments to this website') XXXXX - BB BBBBBBBB - XXX XXXXXXXXXXXXXXXXXXXXXXX XXXX XXXX XXX XXXXXXXXX XXXXXXXXXXXXXX - BBBB - XXX XXXXXXXXXXXXXXXXXXXXXXX XXXX XXX XXXX XXXXXXXX XXXXXXXXX - BBBBB - BBB BBBBBBB BB BBBBBBBB - XXXX XXXXXXXXXXXXX - gettext(u'Posted to:') XX XXXXXXXXXXXXX - BB BBBBBBBBBBBBB B B - XXXXXX gettext(u'In response to another comment.') - BBBBB - BB BBBBBBBBBBBBBBBBBBBB - XXXXXX ngettext(u'It has one response.', u'It has %(number_of_responses)s responses.', count) SS SSS SSS SSSSSSSSSPP PPP PPPPPPPPPPPPPPPPPPPPPPP PPPPPPPPPP - BBBBB - XXXXXX - XXXX XXXXXXXBBBBBBBBBBBBBBBBBBB XXXXXX - XXXX XXXXXXXXXXXXXXXXXX XXXX XXXXXXX - XXXX XXXXXXXXXXXXXXXX - XXXX XXXXXXXXXXXXXXX - XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXXXXXXXX XXXXXXBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBXXXXXXXXXXXXX - XXXXXX - XXXX XXXXXXXXXXXXXFFFFFFFFFFFFFFFFXXXXXX - XXXX XXXXXXXXXXXXXXXXXXXXX - XXXXXX - XXXXXX - BB BBBBBBBBBBBBBBBBBBBBBBB B B - XXX XXXXXXXXXX XXX XXXXXXX - BBB BBBBBBBB BB BBBBBBBBBBBBBBBBBBBBB - BBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB - XXX XXXXXXXXXXXX - ngettext(u'\n One person reacted with &%(r_icon)s\n ', u'\n %(r_counter)s people reacted with &%(r_icon)s\n ', count) - SSS SSSSSS SSSSSSS SSSS SSSSS SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS - - PPPPPPPPPPPPP PPPPPP PPPPPPP PPPP PPPPP PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP - - XFFFFFFFFFFFFFFFFFFF FX - XXXXX - BBBBBBB - BBBBBB - XXXXX - BBBBB - XXXXXX - BBBBBB - XXXXXX - XXXXXX -XXXXXX -BBBBBBBB BBBBBBBBBBB \ No newline at end of file diff --git a/django_comments_xtd/static/django_comments_xtd/js/comment_form.js b/django_comments_xtd/static/django_comments_xtd/js/comment_form.js index 7e1f1537..6f9d630c 100644 --- a/django_comments_xtd/static/django_comments_xtd/js/comment_form.js +++ b/django_comments_xtd/static/django_comments_xtd/js/comment_form.js @@ -1,27 +1,34 @@ export default class CommentForm { constructor(formWrapper) { this.formWrapper = formWrapper; - this.url = ""; // Form action URL. - this._init(); + this.init(); } - _init() { + click_on_post(_) { return this.post("post"); } + + click_on_preview(_) { return this.post("preview"); } + + init() { this.formWrapperEl = document.querySelector(this.formWrapper); this.formEl = this.formWrapperEl.querySelector("form"); - if (!this.url.length) { - this.url = this.formEl.action; - this.formEl.action = ""; - } + const post_btn = this.formEl.elements.post; + const preview_btn = this.formEl.elements.preview; + post_btn.addEventListener("click", (_) => this.post("post")); + preview_btn.addEventListener("click", (_) => this.post("preview")); + // Change the type of the buttons, otherwise the form is submitted. + post_btn.type = "button"; + preview_btn.type = "button"; } - _disable_btns(value) { - this.formEl.elements['post'].disabled = value; - this.formEl.elements['preview'].disabled = value; + disable_btns(value) { + this.formEl.elements.post.disabled = value; + this.formEl.elements.preview.disabled = value; } - _is_valid() { + is_valid() { for (const el of this.formEl.querySelectorAll("[required]")) { if (!el.reportValidity()) { + el.focus(); return false; } } @@ -29,10 +36,10 @@ export default class CommentForm { } post(submit_button_name) { - // this.clean_errors_el(); - if (!this._is_valid()) + if (!this.is_valid()) { return; - this._disable_btns(true); + } + this.disable_btns(true); // If the
...
does exist, // delete it. If the user clicks again in the "preview" button @@ -43,34 +50,37 @@ export default class CommentForm { } const formData = new FormData(this.formEl); - if (submit_button_name != undefined) + if (submit_button_name !== undefined) { formData.append(submit_button_name, 1); + } - fetch(this.url, { + fetch(this.formEl.action, { method: 'POST', headers: { "X-Requested-With": "XMLHttpRequest", }, body: formData }).then(response => { - if (submit_button_name == "preview") + if (submit_button_name === "preview") { this.handle_preview_comment_response(response); - else if (submit_button_name == "post") + } else if (submit_button_name === "post") { this.handle_post_comment_response(response); + } }); - this._disable_btns(false); - return false; // To prevent calling the action attribute. + + this.disable_btns(false); + return false; // To prevent calling the action attribute. } async handle_preview_comment_response(response) { const data = await response.json(); - if (response.status == 200) { + if (response.status === 200) { this.formWrapperEl.innerHTML = data.html; - this._init(); + this.init(); if (data.field_focus) { this.formEl.querySelector(`[name=${data.field_focus}]`).focus(); } - } else if (response.status == 400) { + } else if (response.status === 400) { this.formEl.innerHTML = data.html; } } @@ -78,21 +88,21 @@ export default class CommentForm { async handle_post_comment_response(response) { const data = await response.json(); - if (response.status == 200) { + if (response.status === 200) { this.formWrapperEl.innerHTML = data.html; - this._init(); + this.init(); if (data.field_focus) { this.formEl.querySelector(`[name=${data.field_focus}]`).focus(); } } else if ( - response.status == 201 || - response.status == 202 || - response.status == 400 + response.status === 201 || + response.status === 202 || + response.status === 400 ) { this.formEl.innerHTML = data.html; } - else if (response.status > 400 && response.status < 500) { + else if (response.status > 400) { alert( "Something went wrong and your comment could not be " + "processed. Please, reload the page and try again." diff --git a/django_comments_xtd/static/django_comments_xtd/js/comments.js b/django_comments_xtd/static/django_comments_xtd/js/comments.js index 3f19eef4..a4209d02 100644 --- a/django_comments_xtd/static/django_comments_xtd/js/comments.js +++ b/django_comments_xtd/static/django_comments_xtd/js/comments.js @@ -1,38 +1,43 @@ import CommentForm from "./comment_form.js"; import ReplyFormsHandler from "./reply_forms.js"; -// import ReplyFormsHandler from "./reply_forms_handler.js"; -let comment_form = null; -let reply_forms_handler = null; -function init() { - if (window.comments_api_props == null) - return; +function init_comments() { + if (window.dcx === null) { + return; + } - /* ---------------------------------------------- - * Initialize main comment form. - */ - const qs_cform = "[data-dcx=comment-form]"; - if (document.querySelector(qs_cform)) { - comment_form = new CommentForm(qs_cform); - } + if (window.dcx.page_param === undefined) { + const rroot = document.querySelector("[data-dcx=config]"); + if (rroot) { + window.dcx.page_param = rroot.getAttribute("data-page-qs-param"); + } + } - window.post_comment_form = (submit_button_name) => ( - comment_form ? comment_form.post(submit_button_name) : false - ); + window.dcx.comment_form = null; + window.dcx.reply_forms_handler = null; - /* ---------------------------------------------- - * Initialize reply forms. - */ - const qs_rform_base = "[data-dcx=reply-form-template]"; - const qs_rforms = "[data-dcx=reply-form]"; - if (document.querySelector(qs_rform_base) && - document.querySelectorAll(qs_rforms) - ) { - reply_forms_handler = new ReplyFormsHandler(qs_rform_base, qs_rforms); - } -} + /* ---------------------------------------------- + * Initialize main comment form. + */ + const qs_cform = "[data-dcx=comment-form]"; + if (window.dcx.comment_form === null && + document.querySelector(qs_cform) + ) { + window.dcx.comment_form = new CommentForm(qs_cform); + } -window.addEventListener("DOMContentLoaded", (_) => init()); + /* ---------------------------------------------- + * Initialize reply forms. + */ + const qs_rform_base = "[data-dcx=reply-form-template]"; + const qs_rforms = "[data-dcx=reply-form]"; + if (window.dcx.reply_forms_handler === null && + document.querySelector(qs_rform_base) && + document.querySelectorAll(qs_rforms) + ) { + window.dcx.reply_forms_handler = new ReplyFormsHandler(qs_rform_base, qs_rforms); + } +} -export default init; +export { init_comments }; diff --git a/django_comments_xtd/static/django_comments_xtd/js/index.js b/django_comments_xtd/static/django_comments_xtd/js/index.js index b2970ba6..8ac2178e 100644 --- a/django_comments_xtd/static/django_comments_xtd/js/index.js +++ b/django_comments_xtd/static/django_comments_xtd/js/index.js @@ -1,25 +1,14 @@ -import CommentForm from "./comment_form.js"; -import ReplyFormsHandler from "./reply_forms_handler.js"; +import { init_comments } from "./comments.js"; +import { init_reactions } from "./reactions.js"; - -let comment_form = null; -let reply_forms_handler = null; +window.dcx.init_comments = init_comments; +window.dcx.init_reactions = init_reactions; window.addEventListener("DOMContentLoaded", (_) => { - if (window.comments_api_props != undefined) { - const qs_cform = "[data-dcx=comment-form]"; - const qs_cform_errors = "[data-dcx=comment-form-errors]"; - if (document.querySelector(qs_cform)) { - comment_form = new CommentForm(qs_cform, qs_cform_errors); + if (window.dcx === null) { + return; } - const qs_rform = "[data-dcx=reply-form]"; - if (document.querySelectorAll(qs_rform)) { - reply_forms_handler = new ReplyFormsHandler(qs_rform); - } - } + init_comments(); + init_reactions(); }); - -window.post_comment_form = (submit_button_name) => ( - comment_form ? comment_form.post(submit_button_name) : false -); diff --git a/django_comments_xtd/static/django_comments_xtd/js/reactions.js b/django_comments_xtd/static/django_comments_xtd/js/reactions.js index c2ef8105..f1ba445d 100644 --- a/django_comments_xtd/static/django_comments_xtd/js/reactions.js +++ b/django_comments_xtd/static/django_comments_xtd/js/reactions.js @@ -1,328 +1,337 @@ -let reaction_choices = []; -let reactions_url = "/comments/api/react/"; -let row_length = 4; -let header_title = "Pick your reaction"; +function init_reactions() { + const rroot = document.querySelector("[data-dcx=config]"); + if (rroot === null || window.dcx === null) { + return; + } -function _init_reaction_choices(elem) { - const reactions = elem.getAttribute("data-reactions"); - if (reactions == null) { - throw new Error("Cannot initialize reactions panel => Element with " + - "[data-type=reactions-def] attribute is missing " + - "[data-reactions] attribute."); - } else if (reactions.length == 0) { - throw new Error("Cannot initialize reactions panel => The " + - "[data-reactions] attribute is empty."); - } - const choices = reactions.split(";"); - if (choices.length < 2) { - throw new Error("Cannot initialize reactions panels => The " + - "[data-reactions] attribute must be a semicolon-" + - "separated list of reactions."); - } else { - // Check that each choice is a comma-separated list of 3 items. - for (let i = 0; i < choices.length; i++) { - if (choices[i].split(",").length != 3) { - throw new Error("Cannot initialize reactions panels => The " + - "[data-reactions] attribute must be a semicolon-" + - "separated list of reactions with the format: " + - "code,reaction_title,emoji_unicode."); - } + /* ---------------------------------------------- + * Initialize dcx namespace values. + */ + if (window.dcx.guest === undefined) { + window.dcx.guest = rroot.getAttribute("data-guest-user"); } - } + window.dcx.reactions = { + choices: [], + url: "/comments/api/react/" + }; + window.dcx.reactions.choices = init_reaction_choices(rroot); + window.dcx.reactions.url = init_reactions_url(rroot); - return choices; -} + const header_title = rroot.getAttribute("data-reactions-header-title"); + const row_length = init_row_length(rroot); -function _init_row_length(elem) { - try { - const data_row_length = elem.getAttribute("data-reactions-row-length"); - if (data_row_length == null) { - throw new Error("Cannot initialize reactions panel => Element with " + - "[data-type=reactions-def] attribute is missing " + - "[data-reactions-row-length] attribute."); + /* ---------------------------------------------- + * Initialize the buttons panels and their components. + */ + const links = document.querySelectorAll("[data-dcx=reactions-panel]"); + if (links.length === 0) { + throw new Error("Cannot initialize reactions panel => There are no " + + "elements with [data-dcx=reactions-panel]."); } - return parseInt(data_row_length); - } catch(e) { - throw new Error("Cannot initialize reactions panel => The " + - "[data-reactions-row-length] attribute must be " + - "an integer."); - } + create_buttons_panels(links, row_length, header_title); + + /* ---------------------------------------------- + * Initialize reactions-tooltips. + */ + document + .querySelectorAll("[data-toggle=reactions-tooltip]") + .forEach(node => { + node.addEventListener("mouseover", on_mouseover_tooltip); + node.addEventListener("mouseout", on_mouseout_tooltip); + }); } -function _init_reactions_url(elem) { - const url = elem.getAttribute("data-reactions-url"); - if (url == null || url.length == 0) { - if ( - window.comments_api_props && - window.comments_api_props.is_authenticated - ) { - throw new Error("Cannot initialize reactions panel => The " + - "[data-reactions-url] attribute does not exist or is empty."); +function init_reaction_choices(elem) { + const reactions = elem.getAttribute("data-reactions"); + if (reactions === null) { + throw new Error("Cannot initialize reactions panel => Element with " + + "[data-dcx=config] attribute is missing " + + "[data-reactions] attribute."); + } else if (reactions.length === 0) { + throw new Error("Cannot initialize reactions panel => The " + + "[data-reactions] attribute is empty."); + } + const choices = reactions.split(";"); + if (choices.length < 2) { + throw new Error("Cannot initialize reactions panels => The " + + "[data-reactions] attribute must be a semicolon-" + + "separated list of reactions."); } else { - console.info("Couldn't find the data-reactions-url attribute, but the " + - "user is anonymous. She has to login first in order to post comment " + - "reactions."); + // Check that each choice is a comma-separated list of 3 items. + for (let i = 0; i < choices.length; i++) { + if (choices[i].split(",").length !== 3) { + throw new Error("Cannot initialize reactions panels => The " + + "[data-reactions] attribute must be a semicolon-" + + "separated list of reactions with the format: " + + "code,reaction_title,emoji_unicode."); + } + } } - } - return url; -} - -function init() { - const rroot = document.querySelector("[data-type=reactions-def]"); - if (rroot == null) - return; - /* ---------------------------------------------- - * Initialize global module fields. - */ - reaction_choices = _init_reaction_choices(rroot); - row_length = _init_row_length(rroot); - reactions_url = _init_reactions_url(rroot); - header_title = rroot.getAttribute("data-reactions-header-title"); + return choices; +} - /* ---------------------------------------------- - * Initialize the buttons panels and their components. - */ - const links = document.querySelectorAll("[data-type=reactions-panel]"); - if (links.length == 0) { - throw new Error("Cannot initialize reactions panel => There are no " + - "elements with [data-type=reactions-panel]."); - } - create_buttons_panels(links); +function init_row_length(elem) { + try { + const data_row_length = elem.getAttribute("data-reactions-row-length"); + if (data_row_length === null) { + throw new Error("Cannot initialize reactions panel => Element " + + "with [data-dcx=config] attribute is missing " + + "[data-reactions-row-length] attribute."); + } + return parseInt(data_row_length); + } catch (e) { + throw new Error("Cannot initialize reactions panel => The " + + "[data-reactions-row-length] attribute must be " + + "an integer."); + } +} - /* ---------------------------------------------- - * Initialize reactions-tooltips. - */ - document - .querySelectorAll("[data-toggle=reactions-tooltip]") - .forEach(node => { - node.addEventListener("mouseover", on_mouseover_tooltip); - node.addEventListener("mouseout", on_mouseout_tooltip); - }); +function init_reactions_url(elem) { + const url = elem.getAttribute("data-reactions-url"); + if (url === null || url.length === 0) { + if (window.dcx.guest && window.dcx.guest === "0") { + throw new Error("Cannot initialize reactions panel => The " + + "[data-reactions-url] attribute does not exist or is empty."); + } else { + console.info("Couldn't find the data-reactions-url attribute, " + + "but the user is anonymous. She has to login first in order " + + "to post comment reactions."); + } + } + return url; } + function toggle_buttons_panel(comment_id) { - return (event) => { - event.preventDefault(); - // Hide all the panels but the target. - document.querySelectorAll(".reactions-panel").forEach(elem => { - if (elem.getAttribute("data-crpanel") != comment_id) - elem.style.display = "none"; - }); - // Toggle the panel corresponding to the clicked link. - const panel = document.querySelector(`[data-crpanel="${comment_id}"]`); - if (panel) - if(panel.style.display != "block") - panel.style.display = "block"; - else panel.style.display = ""; - } + return (event) => { + event.preventDefault(); + // Hide all the panels but the target. + document.querySelectorAll(".reactions-panel").forEach(elem => { + if (elem.getAttribute("data-crpanel") !== comment_id) { + elem.style.display = "none"; + } + }); + // Toggle the panel corresponding to the clicked link. + const panel = document.querySelector(`[data-crpanel="${comment_id}"]`); + if (panel) { + if (panel.style.display !== "block") { + panel.style.display = "block"; + } else { + panel.style.display = ""; + } + } + }; } -function create_buttons_panels(nodes) { - for (let elem of Array.from(nodes)) { - const comment_id = elem.getAttribute("data-comment"); - if (comment_id != null) { - const panel = document.createElement("div"); - panel.className = "reactions-panel"; - panel.setAttribute("data-crpanel", `${comment_id}`); +function create_buttons_panels(nodes, row_length, header_title) { + for (const elem of Array.from(nodes)) { + const comment_id = elem.getAttribute("data-comment"); + if (comment_id !== null) { + const panel = document.createElement("div"); + panel.className = "reactions-panel"; + panel.setAttribute("data-crpanel", `${comment_id}`); - const arrow = document.createElement("div"); - arrow.className = "arrow"; - arrow.style.left = "10px"; - panel.appendChild(arrow); + const arrow = document.createElement("div"); + arrow.className = "arrow"; + arrow.style.left = "10px"; + panel.appendChild(arrow); - const header = document.createElement("h3"); - header.textContent = header_title; - panel.appendChild(header); + const header = document.createElement("h3"); + header.textContent = header_title; + panel.appendChild(header); - // For each reaction choice create a reaction button. - const body = document.createElement("p"); - for (let i=0; i 0 && i % row_length == 0) - body.appendChild(document.createElement("br")); - const login_next_url = elem.getAttribute("data-login-next") || ""; - const btn = create_reaction_btn(reaction_choices[i], comment_id, - login_next_url); - btn.addEventListener("mouseover", on_mouseover_reaction_btn(header)); - btn.addEventListener("mouseout", on_mouseout_reaction_btn(header)); - body.appendChild(btn); - } - panel.appendChild(body); - elem.parentNode.insertBefore(panel, elem); - calc_buttons_panel_position(comment_id); - elem.addEventListener("click", toggle_buttons_panel(comment_id)); + // For each reaction choice create a reaction button. + const body = document.createElement("p"); + for (let i = 0; i < window.dcx.reactions.choices.length; i++) { + if (i > 0 && i % row_length === 0) { + body.appendChild(document.createElement("br")); + } + const login_next_url = elem.getAttribute("data-login-next") || ""; + const btn = create_reaction_btn(window.dcx.reactions.choices[i], comment_id, login_next_url); + btn.addEventListener("mouseover", on_mouseover_reaction_btn(header)); + btn.addEventListener("mouseout", on_mouseout_reaction_btn(header, header_title)); + body.appendChild(btn); + } + panel.appendChild(body); + elem.parentNode.insertBefore(panel, elem); + calc_buttons_panel_position(comment_id); + elem.addEventListener("click", toggle_buttons_panel(comment_id)); + } } - } }; function calc_buttons_panel_position(cid) { - const panel = document.querySelector(`[data-crpanel="${cid}"]`); - if (panel) { - const rroot = document.querySelector("[data-type=reactions-def]"); - const bottom = parseInt(rroot.getAttribute("data-pos-bottom")) || 0; - const left = parseInt(rroot.getAttribute("data-pos-left")) || 0; - const elem_sel = `[data-type="reactions-panel"][data-comment="${cid}"]`; - const elem = document.querySelector(elem_sel); - // elem is the React, and elem.parentNode.parentNode is the element - // containing the .active-reactions (reactions already selected by users). - const anchor_pos = elem.getBoundingClientRect(); - const footer_pos = elem.parentNode.parentNode.getBoundingClientRect(); - panel.style.bottom = `${bottom}px`; - panel.style.left = `${anchor_pos.x - footer_pos.x - left}px`; - } + const panel = document.querySelector(`[data-crpanel="${cid}"]`); + if (panel) { + const rroot = document.querySelector("[data-dcx=config]"); + const bottom = parseInt(rroot.getAttribute("data-popover-pos-bottom")) || 0; + const left = parseInt(rroot.getAttribute("data-popover-pos-left")) || 0; + const elem_sel = `[data-dcx="reactions-panel"][data-comment="${cid}"]`; + const elem = document.querySelector(elem_sel); + // elem is the React, and elem.parentNode.parentNode is the element + // containing the .active-reactions (reactions already selected by users). + const anchor_pos = elem.getBoundingClientRect(); + const footer_pos = elem.parentNode.parentNode.getBoundingClientRect(); + panel.style.bottom = `${bottom}px`; + panel.style.left = `${anchor_pos.x - footer_pos.x - left}px`; + } } function on_mouseover_reaction_btn(header) { - return (event) => { - header.textContent = event.target.getAttribute("data-title"); - } + return (event) => { + header.textContent = event.target.getAttribute("data-title"); + }; } -function on_mouseout_reaction_btn(header) { - return (_) => header.textContent = header_title; +function on_mouseout_reaction_btn(header, header_title) { + return (_) => header.textContent = header_title; } function on_click_reaction_btn(crid, cid) { - return (event) => { - event.preventDefault(); - post_reaction({ comment: cid, reaction: crid }).then(data => { - handle_reaction_response(cid, data); - }); - } + return (event) => { + event.preventDefault(); + post_reaction({ comment: cid, reaction: crid }).then(data => { + handle_reaction_response(cid, data); + }); + }; } function on_mouseover_tooltip(event) { - const parent_all = event.target.parentNode.parentNode; - const bottom = parseInt(parent_all.getAttribute("data-pos-bottom")) || 0; - const left = parseInt(parent_all.getAttribute("data-pos-left")) || 0; - const tooltip = event.target.parentNode.children[0]; - const target_pos = event.target.getBoundingClientRect(); - const parent_all_pos = parent_all.getBoundingClientRect(); - if (tooltip.className == "reactions-tooltip") { - tooltip.style.display = "block"; - tooltip.style.bottom = `${bottom}px`; - tooltip.style.left = `${target_pos.x - parent_all_pos.x - left}px`; - } + const parent_all = event.target.parentNode.parentNode; + const bottom = parseInt(parent_all.getAttribute("data-tooltip-pos-bottom")) || 0; + const left = parseInt(parent_all.getAttribute("data-tooltip-pos-left")) || 0; + const tooltip = event.target.parentNode.children[0]; + const target_pos = event.target.getBoundingClientRect(); + const parent_all_pos = parent_all.getBoundingClientRect(); + if (tooltip.className === "reactions-tooltip") { + tooltip.style.display = "block"; + tooltip.style.bottom = `${bottom}px`; + tooltip.style.left = `${target_pos.x - parent_all_pos.x - left}px`; + } } function on_mouseout_tooltip(event) { - const tooltip = event.target.parentNode.children[0]; - if (tooltip.className == "reactions-tooltip") - tooltip.style.display = ""; + const tooltip = event.target.parentNode.children[0]; + if (tooltip.className === "reactions-tooltip") { + tooltip.style.display = ""; + } } function get_cookie(name) { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } } - } - return cookieValue; + return cookieValue; } async function post_reaction(data) { - const response = await fetch(reactions_url, { - method: "POST", - // mode: "no-cors", // mode "no-cors" makes the post fail. - cache: "no-cache", - credentials: "same-origin", - headers: { - "X-CSRFToken": get_cookie("csrftoken"), - "Content-Type": "application/json", - "Accept": "application/json" - }, - redirect: "follow", - referrerPolicy: "no-referrer", - body: JSON.stringify(data) - }); - return response.json(); + const response = await fetch(window.dcx.reactions.url, { + method: "POST", + cache: "no-cache", + credentials: "same-origin", + headers: { + "X-CSRFToken": get_cookie("csrftoken"), + "Content-Type": "application/json", + "Accept": "application/json" + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify(data) + }); + return response.json(); } function create_reaction_btn(reaction, cid, login_next_url) { - const [crid, title, emoji] = reaction.split(","); - const button = document.createElement("button"); - button.setAttribute("data-title", title); - button.setAttribute("data-code", crid); - button.className = "emoji"; - button.innerHTML = `&${emoji};`; - if (window.comments_api_props.is_authenticated) { - button.addEventListener("click", on_click_reaction_btn(crid, cid)); - } else { - button.addEventListener("click", function(_) { - window.location.href = (`${window.comments_api_props.login_url}` + - `?next=${login_next_url}`); - }); - } - return button; + const [crid, title, emoji] = reaction.split(","); + const button = document.createElement("button"); + button.setAttribute("data-title", title); + button.setAttribute("data-code", crid); + button.className = "emoji"; + button.innerHTML = `&${emoji};`; + if (window.dcx.guest === "0") { + button.addEventListener("click", on_click_reaction_btn(crid, cid)); + } else { + button.addEventListener("click", function(_) { + window.location.href = ( + `${window.comments_api_props.login_url}` + + `?next=${login_next_url}` + ); + }); + } + return button; } function handle_reaction_response(cid, data) { - const cm_reactions_div = document.getElementById(`cm-reactions-${cid}`); - if(data.counter > 0) { - const new_list = create_reactions_list(data); - if (cm_reactions_div == null) { - const reactions = document.createElement("div"); - reactions.id = `cm-reactions-${cid}`; - reactions.className = "reactions"; - reactions.appendChild(new_list); - reactions.insertAdjacentHTML("beforeend", " "); - const cm_footer = document.getElementById(`cm-footer-${cid}`); - cm_footer.insertBefore(reactions, cm_footer.children[0]); - reactions.insertAdjacentHTML("afterend", " "); - } else { - const old_list = cm_reactions_div.querySelector(".active-reactions"); - cm_reactions_div.replaceChild(new_list, old_list); + const cm_reactions_div = document.getElementById(`cm-reactions-${cid}`); + if (data.counter > 0) { + const new_list = create_reactions_list(data); + if (cm_reactions_div === null) { + const reactions = document.createElement("div"); + reactions.id = `cm-reactions-${cid}`; + reactions.className = "reactions"; + reactions.appendChild(new_list); + reactions.insertAdjacentHTML("beforeend", " "); + const cm_footer = document.getElementById(`cm-footer-${cid}`); + cm_footer.insertBefore(reactions, cm_footer.children[0]); + reactions.insertAdjacentHTML("afterend", " "); + } else { + const old_list = cm_reactions_div.querySelector(".active-reactions"); + cm_reactions_div.replaceChild(new_list, old_list); + } + } else if (cm_reactions_div) { + cm_reactions_div.remove(); } - } else if (cm_reactions_div) - cm_reactions_div.remove(); - // Recalculate the position of the reactions buttons panel. - calc_buttons_panel_position(cid); + // Recalculate the position of the reactions buttons panel. + calc_buttons_panel_position(cid); } function create_reactions_list(data) { - const js_overlays = window.comments_api_props.reactions_js_overlays; - const list = document.createElement("div"); - list.className = "active-reactions"; - list.setAttribute("data-pos-bottom", js_overlays.tooltip.pos_bottom); - list.setAttribute("data-pos-left", js_overlays.tooltip.pos_left); - for (let item of data.list) { - const reaction = document.createElement("span"); - reaction.className = "reaction"; - reaction.appendChild(create_tooltip(item)); - const anchor = document.createElement("a"); - anchor.className = "small text-primary"; - anchor.setAttribute("data-toggle", "reactions-tooltip"); - anchor.addEventListener("mouseover", on_mouseover_tooltip); - anchor.addEventListener("mouseout", on_mouseout_tooltip); - anchor.appendChild(document.createTextNode(`${item.counter}`)); - const emoji = document.createElement("span"); - emoji.className = "emoji"; - emoji.innerHTML = `&${item.icon};`; - anchor.appendChild(emoji); - reaction.appendChild(anchor); - list.appendChild(reaction); - } - return list; + const rroot = document.querySelector("[data-dcx=config]"); + const list = document.createElement("div"); + list.className = "active-reactions"; + list.setAttribute("data-tooltip-pos-bottom", rroot.getAttribute("data-tooltip-pos-bottom")); + list.setAttribute("data-tooltip-pos-left", rroot.getAttribute("data-tooltip-pos-left")); + for (const item of data.list) { + const reaction = document.createElement("span"); + reaction.className = "reaction"; + reaction.appendChild(create_tooltip(item)); + const anchor = document.createElement("a"); + anchor.className = "small text-primary"; + anchor.setAttribute("data-toggle", "reactions-tooltip"); + anchor.addEventListener("mouseover", on_mouseover_tooltip); + anchor.addEventListener("mouseout", on_mouseout_tooltip); + anchor.appendChild(document.createTextNode(`${item.counter}`)); + const emoji = document.createElement("span"); + emoji.className = "emoji"; + emoji.innerHTML = `&${item.icon};`; + anchor.appendChild(emoji); + reaction.appendChild(anchor); + list.appendChild(reaction); + } + return list; } function create_tooltip(reaction) { - const tooltip = document.createElement("div"); - tooltip.className = "reactions-tooltip"; - const arrow = document.createElement("div"); - arrow.className = "arrow"; - tooltip.appendChild(arrow); - const p = document.createElement("p"); - p.textContent = `${reaction.authors.join(", ")} ` + - `reacted with ${reaction.label}`; - tooltip.appendChild(p); - return tooltip; + const tooltip = document.createElement("div"); + tooltip.className = "reactions-tooltip"; + const arrow = document.createElement("div"); + arrow.className = "arrow"; + tooltip.appendChild(arrow); + const p = document.createElement("p"); + p.textContent = `${reaction.authors.join(", ")} ` + + `reacted with ${reaction.label}`; + tooltip.appendChild(p); + return tooltip; } /* ------------------------------------------------------------------------ @@ -332,23 +341,21 @@ function create_tooltip(reaction) { * or when the user hits the ESC key. */ window.addEventListener("mouseup", (event) => { - let type = event.target.getAttribute("data-type"); - if (type != "reactions-panel") { - // Clicking outside the "React" link must close the reactions panel. - document.querySelectorAll(".reactions-panel").forEach(elem => { - elem.style.display = "none"; - }); - } + const data_attr = event.target.getAttribute("data-dcx"); + if (data_attr !== "reactions-panel") { + // Clicking outside the "React" link must close the reactions panel. + document.querySelectorAll(".reactions-panel").forEach(elem => { + elem.style.display = "none"; + }); + } }); window.addEventListener("keyup", (event) => { - if (event.key == "Escape") { - document.querySelectorAll(".reactions-panel").forEach(elem => { - elem.style.display = "none"; - }); - } + if (event.key === "Escape") { + document.querySelectorAll(".reactions-panel").forEach(elem => { + elem.style.display = "none"; + }); + } }); -window.addEventListener("DOMContentLoaded", (_) => init()); - -export default init; +export { init_reactions }; diff --git a/django_comments_xtd/static/django_comments_xtd/js/reply_forms.js b/django_comments_xtd/static/django_comments_xtd/js/reply_forms.js index f2c1cbca..23ea36c7 100644 --- a/django_comments_xtd/static/django_comments_xtd/js/reply_forms.js +++ b/django_comments_xtd/static/django_comments_xtd/js/reply_forms.js @@ -3,16 +3,16 @@ export default class ReplyFormsHandler { this.replyFormBase = document.querySelector(qsReplyFormBase); this.replyMap = new Map(); - const cpage_field = window.comments_api_props.comments_page_qs_param || "cpage"; + const cpage_field = window.dcx.page_param || "cpage"; - for (let elem of document.querySelectorAll(qsReplyForms)) { + for (const elem of document.querySelectorAll(qsReplyForms)) { // Extract the reply_to value from the current reply_form. // Also, it if does exist, extract the comment's page number too. // Then replace the content of elem with a copy of // this.replyFormBase and update the fields reply_to // and comment's page number. const rFormEl = elem.querySelector("form"); - if (rFormEl == null) { + if (rFormEl === null) { console.error( `Could not find a reply form within one of ` + `the elements retrieved with ${qsReplyForms}.` @@ -20,7 +20,7 @@ export default class ReplyFormsHandler { return; } - const reply_to = rFormEl.elements['reply_to'].value; + const reply_to = rFormEl.elements.reply_to.value; const cpage = rFormEl.elements[cpage_field] ? rFormEl.elements[cpage_field].value : null; @@ -30,8 +30,10 @@ export default class ReplyFormsHandler { // Update fields reply_to and cpage.. const newForm = section.querySelector("form"); - newForm.elements['reply_to'].value = reply_to; - if (cpage) newForm.elements[cpage_field].value = cpage; + newForm.elements.reply_to.value = reply_to; + if (cpage) { + newForm.elements[cpage_field].value = cpage; + } elem.replaceWith(section); this.init(reply_to); @@ -45,11 +47,11 @@ export default class ReplyFormsHandler { // Modify the form (update fields, add event listeners). const newForm = section.querySelector("form"); - const post_btn = newForm.elements['post']; + const post_btn = newForm.elements.post; post_btn.addEventListener("click", this.send_clicked(reply_to)); - const preview_btn = newForm.elements['preview']; + const preview_btn = newForm.elements.preview; preview_btn.addEventListener("click", this.preview_clicked(reply_to)); - const cancel_btn = newForm.elements['cancel']; + const cancel_btn = newForm.elements.cancel; cancel_btn.addEventListener("click", this.cancel_clicked(reply_to)); newForm.style.display = "none"; @@ -59,11 +61,11 @@ export default class ReplyFormsHandler { ta.addEventListener("focus", this.textarea_focus(reply_to)); // If is_active is true, hide the textarea and display the form. - if (is_active == true) { + if (is_active === true) { section.classList.add("active"); divta.style.display = "none"; newForm.style = ""; - newForm.elements['comment'].focus(); + newForm.elements.comment.focus(); } } @@ -78,8 +80,8 @@ export default class ReplyFormsHandler { } disable_buttons(formEl, value) { - formEl.elements['post'].disabled = value; - formEl.elements['preview'].disabled = value; + formEl.elements.post.disabled = value; + formEl.elements.preview.disabled = value; } is_valid(formEl) { @@ -100,8 +102,8 @@ export default class ReplyFormsHandler { item.classList.toggle("active"); divta.style.display = "none"; form.style = ""; - form.elements['comment'].focus(); - } + form.elements.comment.focus(); + }; } cancel_clicked(reply_to) { @@ -110,7 +112,7 @@ export default class ReplyFormsHandler { const item = this.get_map_item(reply_to); const form = item.querySelector("form"); const divta = item.querySelector("[data-dcx=reply-textarea]"); - const comment_value = form.elements['comment'].value; + const comment_value = form.elements.comment.value; divta.querySelector("textarea").value = comment_value; item.classList.toggle("active"); form.style.display = "none"; @@ -120,27 +122,29 @@ export default class ReplyFormsHandler { if (previewEl) { previewEl.remove(); } - } + }; } preview_clicked(reply_to) { return (_) => { this.post("preview", reply_to); - } + }; } send_clicked(reply_to) { return (_) => { this.post("post", reply_to); - } + }; } post(submit_button_name, reply_to) { const item = this.get_map_item(reply_to); const formEl = item.querySelector("form"); - if (!this.is_valid(formEl)) + if (!this.is_valid(formEl)) { return; + } + this.disable_buttons(formEl, true); const previewEl = item.querySelector("[data-dcx=preview]"); @@ -149,7 +153,7 @@ export default class ReplyFormsHandler { } const formData = new FormData(formEl); - if (submit_button_name != undefined) { + if (submit_button_name !== undefined) { formData.append(submit_button_name, 1); } @@ -160,14 +164,14 @@ export default class ReplyFormsHandler { }, body: formData }).then(response => { - if (submit_button_name == "preview") { + if (submit_button_name === "preview") { this.handle_preview_comment_response(response, reply_to); - } else if (submit_button_name == "post") { + } else if (submit_button_name === "post") { this.handle_post_comment_response(response, reply_to); } }); this.disable_buttons(formEl, false); - return false; // To prevent calling the action attribute. + return false; // To prevent calling the action attribute. } handle_http_200(item, data, reply_to) { @@ -176,7 +180,7 @@ export default class ReplyFormsHandler { const qs_section = `[data-dcx=reply-form-${reply_to}]`; const new_item = parent.querySelector(qs_section); this.replyMap.set(reply_to, new_item); - this.init(reply_to, true); // 2nd param: is_active = true. + this.init(reply_to, true); // 2nd param: is_active = true. if (data.field_focus) { new_item.querySelector(`[name=${data.field_focus}]`).focus(); } @@ -202,7 +206,7 @@ export default class ReplyFormsHandler { const item = this.get_map_item(reply_to); const data = await response.json(); - if (response.status == 200) { + if (response.status === 200) { this.handle_http_200(item, data, reply_to); } else if ( @@ -216,7 +220,7 @@ export default class ReplyFormsHandler { alert( "Something went wrong and your comment could not be " + "processed. Please, reload the page and try again." - ); + ); } } -} \ No newline at end of file +} diff --git a/django_comments_xtd/templates/comments/comment.html b/django_comments_xtd/templates/comments/comment.html index 5e36f993..afbefffb 100644 --- a/django_comments_xtd/templates/comments/comment.html +++ b/django_comments_xtd/templates/comments/comment.html @@ -29,13 +29,13 @@ {% endif %} {% if comment_reactions_enabled and is_input_allowed %}
- {% trans "React" %} + {% trans "React" %}
{% endif %} {% endif %} {% if comment.level == 0 and comment.nested_count and not comment.is_removed %}
- {% blocktrans count replies=comment.nested_count%}One reply{% plural %}{{ replies }} replies{% endblocktrans %} + {% blocktrans count replies=comment.nested_count%}One reply{% plural %}{{ replies }} replies{% endblocktrans %}
{% endif %}
diff --git a/django_comments_xtd/templates/comments/comment_reactions.html b/django_comments_xtd/templates/comments/comment_reactions.html index 2e3c7c36..254745d0 100644 --- a/django_comments_xtd/templates/comments/comment_reactions.html +++ b/django_comments_xtd/templates/comments/comment_reactions.html @@ -2,8 +2,8 @@
{% for reaction in comment.reactions.all %} {% if reaction.counter > 0 %} @@ -21,4 +21,4 @@ {% endwith %} {% endif %} {% endfor %} -
\ No newline at end of file + diff --git a/django_comments_xtd/templates/comments/form.html b/django_comments_xtd/templates/comments/form.html index eecfe9b1..58b17e45 100644 --- a/django_comments_xtd/templates/comments/form.html +++ b/django_comments_xtd/templates/comments/form.html @@ -58,8 +58,8 @@
-   - +   +
diff --git a/django_comments_xtd/templates/comments/list.html b/django_comments_xtd/templates/comments/list.html index 96c8d8a6..19523925 100644 --- a/django_comments_xtd/templates/comments/list.html +++ b/django_comments_xtd/templates/comments/list.html @@ -6,15 +6,19 @@ id="comments" class="comment-list" {% if comment_reactions_enabled %} - data-type="reactions-def" + data-dcx="config" data-reactions="{% reactions_enum_strlist %}" {% if user.is_authenticated %} data-reactions-url="{% url 'comments-xtd-api-react' %}" {% endif %} + data-guest-user="{% if user.is_authenticated %}0{% else %}1{% endif %}" data-reactions-row-length="4" data-reactions-header-title="{% trans 'Pick your reaction' %}" - data-pos-bottom="{{ reactions_popover_pos_bottom }}" - data-pos-left="{{ reactions_popover_pos_left }}" + data-popover-pos-bottom="{{ reactions_popover_pos_bottom }}" + data-popover-pos-left="{{ reactions_popover_pos_left }}" + data-tooltip-pos-bottom="{{ reactions_tooltip_pos_bottom }}" + data-tooltip-pos-left="{{ reactions_tooltip_pos_left }}" + data-page-qs-param="{{ comments_page_qs_param }}" {% endif %} > {% if is_paginated %} diff --git a/django_comments_xtd/templatetags/comments_xtd.py b/django_comments_xtd/templatetags/comments_xtd.py index 3646c59e..504b303d 100644 --- a/django_comments_xtd/templatetags/comments_xtd.py +++ b/django_comments_xtd/templatetags/comments_xtd.py @@ -490,7 +490,6 @@ def get_comment(comment_id: str): @register.simple_tag() def dcx_custom_selector(): - print(f"custom_selector: {settings.COMMENTS_XTD_CSS_CUSTOM_SELECTOR}") return f"{settings.COMMENTS_XTD_CSS_CUSTOM_SELECTOR}" diff --git a/django_comments_xtd/tests/templates/my_comments/list.html b/django_comments_xtd/tests/templates/my_comments/list.html index a03016fa..9728efd6 100644 --- a/django_comments_xtd/tests/templates/my_comments/list.html +++ b/django_comments_xtd/tests/templates/my_comments/list.html @@ -6,15 +6,19 @@ id="comments" class="comment-list" {% if comment_reactions_enabled %} - data-type="reactions-def" + data-dcx="config" data-reactions="{% reactions_enum_strlist %}" {% if user.is_authenticated %} data-reactions-url="{% url 'comments-xtd-api-react' %}" {% endif %} + data-guest-user="{% if user.is_authenticated %}0{% else %}1{% endif %}" data-reactions-row-length="4" data-reactions-header-title="{% trans 'Pick your reaction' %}" - data-pos-bottom="{{ reactions_popover_pos_bottom }}" - data-pos-left="{{ reactions_popover_pos_left }}" + data-popover-pos-bottom="{{ reactions_popover_pos_bottom }}" + data-popover-pos-left="{{ reactions_popover_pos_left }}" + data-tooltip-pos-bottom="{{ reactions_tooltip_pos_bottom }}" + data-tooltip-pos-left="{{ reactions_tooltip_pos_left }}" + data-page-qs-param="{{ comments_page_qs_param }}" {% endif %} > {% if is_paginated %} @@ -66,4 +70,4 @@ {% if is_paginated %} {% include "comments/pagination.html" %} {% endif %} - \ No newline at end of file + diff --git a/django_comments_xtd/views.py b/django_comments_xtd/views.py index f4a65430..d8d5ea88 100644 --- a/django_comments_xtd/views.py +++ b/django_comments_xtd/views.py @@ -232,14 +232,21 @@ def post_js(request, next=None, using=None): except ObjectDoesNotExist: error_msg = ( "No object matching content-type %r and object PK %r exists." - % (escape(ctype), escape(object_pk)) + % ( + escape(ctype), + escape(object_pk), + ) ) context = {"bad_error": error_msg} return json_res(request, "comments/bad_form.html", context, status=400) except (ValueError, ValidationError) as e: error_msg = ( "Attempting to get content-type %r and object PK %r raised %s" - % (escape(ctype), escape(object_pk), e.__class__.__name__) + % ( + escape(ctype), + escape(object_pk), + e.__class__.__name__, + ) ) context = {"bad_error": error_msg} return json_res(request, "comments/bad_form.html", context, status=400) @@ -497,7 +504,6 @@ def sent_js(request, comment, using=None): target = model._default_manager.using(using).get(pk=object_pk) except Exception: context = {"bad_error": "Comment does not exist."} - print("Returning bad_form in sent_js.") return json_res( request, "comments/bad_form.html", context, status=400 ) @@ -526,7 +532,6 @@ def sent_js(request, comment, using=None): request, comment.content_type.id, comment.object_pk, comment.id ) comment_url = get_comment_url(comment, None, page_number) - print("Returning published_tmpl.") return json_res( request, published_tmpl, @@ -541,7 +546,6 @@ def sent_js(request, comment, using=None): % comment.content_type.app_label, "comments/moderated_js.html", ] - print("Returning moderated_tmpl.") return json_res( request, moderated_tmpl, {"comment": comment}, status=201 ) diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..12e34b3c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,194 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/b8/kt0gzxn107d3p1m78sc42bnw0000gn/T/jest_dx", + + // Automatically clear mock calls, instances and results before every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + // coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + // coverageProvider: "babel", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + globalSetup: "/global-setup.js", + + // A path to a module which exports an async function that is triggered once after all test suites + globalTeardown: "/global-teardown.js", + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + rootDir: "./js_tests", + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: ["/fetch-setup.js"], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "jsdom", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "**/__tests__/**/*.[jt]s?(x)", + "**/?(*.)+(spec|test).[tj]s?(x)" + ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + verbose: false, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/js_tests/fetch-setup.js b/js_tests/fetch-setup.js new file mode 100644 index 00000000..c2c3d0dc --- /dev/null +++ b/js_tests/fetch-setup.js @@ -0,0 +1,3 @@ +// require('jest-fetch-mock').enableMocks(); +import fetchMock from 'jest-fetch-mock'; +fetchMock.enableMocks(); diff --git a/js_tests/global-setup.js b/js_tests/global-setup.js new file mode 100644 index 00000000..8c38370b --- /dev/null +++ b/js_tests/global-setup.js @@ -0,0 +1,14 @@ +const path = require('path'); + +const { setup: setupDevServer } = require('jest-dev-server'); + +const server_path = path.resolve(__dirname, "serve_static.js"); + +module.exports = async function globalSetup() { + await setupDevServer({ + command: `node ${server_path}`, + launchTimeout: 50000, + port: 3000, + debug: true + }); +}; diff --git a/js_tests/global-teardown.js b/js_tests/global-teardown.js new file mode 100644 index 00000000..094a8349 --- /dev/null +++ b/js_tests/global-teardown.js @@ -0,0 +1,5 @@ +const { teardown: teardownDevServer } = require('jest-dev-server'); + +module.exports = async function globalTeardown() { + await teardownDevServer(); +}; diff --git a/js_tests/serve_static.js b/js_tests/serve_static.js new file mode 100644 index 00000000..47e6d236 --- /dev/null +++ b/js_tests/serve_static.js @@ -0,0 +1,16 @@ +const path = require("path"); +const express = require("express"); +const app = express(); +const port = 3000; + +const PRJ_PATH = path.resolve( + __dirname, '..', 'django_comments_xtd', 'static', 'django_comments_xtd' +); + +app.use(express.static(PRJ_PATH)); + +app.get('/', (req, res) => { + res.send("Serve static resources for jest tests."); +}); + +app.listen(port, () => {}); diff --git a/js_tests/tests/scene1/README.md b/js_tests/tests/scene1/README.md new file mode 100644 index 00000000..3b829c25 --- /dev/null +++ b/js_tests/tests/scene1/README.md @@ -0,0 +1,8 @@ +# JavaScript Tests - Scene 1 + +This scene represents the situation in which: + * there is no comments posted yet, + * only a comment form is displayed. + * Therefore the reactions.js module is not used, and + * neither is used the reply_forms.js module. + * Only the module comment_form.js is tested. diff --git a/js_tests/tests/scene1/comment_form.test.js b/js_tests/tests/scene1/comment_form.test.js new file mode 100644 index 00000000..05d49f8f --- /dev/null +++ b/js_tests/tests/scene1/comment_form.test.js @@ -0,0 +1,379 @@ +import fs from 'fs'; +import path from 'path'; + +import { + findByText, fireEvent, getByLabelText, getByPlaceholderText, getByRole, + waitFor +} from '@testing-library/dom'; +import '@testing-library/jest-dom/extend-expect'; +import { JSDOM, ResourceLoader } from 'jsdom'; + +let dom; +let container; +let qs_cform; + +const index_html_path = path.resolve(__dirname, './index.html'); + +const preview_form_wrong_email_200 = fs.readFileSync( + path.resolve(__dirname, './preview_form_wrong_email_200.html') +).toString(); + +const preview_form_success_200 = fs.readFileSync( + path.resolve(__dirname, './preview_form_success_200.html') +).toString(); + +const post_form_receives_400 = fs.readFileSync( + path.resolve(__dirname, './post_form_receives_400.html') +).toString(); + +const post_form_receives_201 = fs.readFileSync( + path.resolve(__dirname, './post_form_receives_201.html') +).toString(); + +const post_form_receives_202 = fs.readFileSync( + path.resolve(__dirname, './post_form_receives_202.html') +).toString(); + +function prepare_form_with_wrong_email(formEl) { + const comment_ta = getByPlaceholderText(formEl, /your comment/i); + fireEvent.change(comment_ta, {target: {value: "This is my comment"}}); + const name_field = getByLabelText(formEl, /name/i); + fireEvent.change(name_field, {target: {value: "Emma"}}); + const email_field = getByLabelText(formEl, /mail/i); + // Feed the email field with a non-valid email address. + fireEvent.change(email_field, {target: {value: "emma"}}); +} + +function prepare_valid_form(formEl) { + const comment_ta = getByPlaceholderText(formEl, /your comment/i); + fireEvent.change(comment_ta, {target: {value: "This is my comment"}}); + const name_field = getByLabelText(formEl, /name/i); + fireEvent.change(name_field, {target: {value: "Emma"}}); + const email_field = getByLabelText(formEl, /mail/i); + fireEvent.change(email_field, {target: {value: "emma@example.com"}}); +} + +describe("comments.test.js module", () => { + beforeEach(async () => { + const resourceLoader = new ResourceLoader({ + proxy: "http://localhost:3000", + strictSSL: false + }); + const opts = { runScripts: "dangerously", resources: resourceLoader }; + dom = await JSDOM.fromFile(index_html_path, opts); + await new Promise(resolve => { + dom.window.addEventListener("DOMContentLoaded", () => { + container = dom.window.document.body; + qs_cform = "[data-dcx=comment-form]"; + resolve(); + }); + }); + }); + + it("asserts window.dcx.comment_form attributes", () => { + expect(dom.window.dcx !== null && dom.window.dcx !== undefined); + expect(dom.window.dcx.comment_form !== null); + expect(dom.window.dcx.comment_form.formWrapper === qs_cform); + + const elem = container.querySelector(qs_cform); + expect(dom.window.dcx.comment_form.formWrapperEl === elem); + + const form = elem.querySelector("form"); + expect(dom.window.dcx.comment_form.formEl === form); + }); + + it("previewing empty form focuses on textarea comment", () => { + const cFormWrapper = container.querySelector(qs_cform); + const preview = getByRole(cFormWrapper, "button", {name: /preview/i}); + fireEvent.click(preview); + const comment_ta = getByPlaceholderText(cFormWrapper, /your comment/i); + expect(dom.window.document.activeElement).toEqual(comment_ta); + }); + + it("previewing form focuses on input name", () => { + // This happens only when the comment textarea has content. + const cFormWrapper = container.querySelector(qs_cform); + const comment_ta = getByPlaceholderText(cFormWrapper, /your comment/i); + fireEvent.change(comment_ta, {target: {value: "This is my comment"}}); + const preview = getByRole(cFormWrapper, "button", {name: /preview/i}); + fireEvent.click(preview); + const name_field = getByLabelText(cFormWrapper, /name/i); + expect(dom.window.document.activeElement).toEqual(name_field); + }); + + it("previewing form focuses on input email", () => { + // This happens only when both comment and name fields have content. + const cFormWrapper = container.querySelector(qs_cform); + const comment_ta = getByPlaceholderText(cFormWrapper, /your comment/i); + fireEvent.change(comment_ta, {target: {value: "This is my comment"}}); + const name_field = getByLabelText(cFormWrapper, /name/i); + fireEvent.change(name_field, {target: {value: "Emma"}}); + const preview = getByRole(cFormWrapper, "button", {name: /preview/i}); + fireEvent.click(preview); + const email_field = getByLabelText(cFormWrapper, /mail/i); + expect(dom.window.document.activeElement).toEqual(email_field); + }); + + it("preview tampered form receives 400 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_form_with_wrong_email(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 400, + json: () => Promise.resolve({ + html: post_form_receives_400 + }) + })); + + const preview = getByRole(formEl, "button", {name: /preview/i}); + fireEvent.click(preview); + + const formData = new dom.window.FormData(formEl); + formData.append("preview", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + body: formData + } + ); + + await findByText(container, "An error has happened."); + await findByText(container, "The comment form failed security verification."); + await waitFor(() => { + const qs = "[data-dcx=comment-form] form h6"; + const h6 = dom.window.document.querySelector(qs); + expect(h6.textContent.indexOf("An error has happened.") > -1); + }); + dom.window.fetch.mockClear(); + }); + + it("preview form with wrong email receives 200 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_form_with_wrong_email(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 200, + json: () => Promise.resolve({ + html: preview_form_wrong_email_200, + field_focus: "email" + }) + })); + + const preview = getByRole(formEl, "button", {name: /preview/i}); + fireEvent.click(preview); + + const formData = new dom.window.FormData(formEl); + formData.append("preview", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + body: formData + } + ); + + await findByText(container, "Enter a valid email address."); + const email_input = container.querySelector("form [name=email]"); + expect(dom.window.document.activeElement).toEqual(email_input); + dom.window.fetch.mockClear(); + }); + + it("preview form receives 200 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_valid_form(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 200, + json: () => Promise.resolve({ html: preview_form_success_200 }) + })); + + const preview = getByRole(formEl, "button", {name: /preview/i}); + fireEvent.click(preview); + + const formData = new dom.window.FormData(formEl); + formData.append("preview", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + body: formData + } + ); + + await waitFor(() => { + expect( + dom.window.document.querySelector("[data-dcx=preview]") + ).toBeInTheDocument(); + }); + dom.window.fetch.mockClear(); + }); + + it("post form receives 400 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_valid_form(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 400, + json: () => Promise.resolve({ html: post_form_receives_400 }) + })); + + const send_btn = getByRole(formEl, "button", {name: /send/i}); + fireEvent.click(send_btn); + + const formData = new dom.window.FormData(formEl); + formData.append("post", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest" + }, + body: formData + } + ); + + await findByText(container, "An error has happened."); + await findByText(container, "The comment form failed security verification."); + await waitFor(() => { + const qs = "[data-dcx=comment-form] form h6"; + const h6 = dom.window.document.querySelector(qs); + expect(h6.textContent.indexOf("An error has happened.") > -1); + }); + dom.window.fetch.mockClear(); + }); + + it("post form receives 201 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_valid_form(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 201, + json: () => Promise.resolve({ html: post_form_receives_201 }) + })); + + const send_btn = getByRole(formEl, "button", {name: /send/i}); + fireEvent.click(send_btn); + + const formData = new dom.window.FormData(formEl); + formData.append("post", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest" + }, + body: formData + } + ); + + await findByText(container, "Comment published"); + await waitFor(() => { + const qs = "[data-dcx=comment-form] form > div > div"; + const alert = dom.window.document.querySelector(qs); + expect(alert.textContent.indexOf("Comment published") > -1); + }); + dom.window.fetch.mockClear(); + }); + + it("post form receives 202 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_valid_form(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 202, + json: () => Promise.resolve({ html: post_form_receives_202 }) + })); + + const send_btn = getByRole(formEl, "button", {name: /send/i}); + fireEvent.click(send_btn); + + const formData = new dom.window.FormData(formEl); + formData.append("post", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest" + }, + body: formData + } + ); + + const text = "Comment confirmation requested"; + await findByText(container, text); + await waitFor(() => { + const qs = "[data-dcx=comment-form] form > div > div"; + const alert = dom.window.document.querySelector(qs); + expect(alert.textContent.indexOf(text) > -1); + }); + dom.window.fetch.mockClear(); + }); + + it("post form receives 500 response", async () => { + const cFormWrapper = container.querySelector(qs_cform); + const formEl = cFormWrapper.querySelector("form"); + prepare_valid_form(formEl); + + dom.window.fetch = jest.fn(() => Promise.resolve({ + status: 500, + json: () => Promise.resolve({}) + })); + + const alertMock = jest.spyOn(dom.window, 'alert').mockImplementation(); + + const send_btn = getByRole(formEl, "button", {name: /send/i}); + fireEvent.click(send_btn); + + const formData = new dom.window.FormData(formEl); + formData.append("post", 1); + + expect(dom.window.fetch.mock.calls.length).toEqual(1); + expect(dom.window.fetch).toHaveBeenCalledWith( + "file:///comments/post/", + { + method: "POST", + headers: { + "X-Requested-With": "XMLHttpRequest" + }, + body: formData + } + ); + + await waitFor(() => { + expect(alertMock).toHaveBeenCalledTimes(1); + }); + dom.window.fetch.mockClear(); + alertMock.mockClear(); + }); +}); diff --git a/js_tests/tests/scene1/comments.test.js b/js_tests/tests/scene1/comments.test.js new file mode 100644 index 00000000..61102948 --- /dev/null +++ b/js_tests/tests/scene1/comments.test.js @@ -0,0 +1,46 @@ +import path from 'path'; + +import { getByText } from '@testing-library/dom'; +import '@testing-library/jest-dom/extend-expect'; +import { JSDOM, ResourceLoader } from 'jsdom'; + + +const html_path = path.resolve(__dirname, './index.html'); + +let dom; +let container; +let qs_cform; + +describe("comments.test.js module", () => { + beforeEach(async () => { + const resourceLoader = new ResourceLoader({ + proxy: "http://localhost:3000", + strictSSL: false + }); + const opts = { runScripts: "dangerously", resources: resourceLoader }; + dom = await JSDOM.fromFile(html_path, opts); + await new Promise(resolve => { + dom.window.addEventListener("DOMContentLoaded", () => { + dom.window.dcx.init_comments(); + container = dom.window.document.body; + qs_cform = "[data-dcx=comment-form]"; + resolve(); + }); + }); + }); + + it("creates window.dcx.comment_form attribute !== null", () => { + expect(dom.window.dcx !== null && dom.window.dcx !== undefined); + expect(dom.window.dcx.comment_form !== null); + }); + + it("has a div with [data-dcx=comment-form]", () => { + expect(container.querySelector(qs_cform)); + expect(getByText(container, 'Post your comment')).toBeInTheDocument(); + }); + + it("creates window.dcx.reply_forms_handler attribute === null", () => { + expect(dom.window.dcx !== null && dom.window.dcx !== undefined); + expect(dom.window.dcx.comment_form === null); + }); +}); diff --git a/js_tests/tests/scene1/index.html b/js_tests/tests/scene1/index.html new file mode 100644 index 00000000..24c84d60 --- /dev/null +++ b/js_tests/tests/scene1/index.html @@ -0,0 +1,148 @@ + + + + + + dcx-demo • stories + + + + + +
+
+
+

stories project

+

with django-comments-xtd

+
+ +
+
+
+
+

Net Neutrality in Jeopardy

+
April 3, 2021, 2:53 p.m.
+
+
+

The battle over the future of the Internet is a power grab that pits well-heeled lobbyists, corrupt legislators, phony front groups and the world’s most powerful telecommunications companies against the rest of us — the millions of Americans who use the Internet every day, in increasingly inventive ways.

+

Policymakers are public officials, and it’s their job to serve the public interest. You can play an important role by helping spread the word about Net Neutrality, meeting face to face with elected officials and urging them to protect the open Internet.

+

You can start by urging the Senate to stand up for Net Neutrality.

+
+
+
+
Post your comment
+
+ + + + + + + +
+
+
+ +
+ +
+ +
+ +
+ +
+
Required for comment verification. Otherwise login.
+ +
+ +
+
+ + +
+
+
+   + +
+
+
+
+
+
+
+
+ + + + + + +
+
+ +
+ +
+
+
+ + diff --git a/js_tests/tests/scene1/post_form_receives_201.html b/js_tests/tests/scene1/post_form_receives_201.html new file mode 100644 index 00000000..e1903d7f --- /dev/null +++ b/js_tests/tests/scene1/post_form_receives_201.html @@ -0,0 +1,8 @@ +
+
+ Comment published +
+

+ Thank you for taking the time to participate! Your comment has been already published. Follow this link to see it. +

+
diff --git a/js_tests/tests/scene1/post_form_receives_202.html b/js_tests/tests/scene1/post_form_receives_202.html new file mode 100644 index 00000000..efc057f8 --- /dev/null +++ b/js_tests/tests/scene1/post_form_receives_202.html @@ -0,0 +1,9 @@ +
+
+ Comment confirmation requested +
+

+ A confirmation message has been sent to your email address. Please, click on the link in the message to confirm your comment. +

+
+ diff --git a/js_tests/tests/scene1/post_form_receives_400.html b/js_tests/tests/scene1/post_form_receives_400.html new file mode 100644 index 00000000..d1fc477c --- /dev/null +++ b/js_tests/tests/scene1/post_form_receives_400.html @@ -0,0 +1,6 @@ +
+
+ An error has happened. +
+
The comment form failed security verification.
+
diff --git a/js_tests/tests/scene1/preview_form_success_200.html b/js_tests/tests/scene1/preview_form_success_200.html new file mode 100644 index 00000000..2eb69367 --- /dev/null +++ b/js_tests/tests/scene1/preview_form_success_200.html @@ -0,0 +1,57 @@ +
+
+
+
+
Emma
+
comment in preview
+
+
+

This is my comment.

+
+ +
+
+
+
+ + +
+
Post your comment
+
+ + + + + + + +
+
+
+ +
+ +
+ +
+ +
+ +
+
Required for comment verification. Otherwise login.
+ +
+ +
+
+ + +
+
+
+   + +
+
+
+
diff --git a/js_tests/tests/scene1/preview_form_wrong_email_200.html b/js_tests/tests/scene1/preview_form_wrong_email_200.html new file mode 100644 index 00000000..75246d9c --- /dev/null +++ b/js_tests/tests/scene1/preview_form_wrong_email_200.html @@ -0,0 +1,40 @@ +
+
Post your comment
+
+ + + + + + + +
+
+
+ +
+ +
+ +
+ +
+ +
+
Enter a valid email address.
+ +
+ +
+
+ + +
+
+
+   + +
+
+
+
diff --git a/package.json b/package.json index 37c22b73..9218c8ab 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,42 @@ { "name": "django-comments-xtd", + "private": true, "version": "3.0.0", - "description": "Frontend side of django-comments-xtd", - "main": "index.js", + "description": "Test the frontend modules of django-comments-xtd", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build:pro": "NODE_ENV=production webpack --config webpack.config.js --progress", - "build:dev": "webpack --config webpack.config.js --progress", - "watch": "webpack serve --config webpack.config.js" + "pretest": "eslint django_comments_xtd/static/django_comments_xtd", + "watch": "NODE_ENV=development npx webpack --watch", + "build": "NODE_ENV=production npx webpack", + "test": "NODE_ENV=test jest" }, "repository": { "type": "git", "url": "git+https://github.com/danirus/django-comments-xtd.git" }, "keywords": [], - "author": "Daniel Rus Morales", - "license": "MIT", + "author": "Daniela Rus Morales", + "license": "BSD-2-CLAUSE", "bugs": { "url": "https://github.com/danirus/django-comments-xtd/issues" }, "homepage": "https://github.com/danirus/django-comments-xtd", "devDependencies": { - "@babel/core": "^7.13.1", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/preset-env": "^7.13.5", - "babel": "^6.23.0", - "babel-loader": "^8.2.2", - "clean-webpack-plugin": "^3.0.0", - "css-loader": "^5.0.2", - "css-minimizer-webpack-plugin": "^1.2.0", - "mini-css-extract-plugin": "^1.3.9", - "style-loader": "^2.0.0", - "url-loader": "^4.1.1", - "webpack": "^5.24.1", - "webpack-cli": "^4.5.0", - "webpack-dev-server": "^3.11.2", - "webpack-manifest-plugin": "^3.0.0" + "@babel/plugin-transform-runtime": "^7.16.10", + "@testing-library/dom": "^8.11.3", + "@testing-library/jest-dom": "^5.16.1", + "babel-jest": "^27.4.6", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "eslint": "^7.32.0", + "express": "^4.17.2", + "fake-fetch": "^2.3.0", + "jest": "^27.4.7", + "jest-dev-server": "^6.0.3", + "jest-fetch-mock": "^3.0.3", + "webpack-cli": "^4.9.2" + }, + "dependencies": { + "@babel/preset-env": "^7.16.11", + "babel-loader": "^8.2.3", + "clean-webpack-plugin": "^4.0.0" } } diff --git a/pyproject.toml b/pyproject.toml index 37fe5a0f..eaa45a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,4 @@ multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true -ensure_newline_before_comments = true \ No newline at end of file +ensure_newline_before_comments = true diff --git a/webpack.config.js b/webpack.config.js index 498864f9..c0c2c7e0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,81 +1,90 @@ +"use strict"; + const path = require('path'); const webpack = require('webpack'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const package_json = require("./package.json"); +const ver = package_json.version; const devMode = process.env.NODE_ENV !== 'production'; -const PRJ_PATH = path.resolve(__dirname, 'django_comments_xtd', - 'static', 'django_comments_xtd'); + +const PRJ_PATH = path.resolve( + __dirname, 'django_comments_xtd', 'static', 'django_comments_xtd' +); const plugins = [ - new webpack.ProgressPlugin(), - new CleanWebpackPlugin(), - new MiniCssExtractPlugin({filename: '[name]-[contenthash].css'}), - new WebpackManifestPlugin() + new webpack.ProgressPlugin(), + new CleanWebpackPlugin({ + cleanOnceBeforeBuildPatterns: [ + `!*${ver}*.js` + ] + }), ]; module.exports = { - mode: devMode ? 'development' : 'production', - context: __dirname, - devtool: 'source-map', - - entry: { - bundle: path.resolve(PRJ_PATH, "js/index.js") - }, + mode: devMode ? 'development' : 'production', + context: __dirname, + devtool: 'source-map', - output: { - path: path.resolve(PRJ_PATH, "dist"), - filename: devMode ? '[name]-[contenthash].js' : '[name]-3.0.0.js', - publicPath: '' - }, - - optimization: { - splitChunks: { - cacheGroups: { - styles: { - name: 'styles', - type: 'css/mini-extract', - chunks: 'all', - enforce: true - } - } + entry: { + dcx: path.resolve(PRJ_PATH, "js/index.js") }, - minimizer: [ - `...`, - new CssMinimizerPlugin() - ] - }, - plugins: plugins, + output: { + path: path.resolve(PRJ_PATH, "dist"), + filename: devMode ? `[name]-${ver}.js` : `[name]-${ver}.min.js` + }, - module: { - rules: [ - { - test: /\.(js|jsx)$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'] - } - } - }, - { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader' + optimization: { + minimize: !devMode, + minimizer: [ + new TerserPlugin({ + terserOptions: { + parse: { + ecma: 8 + }, + compress: { + ecma: 5, + warnings: false, + comparisons: false, + inline: 2 + }, + mangle: { + safari10: true + }, + keep_classnames: !devMode, + keep_fnames: !devMode, + output: { + ecma: 5, + comments: false, + ascii_only: true + } + } + }), ] - }, - { - test: /\.(svg|gif|png|eot|woff|ttf)$/, - use: ['url-loader'] - } - ] - } -} + }, + + plugins: plugins, + // module: { + // rules: [ + // { + // test: /\.js$/, + // exclude: /node_modules/, + // use: { + // loader: 'babel-loader', + // options: { + // presets: ['@babel/preset-env'] + // } + // } + // }, + // { + // test: /\.(svg|gif|png|eot|woff|ttf)$/, + // use: ['url-loader'] + // } + // ] + // } +};