tilde.news/app/assets/javascripts/application.js.erb

693 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//= require jquery
//= require jquery_ujs
//= require_tree .
"use strict";
var _Lobsters = Class.extend({
curUser: null,
storyFlagReasons: { <%= Vote::STORY_REASONS.map{|k,v|
"#{k.inspect}: #{v.inspect}" }.join(", ") %> },
commentFlagReasons: { <%= Vote::COMMENT_REASONS.map{|k,v|
"#{k.inspect}: #{v.inspect}" }.join(", ") %> },
upvoteStory: function(voterEl) {
Lobster.vote("story", voterEl, 1);
},
flagStory: function(voterEl) {
Lobsters._showFlagWhyAt("story", voterEl, function(k) {
Lobster.vote("story", voterEl, -1, k); });
},
upvoteComment: function(voterEl) {
Lobster.vote("comment", voterEl, 1);
},
flagComment: function(voterEl) {
Lobsters._showFlagWhyAt("comment", voterEl, function(k) {
Lobster.vote("comment", voterEl, -1, k); });
},
_showFlagWhyAt: function(thingType, voterEl, onChooseWhy) {
if (!Lobsters.curUser)
return Lobster.bounceToLogin();
var li = $(voterEl).closest(".story, .comment");
if (li.hasClass("flagged")) {
/* already upvoted, neutralize */
Lobster.vote(thingType, voterEl, -1, null);
return;
}
if ($("#flag_why"))
$("#flag_why").remove();
if ($("#flag_why_shadow"))
$("#flag_why_shadow").remove();
var sh = $("<div id=\"flag_why_shadow\"></div>");
$(voterEl).after(sh);
sh.click(function() {
$("#flag_why_shadow").remove();
$("#flag_why").remove();
});
var d = $("<div id=\"flag_why\"></div>");
var reasons;
if (thingType == "comment")
reasons = Lobsters.commentFlagReasons;
else
reasons = Lobsters.storyFlagReasons;
$.each(reasons, function(k, v) {
var a = $("<a href=\"#\"" + (k == "" ? " class=\"cancelreason\"" : "") +
">" + v + "</a>");
a.click(function() {
$("#flag_why").remove();
$("#flag_why_shadow").remove();
if (k != "")
onChooseWhy(k);
return false;
});
d.append(a);
});
if (thingType == "story") {
$(voterEl).closest("li").after(d);
d.position({
my: "left top",
at: "left bottom",
offset: "-2 -2",
of: $(voterEl),
collision: "none",
});
d.css("left", $(voterEl).position().left);
} else {
// place flag menu outside of the comment to avoid inheriting opacity
var voterPos = $(voterEl).position();
d.appendTo($(voterEl).parent());
d.css({
left: voterPos.left,
top: voterPos.top + $(voterEl).outerHeight()
});
}
},
previewStory: function(form) {
$("#inside").load("/stories/preview", $(form).serializeArray(),
function() {
Lobsters.runSelect2();
});
},
runSelect2: function() {
$("#story_tags_a").select2({
formatSelection: function(what) {
return what.id;
},
matcher: function(term, text) {
return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
},
sortResults: function(results, container, query) {
if (query.term) {
var tmatches = [];
var dmatches = [];
/* prefer tag matches first, then description matches */
for (var x in results) {
var r = results[x];
if (r.id.toUpperCase().indexOf(query.term.toUpperCase()) == 0)
tmatches.push(r);
else
dmatches.push(r);
}
tmatches = tmatches.sort(function(a, b) {
return a.text.localeCompare(b.text);
});
dmatches = dmatches.sort(function(a, b) {
return a.text.localeCompare(b.text);
});
return tmatches.concat(dmatches);
}
return results;
}
});
},
fetchURLTitle: function(button, url_field, title_field) {
if (url_field.val() == "")
return;
var old_value = button.val();
button.prop("disabled", true);
button.val("Fetching...");
$.post("/stories/fetch_url_attributes", {
fetch_url: url_field.val(),
})
.success(function(data) {
if (data) {
if (data.title)
title_field.val(data.title.substr(0, title_field.maxLength));
if (data.url)
url_field.val(data.url);
}
button.val(old_value);
button.prop("disabled", false);
})
.error(function() {
button.val(old_value);
button.prop("disabled", false);
});
Lobster.checkStoryTitle();
},
});
var Lobsters = new _Lobsters();
$(document).ready(function() {
$(document).on("click", "a.comment_replier", function() {
if (!Lobsters.curUser) {
Lobster.bounceToLogin();
return false;
}
var comment = $(this).closest(".comment");
if ($("#reply_form_" + comment.attr("id")).length > 0)
return false;
var sel = "";
if (window.getSelection)
sel = window.getSelection().toString();
else if (document.selection && document.selection.type != "Control")
sel = document.selection.createRange().text;
if (sel != "") {
var t = "";
$.each(sel.split("\n"), function(k, v) {
t += (t == "" ? "" : "\n") + "> " + v;
});
sel = t;
if (sel != "")
sel += "\n\n";
}
var replies = comment.nextAll(".comments").first();
$.get("/comments/" + comment.attr("data-shortid") + "/reply",
function(data) {
var reply = $($.parseHTML(data));
reply.attr("id", "reply_form_" + comment.attr("id"));
replies.prepend(reply);
var ta = reply.find("textarea");
ta.focus().text(sel);
autosize(ta);
});
return false;
});
$(document).on("click", "a.comment_disownor", function() {
if (confirm("Are you sure you want to disown this comment?")) {
var li = $(this).closest(".comment");
$.post("/comments/" + $(li).attr("data-shortid") + "/disown",
function(d) {
$(li).replaceWith(d);
});
}
});
Lobsters.runSelect2();
$(document).on("blur", "#story_url", function() {
var url_tags = {
"\.pdf$": "pdf",
"[\/\.]((youtube|vimeo)\.com|youtu\.be|twitch.tv)\/": "video",
"[\/\.](slideshare\.net|speakerdeck\.com)\/": "slides",
"[\/\.](soundcloud\.com)\/": "audio",
};
for (var u in url_tags) {
var tag = url_tags[u];
if ($("#story_url").val().match(new RegExp(u, "i"))) {
var ta = $("#story_tags_a").data("select2");
if (ta.getVal().indexOf(tag) < 0)
ta.addSelectedChoice({ id: tag });
}
}
// check for dupe if there's a URL, but not when editing existing
if ($("#story_url").val().length > 0 &&
$("#edit_story input[name=_method]").val() !== "put") {
Lobster.checkStoryDuplicate(parentSelector(document.getElementById('story_url'), 'form'));
}
});
});
const parentSelector = (target, selector) => {
let parent = target;
while (!parent.matches(selector)) {
parent = parent.parentElement;
if (parent === null) {
throw new Error(`Did not match a parent of ${target} with the selector ${selector}`);
}
}
return parent;
};
function on(eventTypes, selector, callback) {
eventTypes.split(/ /).forEach( (eventType) => {
document.addEventListener(eventType, event => {
if (event.target.matches(selector)) {
callback(event);
}
});
});
}
const onPageLoad = (callback) => {
document.addEventListener('DOMContentLoaded', callback);
};
const replace = (oldElement, newHTMLString) => {
const placeHolder = document.createElement('div');
placeHolder.insertAdjacentHTML('afterBegin', newHTMLString);
const newElements = placeHolder.childNodes.values();
oldElement.replaceWith(...newElements);
removeExtraInputs();
}
const slideDownJS = (element) => {
if (element.classList.contains('slide-down'))
return;
element.classList.add('slide-down');
const cs = getComputedStyle(element);
const paddingHeight = parseInt(cs.paddingTop) + parseInt(cs.paddingBottom);
const height = (element.clientHeight - paddingHeight) + 'px';
element.style.height = '0px';
setTimeout(() => { element.style.height = height; }, 0);
};
const fetchWithCSRF = (url, params) => {
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
params = params || {};
params['headers'] = params['headers'] || new Headers;
params['headers'].append('X-CSRF-Token', csrfToken);
return fetch(url, params);
}
const removeExtraInputs = () => {
// This deletion will resovle a bug that creates an extra hidden input when rendering the comment elements.
const extraInputs = document.querySelectorAll('.comment_folder_button + .comment_folder_button');
for (const i of extraInputs) {
i.remove();
}
}
class _LobstersFunction {
constructor () {
this.curUser = document.body.getAttribute('data-username');
this.storyFlagReasons = ({<%= Vote::STORY_REASONS.map{|k,v|
"#{k.inspect}: #{v.inspect}" }.join(", ") %>});
this.commentFlagReasons = ({<%= Vote::COMMENT_REASONS.map{|k,v|
"#{k.inspect}: #{v.inspect}" }.join(", ") %>});
}
bounceToLogin() {
document.location = "/login?return=" + encodeURIComponent(document.location);
}
checkStoryDuplicate(form) {
const formData = new FormData(form);
const action = '/stories/check_url_dupe';
fetch(action, {
method: 'POST',
body: formData,
}).then (response => {
response.text().then(text => {
document.querySelector('.form_errors_header').innerHTML = text;
});
});
}
checkStoryTitle() { //partial removal (due to select2)
const titleLocation = document.getElementById('story_title');
if (!titleLocation) return;
const title = titleLocation.value;
if (!title) return;
// Will check title for common phrases to be removed like "ask/show" lobsters :"
// Then it will add the keyword "ask/show" to the tags.
const m = title.match(/^(show|ask) lobste\.?rs:? (.+)$/i);
if (m) {
const ta = $("#story_tags_a").data("select2");
if (ta.getVal().indexOf(m[1].toLowerCase()) < 0) {
ta.addSelectedChoice({ id: m[1].toLowerCase() });
$("#story_title").val(m[2]);
}
}
// common separators or (parens) that don't enclose a 4-digit year
if (title.match(/: | - | | — | \| | · | • | by /) ||
(title.match(/\([^\)]*\)/g) || []).some(function (p) { return !p.match(/\(\d{4}\)/) })) {
slideDownJS(document.querySelector('.title-reminder'));
}
}
fetchURLTitle(button, urlField, titleField) {
}
flagComment(voterEl) { //requires [_showFlagWhyAt]
}
flagStory(voterEl) { //requires [_showFlagWhyAt]
}
hideStory(hiderEl) {
if (!Lobster.curUser) return Lobster.bounceToLogin();
const li = parentSelector(hiderEl, ".story, .comment");
let act;
if (li.classList.contains("hidden")) {
act = "unhide";
li.classList.remove("hidden");
hiderEl.innerHTML = "hide";
} else {
act = "hide";
li.classList.add("hidden");
hiderEl.innerHTML = "unhide";
}
fetchWithCSRF("/stories/" + li.getAttribute("data-shortid") + "/" + act, {method: 'post'});
}
postComment(form) {
const formData = new FormData(form);
const action = form.getAttribute('action');
formData.append('show_tree_lines', true);
fetchWithCSRF (action, {
method: 'POST',
headers: new Headers({'X-Requested-With': 'XMLHttpRequest'}),
body: formData
})
.then(response => {
response.text().then(text => replace(form.parentElement, text));
})
}
previewComment(form) {
const formData = new FormData(form);
const action = form.getAttribute('action');
formData.append('preview', 'true');
formData.append('show_tree_lines', 'true');
fetchWithCSRF(action, {
method: 'POST',
headers: new Headers({'X-Requested-With': 'XMLHttpRequest'}),
body: formData
})
.then(response => {
response.text().then(text => {
replace(form.parentElement, text);
autosize(document.querySelectorAll('textarea'));
});
});
}
previewStory(form) { //requires [runSelect2]
}
runSelect2() { //requires [] (will actully replace select2)
}
saveStory(saverEl) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const li = parentSelector(saverEl, ".story, .comment");
let act;
if (li.classList.contains("saved")) {
act = "unsave";
li.classList.remove("saved");
saverEl.innerHTML = "save";
} else {
act = "save";
li.classList.add("saved");
saverEl.innerHTML = "unsave";
}
fetchWithCSRF("/stories/" + li.getAttribute("data-shortid") + "/" + act, {method: 'post'});
}
_showFlagWhyAt(thingType, voterEl, onChooseWhy) {
}
upvoteComment(voterEl) {
}
upvoteStory(voterEl) {
}
vote(thingType, voterEl, point, reason) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const li = parentSelector(voterEl, ".story, .comment");
const scoreDiv = li.querySelector("div.score");
const formData = new FormData();
formData.append('reason', reason || '');
let showScore = true;
let score = parseInt(scoreDiv.innerHTML);
let action = "";
if (isNaN(score)) {
showScore = false;
score = 0;
}
if (li.classList.contains("upvoted") && point > 0) {
/* already upvoted, neutralize */
li.classList.remove("upvoted");
score--;
action = "unvote";
} else if (li.classList.contains("flagged") && point < 0) {
/* already flagged, neutralize */
li.classList.remove("flagged");
score++;
action = "unvote";
} else if (point > 0) {
if (li.classList.contains("flagged")) {
/* Give back the lost flagged point */
score++;
}
li.classList.remove("flagged");
li.classList.add("upvoted");
score++;
action = "upvote";
} else if (point < 0) {
if (li.classList.contains("upvoted")) {
/* Removes the upvote point this user already gave the story*/
score--;
}
li.classList.remove("upvoted");
li.classList.add("flagged");
if (li.parentElement.querySelector('.comment_folder_button')) {
li.parentElement.querySelector('.comment_folder_button').setAttribute("checked", true);
};
showScore = false;
score--;
action = "flag";
}
if (showScore) {
scoreDiv.innerHTML = score;
} else {
scoreDiv.innerHTML = '~';
}
if (action == "upvote" || action == "unvote") {
if (li.querySelector(".reason")) {
li.querySelector(".reason").innerHTML = ""
};
if (action == "unvote" && point < 0)
li.querySelector(".flagger").textContent = "flag";
} else if (action == "flag") {
li.querySelector(".flagger").textContent = "unflag";
if (thingType == "comment") {
li.querySelector(".reason").innerHTML = "| " + Lobster.commentFlagReasons[reason].toLowerCase();
}
}
fetchWithCSRF("/" + (thingType == "story" ? "stories" : thingType + "s") + "/" +
li.getAttribute("data-shortid") + "/" + action, {
method: 'post',
body: formData });
}
}
onPageLoad(() => {
// Global Functions
const Lobster = new _LobstersFunction();
on('click', '.markdown_help_label', (event) => {
parentSelector(event.target, '.markdown_help_toggler').querySelector('.markdown_help').classList.toggle('display-block');
});
// Account Settings Functions
on('focusout', '#user_homepage', (event) => {
const homePage = event.target
if (homePage.value.trim() !== '' && !homePage.value.match('^[a-z]+:\/\/'))
homePage.value = 'https://' + homePage.value
})
// Inbox Related Funtions
on('change', '#message_hat_id', (event) => {
let selectedOption = event.target.selectedOptions[0];
document.getElementById('message_mod_note').checked = (selectedOption.getAttribute('data-modnote') === 'true');
});
// Story Related Functions
on('change', '#story_title', Lobster.checkStoryTitle);
Lobster.checkStoryTitle()
on('click', 'li.story a.upvoter', (event) => {
event.preventDefault();
Lobsters.upvoteStory(event.target);
});
on('click', 'li.story a.flagger', (event) => {
event.preventDefault();
Lobsters.flagStory(event.target);
});
on('click', 'li.story a.hider', (event) => {
event.preventDefault();
Lobster.hideStory(event.target);
})
on('click', 'li.story a.saver', (event) => {
event.preventDefault();
Lobster.saveStory(event.target);
});
on('click', 'button.story-preview', (event) => {
Lobsters.previewStory(parentSelector(event.target, 'form'));
});
// Comment Related Functions
on('click', '.comment a.flagger', (event) => {
event.preventDefault();
Lobsters.flagComment(event.target);
});
on("click", '.comment a.upvoter', (event) => {
event.preventDefault();
Lobsters.upvoteComment(event.target);
});
on('click', 'button.comment-preview', (event) => {
Lobster.previewComment(parentSelector(event.target, 'form'));
});
on('submit', '.comment_form_container form', (event) => {
event.preventDefault();
Lobster.postComment(event.target);
});
on('keydown', 'textarea#comment', (event) => {
if ((event.metaKey || event.ctrlKey) && event.keyCode == 13) {
Lobster.postComment(parentSelector(event.target, 'form'));
}
});
on('click', 'button.comment-cancel', (event) => {
const comment = (parentSelector(event.target, '.comment'));
const commentId = comment.getAttribute('data-shortid');
if (commentId !== null && commentId !== '') {
fetch('/comments/' + commentId + '?show_tree_lines=true')
.then(response => {
response.text().then(text => replace(comment, text));
});
} else {
comment.remove();
}
});
on('click', 'a.comment_editor', (event) => {
let comment = parentSelector(event.target, '.comment');
const commentId = comment.getAttribute('data-shortid')
fetch('/comments/' + commentId + '/edit')
.then(response => {
response.text().then(text => {
replace(comment, text);
autosize(document.querySelectorAll('textarea'));
});
});
autosize(document.querySelectorAll('textarea'));
});
on("click", "a.comment_deletor", (event) => {
event.preventDefault();
if (confirm("Are you sure you want to delete this comment?")) {
const comment = parentSelector(event.target, '.comment');
const commentId = comment.getAttribute('data-shortid');
fetchWithCSRF('/comments/' + commentId + '/delete',{method: 'post'})
.then(response => {
response.text().then(text => replace(comment, text));
});
}
});
on('click', 'a.comment_undeletor', (event) => {
event.preventDefault();
if (confirm("Are uou sure you want to undelete this comment?")) {
const comment = parentSelector(event.target, '.comment');
const commentId = comment.getAttribute('data-shortid');
fetchWithCSRF('/comments/' + commentId + '/undelete', {method: 'post'})
.then(response => {
response.text().then(text => replace(comment, text));
});
}
});
on('click', 'a.comment_moderator', (event) => {
const reason = prompt("Moderation reason:");
if (reason == null || reason == '')
return false;
const formData = new FormData();
formData.append('reason', reason);
const comment = parentSelector(event.target, '.comment');
const commentId = comment.getAttribute('data-shortid');
fetchWithCSRF('/comments/' + commentId + '/delete', { method: 'post', body: formData })
.then(response => {
response.text().then(text => replace(comment, text));
});
});
});