diff --git a/frontend/Makefile b/frontend/Makefile
index 5dfb413..f3b0da0 100644
--- a/frontend/Makefile
+++ b/frontend/Makefile
@@ -1,7 +1,7 @@
SHELL = bash
# list of JS files to be built
-JS_BUILD = jquery.js vue.js highlight.js chartist.js login.js signup.js dashboard.js logout.js
+JS_BUILD = jquery.js vue.js highlight.js chartist.js login.js signup.js dashboard.js logout.js commento.js
jquery.js = jquery.js
vue.js = vue.js
@@ -11,6 +11,7 @@ login.js = utils.js http.js auth-common.js login.js
signup.js = utils.js http.js auth-common.js signup.js
dashboard.js = utils.js http.js errors.js self.js dashboard.js dashboard-setting.js dashboard-domain.js dashboard-installation.js dashboard-general.js dashboard-moderation.js dashboard-statistics.js dashboard-import.js dashboard-danger.js
logout.js = utils.js logout.js
+commento.js = commento.js
# for each file in $(JS_BUILD), list its composition
diff --git a/frontend/js/commento.js b/frontend/js/commento.js
new file mode 100644
index 0000000..764c904
--- /dev/null
+++ b/frontend/js/commento.js
@@ -0,0 +1,975 @@
+(function(global, document) {
+ 'use strict';
+
+ // Do not use other files like utils.js and http.js in the Makefile to build
+ // commento.js for the following reasons:
+ // - We don't use jQuery in the actual JavaScript payload because we need
+ // to be lightweight.
+ // - They pollute the global/window namespace (with global.post, etc.).
+ // That's NOT fine when we expect them source our JavaScript. For example,
+ // the user may have their own window.post defined. We don't want to
+ // override that.
+
+
+ var origin = global.commento_origin;
+ var cdn = global.commento_cdn;
+ var root = null;
+ var isAuthenticated = false;
+ var comments = [];
+ var commenters = [];
+ var requireIdentification = true;
+ var requireModeration = true;
+ var isModerator = false;
+ var isFrozen = false;
+ var chosenAnonymous = false;
+ var shownSubmitButton = {"root": false};
+ var shownReply = {};
+
+
+ function $(id) {
+ return document.getElementById(id);
+ }
+
+
+ function dataGet(el, key) {
+ return el.dataset[key];
+ }
+
+
+ function dataSet(el, key, data) {
+ el.dataset[key] = data;
+ }
+
+
+ function append(root, el) {
+ root.appendChild(el);
+ }
+
+
+ function prepend(root, el) {
+ root.prepend(el);
+ }
+
+
+ function classAdd(el, cls) {
+ el.classList.add("commento-" + cls);
+ }
+
+
+ function classRemove(el, cls) {
+ el.classList.remove("commento-" + cls);
+ }
+
+
+ function create(el) {
+ return document.createElement(el);
+ }
+
+
+ function remove(el) {
+ el.parentNode.removeChild(el);
+ }
+
+
+ function attr(node, a, value) {
+ node.setAttribute(a, value);
+ }
+
+
+ function post(url, data, callback) {
+ var xmlDoc = new XMLHttpRequest();
+
+ xmlDoc.open("POST", url, true);
+ xmlDoc.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ xmlDoc.onload = function() {
+ callback(JSON.parse(xmlDoc.response));
+ };
+
+ xmlDoc.send(JSON.stringify(data));
+ }
+
+
+ function get(url, callback) {
+ var xmlDoc = new XMLHttpRequest();
+
+ xmlDoc.open('GET', url, true);
+ xmlDoc.onload = function() {
+ callback(JSON.parse(xmlDoc.response));
+ };
+
+ xmlDoc.send(null);
+ }
+
+
+ function call(callback) {
+ if (typeof(callback) == "function")
+ callback();
+ }
+
+
+ function rootSpinnerShow() {
+ var spinner = create("div");
+
+ classAdd(spinner, "loading");
+
+ append(root, spinner);
+ }
+
+
+ function cookieGet(name) {
+ var c = "; " + document.cookie;
+ var x = c.split("; " + name + "=");
+ if (x.length == 2)
+ return x.pop().split(";").shift();
+ }
+
+
+ function cookieSet(name, value) {
+ var expires = "";
+ var date = new Date();
+ date.setTime(date.getTime() + (365*24*60*60*1000));
+ expires = "; expires=" + date.toUTCString();
+
+ document.cookie = name + "=" + value + expires + "; path=/";
+ }
+
+ function sessionGet() {
+ var session = cookieGet("session");
+ if (session === undefined)
+ return "anonymous";
+
+ return session;
+ }
+
+ global.logout = function() {
+ cookieSet("session", "anonymous");
+ refreshAll();
+ }
+
+ function selfGet(callback) {
+ var session = sessionGet();
+ if (session == "anonymous") {
+ isAuthenticated = false;
+ call(callback);
+ return;
+ }
+
+ var json = {
+ session: sessionGet(),
+ };
+
+ post(origin + "/api/commenter/self", json, function(resp) {
+ console.log(resp);
+ if (!resp.success) {
+ cookieSet("session", "anonymous");
+ call(callback);
+ return;
+ }
+
+ var loggedContainer = create("div");
+ var loggedInAs = create("div");
+ var name = create("a");
+ var photo = create("img");
+ var logout = create("div");
+
+ classAdd(loggedContainer, "logged-container");
+ classAdd(loggedInAs, "logged-in-as");
+ classAdd(name, "name");
+ classAdd(photo, "photo");
+ classAdd(logout, "logout");
+
+ name.innerText = resp.commenter.name;
+ logout.innerText = "Logout";
+
+ attr(name, "href", resp.commenter.link);
+ if (resp.commenter.provider == "google") {
+ attr(photo, "src", resp.commenter.photo + "?sz=50");
+ } else {
+ attr(photo, "src", resp.commenter.photo);
+ }
+ attr(logout, "onclick", "logout()");
+
+ append(loggedInAs, photo);
+ append(loggedInAs, name);
+ append(loggedContainer, loggedInAs);
+ append(loggedContainer, logout);
+ append(root, loggedContainer);
+
+ isAuthenticated = true;
+
+ call(callback);
+ });
+ }
+
+ function cssLoad(file) {
+ var link = create("link");
+ var head = document.getElementsByTagName('head')[0];
+
+ link.type = "text/css";
+ attr(link, "href", file);
+ attr(link, "rel", "stylesheet");
+
+ append(head, link);
+ }
+
+ function jsLoad(file, ready) {
+ var script = document.createElement("script");
+ var loaded = false;
+
+ script.type = "application/javascript";
+ script.src = file;
+ script.async = true;
+ script.onreadysessionchange = script.onload = function() {
+ if (!loaded &&
+ (!this.readySession ||
+ this.readySession === "loaded" ||
+ this.readySession === "complete"))
+ {
+ ready();
+ }
+
+ loaded = true;
+ script.onload = script.onreadysessionchange = null;
+ };
+
+ append(document.body, script);
+ }
+
+ function footerLoad() {
+ var footer = create("div");
+ var aContainer = create("div");
+ var a = create("a");
+ var img = create("img");
+ var text = create("span");
+
+ classAdd(footer, "footer");
+ classAdd(aContainer, "logo-container");
+ classAdd(a, "logo");
+ classAdd(img, "logo-svg");
+ classAdd(text, "logo-text");
+
+ attr(a, "href", "https://commento.io");
+ attr(a, "target", "_blank");
+ attr(img, "src", cdn + "/images/logo.svg");
+
+ text.innerText = "Powered by Commento";
+
+ append(a, img);
+ append(a, text);
+ append(aContainer, a);
+ append(footer, aContainer);
+ append(root, footer);
+ }
+
+ function commentsGet(callback) {
+ var json = {
+ session: sessionGet(),
+ domain: location.host,
+ path: location.pathname,
+ };
+
+ post(origin + "/api/comment/list", json, function(resp) {
+ if (!resp.success) {
+ errorShow(resp.message);
+ return;
+ }
+
+ requireModeration = resp.requireModeration;
+ requireIdentification = resp.requireIdentification;
+ isModerator = resp.isModerator;
+ isFrozen = resp.isFrozen;
+ comments = resp.comments;
+ commenters = resp.commenters;
+
+ cssLoad(cdn + "/css/commento.css");
+
+ call(callback);
+ });
+ }
+
+ function errorShow(text) {
+ var el = $(ID_ERROR);
+
+ el.innerText = text;
+
+ attr(el, "style", "display: block;");
+ }
+
+ function createErrorElement() {
+ var el = create("div");
+
+ el.id = ID_ERROR;
+
+ classAdd(el, "error-box");
+ attr(el, "style", "display: none;");
+
+ append(root, el);
+ }
+
+ function autoExpander(el) {
+ return function() {
+ el.style.height = "";
+ el.style.height = Math.min(Math.max(el.scrollHeight, 75), 400) + "px";
+ }
+ };
+
+ function isMobile() {
+ var mobile = false;
+ if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
+ || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)))
+ mobile = true;
+
+ return mobile;
+ }
+
+ function textareaCreate(id) {
+ var textareaSuperContainer = create("div");
+ var textareaContainer = create("div");
+ var textarea = create("textarea");
+
+ textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
+ textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
+ textarea.id = ID_TEXTAREA + id;
+
+ classAdd(textareaContainer, "textarea-container");
+
+ if (!isAuthenticated && !chosenAnonymous) {
+ var buttonsContainer = create("div");
+
+ if (!isMobile()) {
+ classAdd(buttonsContainer, "buttons-container");
+ classAdd(textarea, "blurred-textarea");
+ } else {
+ classAdd(textarea, "hidden");
+ classAdd(buttonsContainer, "mobile-buttons-container");
+ classAdd(buttonsContainer, "opaque");
+ }
+
+ var oauths = ["google"];
+ if (!requireIdentification)
+ oauths.push("anonymous");
+
+ for (var i = 0; i < oauths.length; i++) {
+ var oauthButton = create("button");
+
+ classAdd(oauthButton, "button");
+ classAdd(oauthButton, oauths[i] + "-button");
+
+ if (isMobile())
+ classAdd(oauthButton, "opaque");
+
+ attr(oauthButton, "onclick", "commentoAuth('" + oauths[i] + "', '" + id + "')");
+
+ oauthButton.innerText = oauths[i][0].toUpperCase() + oauths[i].slice(1);
+
+ append(buttonsContainer, oauthButton);
+ }
+
+ attr(textarea, "disabled", true);
+
+ append(textareaContainer, buttonsContainer);
+ }
+
+ attr(textarea, "placeholder", "Join the discussion!");
+ attr(textarea, "onclick", "showSubmitButton('" + id + "')");
+
+ textarea.oninput = autoExpander(textarea);
+
+ append(textareaContainer, textarea);
+ append(textareaSuperContainer, textareaContainer);
+
+ return textareaSuperContainer;
+ }
+
+ function rootCreate(callback) {
+ var commentsArea = create("div");
+
+ commentsArea.id = ID_COMMENTS_AREA;
+
+ classAdd(commentsArea, "comments");
+
+ commentsArea.innerHTML = "";
+
+ append(root, textareaCreate("root"));
+ append(root, commentsArea);
+
+ call(callback);
+ }
+
+ function messageCreate(text) {
+ var msg = create("div");
+
+ classAdd(msg, "moderation-notice");
+
+ msg.innerText = text;
+
+ return msg;
+ }
+
+ global.postComment = function(id) {
+ var textarea = $(ID_TEXTAREA + id);
+
+ var comment = textarea.value;
+
+ if (comment == "") {
+ classAdd(textarea, "red-border");
+ return;
+ }
+ else {
+ classRemove(textarea, "red-border");
+ }
+
+ var json = {
+ "session": sessionGet(),
+ "domain": location.host,
+ "path": location.pathname,
+ "parentHex": id,
+ "markdown": comment,
+ };
+
+ post(origin + "/api/comment/new", json, function(resp) {
+ if (!resp.success) {
+ errorShow(resp.message);
+ return;
+ }
+
+ $(ID_TEXTAREA + id).value = "";
+
+ commentsGet(function() {
+ $(ID_COMMENTS_AREA).innerHTML = "";
+ commentsRender();
+
+ if (requireModeration && !isModerator) {
+ if (id == "root") {
+ var body = $(ID_SUPER_CONTAINER + id);
+ prepend(body, messageCreate("Your comment is under moderation."));
+ } else {
+ var body = $(ID_BODY + id);
+ append(body, messageCreate("Your comment is under moderation."));
+ }
+ }
+ });
+ });
+ }
+
+ function colorGet(name) {
+ var colors = [
+ // some visually distincy
+ "#35b2de", // some kind of teal/cyan
+ "#62cd0a", // fresh lemon green
+ "#383838", // shade of gray
+ "#e4a90f", // comfy yellow
+ "#f80707", // sharp red
+ "#f0479c", // bright pink
+ ];
+
+ var total = 0;
+ for (var i = 0; i < name.length; i++)
+ total += name.charCodeAt(i);
+ var color = colors[total % colors.length];
+
+ return color;
+ }
+
+ function timeDifference(current, previous) { // thanks stackoverflow
+ var msJustNow = 5000;
+ var msPerMinute = 60000;
+ var msPerHour = 3600000;
+ var msPerDay = 86400000;
+ var msPerMonth = 2592000000;
+ var msPerYear = 946080000000;
+
+ var elapsed = current - previous;
+
+ if (elapsed < msJustNow) {
+ return 'just now';
+ }
+ else if (elapsed < msPerMinute) {
+ return Math.round(elapsed/1000) + ' seconds ago';
+ }
+ else if (elapsed < msPerHour) {
+ return Math.round(elapsed/msPerMinute) + ' minutes ago';
+ }
+ else if (elapsed < msPerDay ) {
+ return Math.round(elapsed/msPerHour ) + ' hours ago';
+ }
+ else if (elapsed < msPerMonth) {
+ return Math.round(elapsed/msPerDay) + ' days ago';
+ }
+ else if (elapsed < msPerYear) {
+ return Math.round(elapsed/msPerMonth) + ' months ago';
+ }
+ else {
+ return Math.round(elapsed/msPerYear ) + ' years ago';
+ }
+ }
+
+ function scorify(score) {
+ if (score != 1)
+ return score + " points";
+ else
+ return score + " point";
+ }
+
+ var ID_ERROR = "commento-error";
+ var ID_COMMENTS_AREA = "commento-comments-area";
+ var ID_SUPER_CONTAINER = "commento-textarea-super-container-";
+ var ID_TEXTAREA_CONTAINER = "commento-textarea-container-";
+ var ID_TEXTAREA = "commento-textarea-";
+ var ID_CARD = "commento-comment-card-";
+ var ID_BODY = "commento-comment-body-";
+ var ID_SUBTITLE = "commento-comment-subtitle-";
+ var ID_SCORE = "commento-comment-score-";
+ var ID_OPTIONS = "commento-comment-options-";
+ var ID_EDIT = "commento-comment-edit-";
+ var ID_REPLY = "commento-comment-reply-";
+ var ID_COLLAPSE = "commento-comment-collapse-";
+ var ID_UPVOTE = "commento-comment-upvote-";
+ var ID_DOWNVOTE = "commento-comment-downvote-";
+ var ID_APPROVE = "commento-comment-approve-";
+ var ID_REMOVE = "commento-comment-remove-";
+ var ID_CONTENTS = "commento-comment-contents-";
+ var ID_SUBMIT_BUTTON = "commento-submit-button-";
+
+ function commentsRecurse(parentMap, parentHex) {
+ var cur = parentMap[parentHex];
+ if (!cur || !cur.length) {
+ return null;
+ }
+
+ var cards = create("div");
+ cur.forEach(function(comment) {
+ var commenter = commenters[comment.commenterHex];
+ var avatar;
+ var card = create("div");
+ var header = create("div");
+ var subtitle = create("div");
+ var score = create("div");
+ var body = create("div");
+ var options = create("div");
+ var edit = create("button");
+ var reply = create("button");
+ var collapse = create("button");
+ var upvote = create("div");
+ var downvote = create("div");
+ var approve = create("button");
+ var remove = create("button");
+ var children = commentsRecurse(parentMap, comment.commentHex);
+ var contents = create("div");
+ var color = colorGet(commenter.name);
+ var name;
+ if (commenter.link != "undefined")
+ name = create("a");
+ else
+ name = create("div");
+
+ card.id = ID_CARD + comment.commentHex;
+ body.id = ID_BODY + comment.commentHex;
+ subtitle.id = ID_SUBTITLE + comment.commentHex;
+ score.id = ID_SCORE + comment.commentHex;
+ options.id = ID_OPTIONS + comment.commentHex;
+ edit.id = ID_EDIT + comment.commentHex;
+ reply.id = ID_REPLY + comment.commentHex;
+ collapse.id = ID_COLLAPSE + comment.commentHex;
+ upvote.id = ID_UPVOTE + comment.commentHex;
+ downvote.id = ID_DOWNVOTE + comment.commentHex;
+ approve.id = ID_APPROVE + comment.commentHex;
+ remove.id = ID_REMOVE + comment.commentHex;
+ contents.id = ID_CONTENTS + comment.commentHex;
+
+ collapse.title = "Collapse";
+ upvote.title = "Upvote";
+ downvote.title = "Downvote";
+ edit.title = "Edit";
+ reply.title = "Reply";
+ approve.title = "Approve";
+ remove.title = "Remove";
+
+ card.style["borderLeft"] = "2px solid " + color;
+ name.innerText = commenter.name;
+ body.innerHTML = comment.html;
+ subtitle.innerHTML = timeDifference((new Date()).getTime(), Date.parse(comment.creationDate));
+ score.innerText = scorify(comment.score);
+
+ if (commenter.photo == "undefined") {
+ avatar = create("div");
+ avatar.style["background"] = color;
+ avatar.style["boxShadow"] = "0px 0px 0px 2px " + color;
+ avatar.innerHTML = commenter.name[0].toUpperCase();
+ classAdd(avatar, "avatar");
+ } else {
+ avatar = create("img");
+ if (commenter.provider == "google") {
+ attr(avatar, "src", commenter.photo + "?sz=50");
+ } else {
+ attr(avatar, "src", commenter.photo);
+ }
+ classAdd(avatar, "avatar-img");
+ }
+
+ classAdd(card, "card");
+ if (isModerator && comment.state == "unapproved")
+ classAdd(card, "dark-card");
+ classAdd(header, "header");
+ classAdd(name, "name");
+ classAdd(subtitle, "subtitle");
+ classAdd(score, "score");
+ classAdd(body, "body");
+ classAdd(options, "options");
+ classAdd(edit, "option-button");
+ classAdd(edit, "option-edit");
+ classAdd(reply, "option-button");
+ classAdd(reply, "option-reply");
+ classAdd(collapse, "option-button");
+ classAdd(collapse, "option-collapse");
+ classAdd(upvote, "option-button");
+ classAdd(upvote, "option-upvote");
+ classAdd(downvote, "option-button");
+ classAdd(downvote, "option-downvote");
+ classAdd(approve, "option-button");
+ classAdd(approve, "option-approve");
+ classAdd(remove, "option-button");
+ classAdd(remove, "option-remove");
+
+ if (isAuthenticated) {
+ if (comment.direction > 0)
+ classAdd(upvote, "upvoted");
+ else if (comment.direction < 0)
+ classAdd(downvote, "downvoted");
+ }
+
+ attr(edit, "onclick", "startEdit('" + comment.commentHex + "')");
+ attr(reply, "onclick", "replyShow('" + comment.commentHex + "')");
+ attr(collapse, "onclick", "commentCollapse('" + comment.commentHex + "')");
+ attr(approve, "onclick", "commentApprove('" + comment.commentHex + "')");
+ attr(remove, "onclick", "commentDelete('" + comment.commentHex + "')");
+
+ if (isAuthenticated) {
+ if (comment.direction > 0) {
+ attr(upvote, "onclick", "vote('" + comment.commentHex + "', 1, 0)");
+ attr(downvote, "onclick", "vote('" + comment.commentHex + "', 1, -1)");
+ }
+ else if (comment.direction < 0) {
+ attr(upvote, "onclick", "vote('" + comment.commentHex + "', -1, 1)");
+ attr(downvote, "onclick", "vote('" + comment.commentHex + "', -1, 0)");
+ }
+ else {
+ attr(upvote, "onclick", "vote('" + comment.commentHex + "', 0, 1)");
+ attr(downvote, "onclick", "vote('" + comment.commentHex + "', 0, -1)");
+ }
+ } else if (!chosenAnonymous) {
+ attr(upvote, "onclick", "replyShow('" + comment.commentHex + "')");
+ }
+
+ if (isAuthenticated) {
+ if (isModerator) {
+ if (comment.state == "unapproved")
+ attr(options, "style", "width: 192px;");
+ else
+ attr(options, "style", "width: 160px;");
+ }
+ else
+ attr(options, "style", "width: 128px;");
+ }
+ else
+ attr(options, "style", "width: 32px;");
+
+ if (commenter.link != "undefined")
+ attr(name, "href", commenter.link);
+
+ if (false) // replace when edit is implemented
+ append(options, edit);
+
+ if (isAuthenticated) {
+ append(options, upvote);
+ append(options, downvote);
+ append(options, reply);
+ }
+
+ if (isModerator) {
+ if (comment.state == "unapproved")
+ append(options, approve);
+ append(options, remove);
+ }
+
+ append(options, collapse);
+ append(subtitle, score);
+ append(header, options);
+ append(header, avatar);
+ append(header, name);
+ append(header, subtitle);
+ append(contents, body);
+
+ if (children) {
+ classAdd(children, "body");
+ append(contents, children);
+ }
+
+ append(card, header);
+ append(card, contents);
+ append(cards, card);
+
+ shownSubmitButton[comment.commentHex] = false;
+ });
+
+ return cards;
+ }
+
+
+ global.commentApprove = function(commentHex) {
+ var json = {
+ "session": sessionGet(),
+ "commentHex": commentHex,
+ }
+
+ post(origin + "/api/comment/approve", json, function(resp) {
+ if (!resp.success) {
+ errorShow(resp.message);
+ return
+ }
+
+ var card = $(ID_CARD + commentHex);
+ var options = $(ID_OPTIONS + commentHex);
+ var tick = $(ID_APPROVE + commentHex);
+
+ classRemove(card, "dark-card");
+ attr(options, "style", "width: 160px;");
+ remove(tick);
+ });
+ }
+
+
+ global.commentDelete = function(commentHex) {
+ var json = {
+ "session": sessionGet(),
+ "commentHex": commentHex,
+ }
+
+ post(origin + "/api/comment/delete", json, function(resp) {
+ if (!resp.success) {
+ errorShow(resp.message);
+ return
+ }
+
+ var card = $(ID_CARD + commentHex);
+ remove(card);
+ });
+ }
+
+
+ function nameWidthFix() {
+ var els = document.getElementsByClassName("commento-name");
+
+ for (var i = 0; i < els.length; i++)
+ attr(els[i], "style", "max-width: " + (els[i].getBoundingClientRect()["width"] + 20) + "px;")
+ }
+
+
+ global.vote = function(commentHex, oldVote, direction) {
+ var upvote = $(ID_UPVOTE + commentHex);
+ var downvote = $(ID_DOWNVOTE + commentHex);
+ var score = $(ID_SCORE + commentHex);
+
+ var json = {
+ "session": sessionGet(),
+ "commentHex": commentHex,
+ "direction": direction,
+ };
+
+ if (direction > 0) {
+ attr(upvote, "onclick", "vote('" + commentHex + "', 1, 0)");
+ attr(downvote, "onclick", "vote('" + commentHex + "', 1, -1)");
+ }
+ else if (direction < 0) {
+ attr(upvote, "onclick", "vote('" + commentHex + "', -1, 1)");
+ attr(downvote, "onclick", "vote('" + commentHex + "', -1, 0)");
+ }
+ else {
+ attr(upvote, "onclick", "vote('" + commentHex + "', 0, 1)");
+ attr(downvote, "onclick", "vote('" + commentHex + "', 0, -1)");
+ }
+
+ classRemove(upvote, "upvoted");
+ classRemove(downvote, "downvoted");
+ if (direction > 0)
+ classAdd(upvote, "upvoted");
+ else if (direction < 0)
+ classAdd(downvote, "downvoted");
+
+ score.innerText = scorify(parseInt(score.innerText.replace(/[^\d-.]/g, "")) + direction - oldVote);
+
+ post(origin + "/api/comment/vote", json, function(resp) {});
+ }
+
+ global.replyShow = function(id) {
+ if (id in shownReply && shownReply[id])
+ return;
+
+ var body = $(ID_BODY + id);
+ append(body, textareaCreate(id));
+ shownReply[id] = true;
+
+ var replyButton = $(ID_REPLY + id);
+
+ classRemove(replyButton, "option-reply");
+ classAdd(replyButton, "option-cancel");
+
+ replyButton.title = "Cancel reply";
+
+ attr(replyButton, "onclick", "replyCollapse('" + id + "')")
+ };
+
+ global.replyCollapse = function(id) {
+ var replyButton = $(ID_REPLY + id);
+ var el = $(ID_SUPER_CONTAINER + id);
+
+ el.remove();
+ shownReply[id] = false;
+ shownSubmitButton[id] = false;
+
+ classAdd(replyButton, "option-reply");
+ classRemove(replyButton, "option-cancel");
+
+ replyButton.title = "Reply to this comment";
+
+ attr(replyButton, "onclick", "replyShow('" + id + "')")
+ }
+
+ global.commentCollapse = function(id) {
+ var contents = $(ID_CONTENTS + id);
+ var button = $(ID_COLLAPSE + id);
+
+ classAdd(contents, "hidden");
+
+ classRemove(button, "option-collapse");
+ classAdd(button, "option-uncollapse");
+
+ button.title = "Expand";
+
+ attr(button, "onclick", "commentUncollapse('" + id + "')");
+ }
+
+ global.commentUncollapse = function(id) {
+ var contents = $(ID_CONTENTS + id);
+ var button = $(ID_COLLAPSE + id);
+
+ classRemove(contents, "hidden");
+
+ classRemove(button, "option-uncollapse");
+ classAdd(button, "option-collapse");
+
+ button.title = "Collapse";
+
+ attr(button, "onclick", "commentCollapse('" + id + "')");
+ }
+
+ function commentsRender() {
+ var parentMap = {};
+ var parentHex;
+
+ var commentsArea = $(ID_COMMENTS_AREA);
+
+ comments.forEach(function(comment) {
+ parentHex = comment.parentHex;
+ if (!(parentHex in parentMap)) {
+ parentMap[parentHex] = [];
+ }
+ parentMap[parentHex].push(comment);
+ });
+
+ var cards = commentsRecurse(parentMap, "root");
+ if (cards) {
+ append(commentsArea, cards);
+ }
+ }
+
+ global.showSubmitButton = function(id) {
+ if (id in shownSubmitButton && shownSubmitButton[id])
+ return;
+
+ shownSubmitButton[id] = true;
+
+ var el = $(ID_SUPER_CONTAINER + id);
+
+ var submit = create("button");
+
+ submit.id = ID_SUBMIT_BUTTON + id;
+
+ submit.innerText = "Add Comment";
+
+ classAdd(submit, "button");
+ classAdd(submit, "submit-button");
+ classAdd(el, "button-margin");
+
+ attr(submit, "onclick", "postComment('" + id + "')");
+
+ append(el, submit);
+ }
+
+ global.commentoAuth = function(provider, id) {
+ if (provider == "anonymous") {
+ cookieSet("session", "anonymous");
+ chosenAnonymous = true;
+ refreshAll(function() {
+ if (id != "root")
+ global.replyShow(id);
+ $(ID_TEXTAREA + id).click();
+ $(ID_TEXTAREA + id).focus();
+ });
+ return;
+ }
+
+ var popup = window.open("", "_blank");
+
+ get(origin + "/api/commenter/session/new", function(resp) {
+ if (!resp.success) {
+ errorShow(resp.message);
+ return;
+ }
+
+ cookieSet("session", resp.session);
+
+ popup.location = origin + "/api/oauth/" + provider + "/redirect?session=" + resp.session;
+
+ var interval = setInterval(function() {
+ if (popup.closed) {
+ refreshAll(function() {
+ if (id != "root")
+ global.replyShow(id);
+ $(ID_TEXTAREA + id).click();
+ $(ID_TEXTAREA + id).focus();
+ });
+ clearInterval(interval);
+ }
+ }, 250);
+ });
+ }
+
+ function refreshAll(callback) {
+ $("commento").innerHTML = "";
+ shownSubmitButton = {"root": false};
+ shownReply = {};
+ main(callback);
+ }
+
+ function main(callback) {
+ root = $("commento");
+
+ createErrorElement();
+
+ selfGet(function() {
+ commentsGet(function() {
+ rootCreate(function() {
+ commentsRender();
+ nameWidthFix();
+ footerLoad();
+ attr(root, "style", "");
+ call(callback);
+ });
+ });
+ });
+ }
+
+ document.addEventListener("DOMContentLoaded", main);
+
+}(window, document));
diff --git a/frontend/sass/commento-buttons.scss b/frontend/sass/commento-buttons.scss
new file mode 100644
index 0000000..8dea5b1
--- /dev/null
+++ b/frontend/sass/commento-buttons.scss
@@ -0,0 +1,99 @@
+@import "colors-main.scss";
+
+@mixin mask-image($image) {
+ -webkit-mask-image: url($image);
+ mask-image: url($image);
+}
+
+.commento-option-button {
+ border: none;
+ cursor: pointer;
+ outline: none;
+ padding: 0px;
+ position: absolute;
+ top: 0px;
+ z-index: 3;
+ background: $gray-4;
+}
+
+.commento-option-reply {
+ height: 20px;
+ width: 20px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 9px 3px 9px 3px;
+ right: 32px;
+}
+
+.commento-option-cancel {
+ height: 13px;
+ width: 13px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ right: 32px;
+ background: $gray-5;
+}
+
+.commento-option-collapse {
+ height: 14px;
+ width: 14px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ right: 0px;
+ background: $gray-7;
+}
+
+.commento-option-uncollapse {
+ height: 14px;
+ width: 14px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ right: 0px;
+ background: $gray-7;
+}
+
+.commento-option-upvote,
+.commento-option-downvote {
+ height: 14px;
+ width: 14px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+}
+
+.commento-option-upvote {
+ right: 96px;
+}
+
+.commento-option-downvote {
+ transform: rotate(180deg);
+ right: 64px;
+}
+
+.commento-upvoted {
+ background: $orange-7;
+}
+
+.commento-downvoted {
+ background: $indigo-6;
+}
+
+.commento-option-remove {
+ height: 14px;
+ width: 14px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ right: 128px;
+ background: $red-8;
+}
+
+.commento-option-approve {
+ height: 14px;
+ width: 14px;
+ @include mask-image('data:image/svg+xml;utf8,');
+ margin: 12px 6px 12px 6px;
+ right: 160px;
+ background: $green-7;
+}
+
+.commento-option-button:focus {
+ outline: none;
+}
diff --git a/frontend/sass/commento-input.scss b/frontend/sass/commento-input.scss
new file mode 100644
index 0000000..14b910d
--- /dev/null
+++ b/frontend/sass/commento-input.scss
@@ -0,0 +1,187 @@
+@import "colors-main.scss";
+
+textarea,
+input[type=text] {
+ background: #ffffff;
+ border: 1px solid rgba(50, 50, 93, .1);
+ border-radius: 3px;
+ color: #525f7f;
+}
+
+input[type=text]::placeholder {
+ color: #cacaca;
+}
+
+textarea::placeholder {
+ color: #aaa;
+ font-size: 22px;
+ padding-top: 13px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+textarea {
+ display: inline-block;
+ font-family: "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
+ padding: 8px;
+ outline: none;
+ overflow: auto;
+ min-height: 75px;
+ width: 100%;
+}
+
+.commento-red-border {
+ border: 1px solid $red-7;
+}
+
+.commento-textarea-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ .commento-button, .commento-buttons-container::before {
+ opacity: 1;
+ transform: translate(0px,-3px);
+ }
+
+ .commento-submit-button {
+ transform: none;
+ }
+
+ .commento-blurred-textarea {
+ opacity: .7;
+ transform: scale(.95);
+ filter: blur(4px);
+ }
+
+ @media only screen and (max-width: 550px) {
+ .commento-buttons-container::before {
+ display: none;
+ }
+ }
+ }
+}
+
+.commento-buttons-container {
+ display: inline-block;
+ position: absolute;
+ z-index: 1;
+ opacity: 1;
+}
+
+.commento-mobile-buttons-container::before,
+.commento-buttons-container::before {
+ content: "Authenticate with";
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ line-height: 24px;
+ font-size: 14px;
+ padding: 6px;
+ color: $gray-8;
+ transition: all 0.3s;
+ opacity: 0;
+ outline: none;
+}
+
+.commento-mobile-buttons-container::before {
+ content: "To join the discussion, authenticate with";;
+ display: block;
+ text-align: center;
+}
+
+@media only screen and (max-width: 550px) {
+ .commento-buttons-container::before {
+ display: none;
+ }
+}
+
+.commento-button {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ cursor: pointer;
+ font-weight: bold;
+ line-height: 24px;
+ font-size: 14px;
+ padding: 6px;
+ padding-left: 8px;
+ padding-right: 8px;
+ box-shadow: 0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);
+ border: 1px solid transparent;
+ border-radius: 3px;
+ color: #fff;
+ width: 100px;
+ margin-left: 5px;
+ margin-left: 5px;
+ opacity: 0;
+}
+
+.commento-opaque {
+ opacity: 1;
+}
+
+.commento-opaque::before {
+ opacity: 1;
+}
+
+.commento-google-button {
+ transition: all 0.3s;
+ background: #dd4b39;
+}
+
+.commento-github-button {
+ transition: all 0.3s;
+ background: #000000;
+}
+
+.commento-anonymous-button {
+ transition: all 0.3s;
+ background: #096fa6;
+}
+
+.commento-blurred-textarea {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ justify-content: space-between;
+ transition: 0.3s all;
+ will-change: transform;
+}
+
+.commento-approve-button,
+.commento-delete-button,
+.commento-submit-button {
+ margin-top: 10px;
+ opacity: 1;
+ font-size: 14px;
+ width: -moz-fit-content;
+ width: -webkit-fit-content;
+ width: -ms-fit-content;
+ width: -o-fit-content;
+ width: fit-content;
+}
+
+.commento-submit-button {
+ float: right;
+ background: $indigo-7;
+}
+
+.commento-approve-button {
+ background: $green-7;
+}
+
+.commento-delete-button {
+ background: $red-7;
+}
+
+.commento-button-margin {
+ padding-bottom: 60px;
+}
diff --git a/frontend/sass/commento-logged.scss b/frontend/sass/commento-logged.scss
new file mode 100644
index 0000000..0e022cf
--- /dev/null
+++ b/frontend/sass/commento-logged.scss
@@ -0,0 +1,37 @@
+@import "colors-main.scss";
+
+.commento-logged-container {
+ width: 100%;
+ text-align: left;
+ margin-bottom: 16px;
+ position: relative;
+
+ .commento-logout {
+ cursor: pointer;
+ position: absolute;
+ top: 6px;
+ right: 16px;
+ color: $gray-5;
+ }
+
+ .commento-logged-in-as {
+ position: relative;
+
+ .commento-name {
+ color: $gray-8;
+ border: none;
+ font-weight: bold;
+ position: absolute;
+ top: 6px;
+ left: 64px;
+ }
+
+ .commento-photo {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ margin-left: 16px;
+ margin-right: 16px;
+ }
+ }
+}
diff --git a/frontend/sass/commento-logo.scss b/frontend/sass/commento-logo.scss
new file mode 100644
index 0000000..7a4a063
--- /dev/null
+++ b/frontend/sass/commento-logo.scss
@@ -0,0 +1,38 @@
+@import "colors-main.scss";
+
+.commento-footer {
+ margin: 36px 0px 12px 0px;
+ border-top: 1px solid $gray-1;
+ padding-right: 12px;
+
+ .commento-logo-container {
+ float: right;
+
+ .commento-logo {
+ border: none;
+ width: auto;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ padding: 5px;
+ border-radius: 3px;
+
+ .commento-logo-svg {
+ display: inline;
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+ outline: none;
+ }
+ }
+
+ .commento-logo-text {
+ font-size: 13px;
+ color: $gray-6;
+ display: inline;
+ line-height: 24px;
+ position: relative;
+ font-weight: bold;
+ }
+ }
+}
diff --git a/frontend/sass/commento-tags.scss b/frontend/sass/commento-tags.scss
new file mode 100644
index 0000000..cd9654d
--- /dev/null
+++ b/frontend/sass/commento-tags.scss
@@ -0,0 +1,20 @@
+@import "colors-main.scss";
+
+code {
+ background: $red-3;
+ font-family: monospace;
+ line-height: 1.5;
+ color: $red-6;
+ padding: 4px;
+}
+
+a {
+ color: #1f89ff;
+ border-bottom: 1px solid #1f89ff;
+ outline: none;
+ text-decoration: none;
+}
+
+a:focus {
+ box-shadow: 0 0 0 1px rgba(87, 85, 217, .2);
+}
diff --git a/frontend/sass/commento.scss b/frontend/sass/commento.scss
new file mode 100644
index 0000000..49916e9
--- /dev/null
+++ b/frontend/sass/commento.scss
@@ -0,0 +1,134 @@
+#commento {
+ font-family: "Source Sans Pro", "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: #50596c;
+ overflow-x: hidden;
+ text-rendering: optimizeLegibility;
+ padding: 8px;
+
+ @import "colors-main.scss";
+ @import "common-main.scss";
+
+ @import "commento-tags.scss";
+ @import "commento-logo.scss";
+ @import "commento-input.scss";
+ @import "commento-logged.scss";
+ @import "commento-buttons.scss";
+
+ .commento-hidden {
+ display: none;
+ }
+
+ .commento-error-box {
+ width: 100%;
+ border-radius: 4px;
+ height: 32px;
+ text-align: center;
+ color: $red-7;
+ font-weight: bold;
+ }
+
+ .commento-moderation-notice {
+ width: 100%;
+ border-radius: 4px;
+ height: 32px;
+ text-align: center;
+ color: $orange-7;
+ font-weight: bold;
+ margin-top: 16px;
+ }
+
+ .commento-dark-card {
+ background: $blue-1;
+ }
+
+ .commento-card {
+ padding: 12px 0px 0px 12px;
+ margin-top: 16px;
+ border-top: 1px solid #f0f0f0;
+
+ .commento-header {
+ padding-bottom: 12px;
+ }
+
+ .commento-avatar {
+ width: 34px;
+ height: 34px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ font-size: 22px;
+ float: left;
+ margin-right: 10px;
+ border: 1px solid #fff;
+ box-shadow: 0px 0px 0px 2px #f00;
+ }
+
+ .commento-avatar-img {
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ float: left;
+ margin-right: 10px;
+ }
+
+ .commento-avatar::after {
+ content:"";
+ display:block;
+ }
+
+ .commento-name {
+ font-weight: bold;
+ font-size: 14px;
+ color: #555;
+ border: none;
+ display: block;
+ z-index: 1;
+ margin-left: 48px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .commento-subtitle {
+ display: block;
+ color: #999;
+ font-size: 12px;
+ margin-left: 48px;
+ }
+
+ .commento-score {
+ display: inline;
+ color: #999;
+ font-size: 12px;
+ }
+
+ .commento-score::before {
+ content: "\00a0 \00a0 \00b7 \00a0 \00a0";
+ }
+
+ .commento-body {
+ p {
+ margin-top: 6px;
+ margin-bottom: 6px;
+ }
+ }
+
+ .commento-options {
+ float: right;
+ position: relative;
+ height: 38px;
+ z-index: 2;
+ }
+
+ .commento-moderation {
+ height: 48px;
+ }
+ }
+}