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; + } + } +}