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

740 lines
22 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_tree .
//= require tom-select.base.js
//= require TomSelect_remove_button
//= require TomSelect_caret_position
//= require TomSelect_input_autogrow
"use strict";
const csrfToken = () => {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
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 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;
};
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) => {
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 (username) {
this.curUser = null;
this.storyFlagReasons = ({
<%= Vote::STORY_REASONS.map{|k,v| "'#{k}': '#{v}'" }.join(", ") %>
});
this.commentFlagReasons = ({
<%= Vote::COMMENT_REASONS.map{|k,v| "'#{k}': '#{v}'" }.join(", ") %>
});
}
bounceToLogin() {
document.location = "/login?return=" + encodeURIComponent(document.location);
}
modalFlaggingDropDown(flaggedItemType, voterEl, reasons) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const li = parentSelector(voterEl, '.story, .comment');
if (li.classList.contains('flagged')) {
/* already upvoted, neutralize */
if (li.classList.contains('story')) {
Lobster.voteStory(voterEl, -1, null);
} else {
Lobster.voteComment(voterEl, -1, null);
}
return
}
if (document.querySelector("#flag_dropdown") || document.querySelector('#modal_backdrop')) {
Lobster.removeFlagModal()
}
const modalDiv = document.createElement("div");
modalDiv.setAttribute('id', 'modal_backdrop');
document.body.appendChild(modalDiv);
const flaggingDropDown = document.createElement('div');
flaggingDropDown.setAttribute('id', 'flag_dropdown');
voterEl.after(flaggingDropDown);
Object.keys(reasons).map(function(k, v) {
let a = document.createElement('a')
a.textContent = reasons[k]
a.setAttribute('data', k)
a.setAttribute('href', '#')
if (k === '') {
a.classList.add('cancel-reason')
}
flaggingDropDown.append(a);
});
}
checkStoryDuplicate(form) {
const formData = new FormData(form);
const action = '/stories/check_url_dupe';
fetchWithCSRF(action, {
method: 'post',
headers: new Headers({'X-Requested-With': 'XMLHttpRequest'}),
body: formData,
}).then (response => {
response.text().then(text => {
document.querySelector('.form_errors_header').innerHTML = text;
});
});
}
checkStoryTitle() {
const titleLocation = document.getElementById('story_title');
if (!titleLocation) return;
const title = titleLocation.value;
if (!title) return;
// Check for common prefixes like "ask lobsters:", remove it, and add the appropriate tag.
const m = title.match(/^(show|ask) lobste\.?rs:? (.+)$/i);
if (m) {
const titleEl = document.getElementById('story_title');
Lobster.tom.addItem(m[1].toLowerCase());
titleEl.value = 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) {
const targetUrl = document.getElementById('story_url').value;
const title_field = document.getElementById('story_title');
const formData = new FormData();
const old_text = button.textContent;
if (targetUrl == "")
return;
button.setAttribute("disabled", true);
button.textContent = "Fetching...";
formData.append('fetch_url', targetUrl);
fetchWithCSRF('/stories/fetch_url_attributes', {
method: 'post',
headers: new Headers({'X-Requested-With': 'XMLHttpRequest'}),
body: formData,})
.then (response => response.json())
.then (data => {
title_field.value = data.title
button.textContent = old_text
});
button.removeAttribute("disabled");
Lobster.checkStoryTitle();
}
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'});
}
removeFlagModal() {
document.getElementById("flag_dropdown").remove();
document.getElementById("modal_backdrop").remove();
}
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(formElement) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const formData = new FormData(formElement);
const previewElement = document.getElementById('inside');
fetchWithCSRF('/stories/preview', {
method: 'post',
headers: new Headers({'X-Requested-With': 'XMLHttpRequest'}),
body: formData
}).then(response => {
response.text().then(text => {
previewElement.innerHTML = text;
Lobster.tomSelect();
});
});
}
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'});
}
tomSelect(item) {
if (!document.getElementById('story_tags_a')) {
return
}
TomSelect.define('caret_position', caret_position);
TomSelect.define('input_autogrow', input_autogrow);
TomSelect.define('remove_button', remove_button);
this.tom = new TomSelect('#story_tags_a', {
plugins: ['caret_position', 'input_autogrow', 'remove_button'],
maxOptions: 200,
maxItems: 10,
hideSelected: true,
closeAfterSelect: true,
selectOnTab: true,
sortField: {field: "data-value"},
onInitialize: function() {
const parent = document.querySelector(".ts-control");
parent.appendChild(document.querySelector(".ts-dropdown"));
},
render: {
option: function(data) {
return '<div>' +
'<span class="dropDownItem">' + data.title + '</span>' +
'</div>';
},
item: function(data) {
return '<div class="data-ts-item">' + data.value + '</div>';
}
}
});
}
upvoteComment(voterEl) {
Lobster.voteComment(voterEl, 1);
}
upvoteStory(voterEl) {
Lobster.voteStory(voterEl, 1);
}
voteStory(voterEl, point, reason) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const li = parentSelector(voterEl, '.story');
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";
}
fetchWithCSRF("/stories/" + li.getAttribute("data-shortid") + "/" + action, {
method: 'post',
body: formData });
}
voteComment(voterEl, point, reason) {
if (!Lobster.curUser)
return Lobster.bounceToLogin();
const li = parentSelector(voterEl, ".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");
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") {
li.querySelector(".reason").innerHTML = ""
}
if (action == "unvote" && point < 0) {
li.querySelector(".flagger").textContent = "flag";
} else if (action == "flag") {
li.querySelector(".flagger").textContent = "unflag";
li.querySelector(".reason").innerHTML = "| " + Lobster.commentFlagReasons[reason].toLowerCase();
}
fetchWithCSRF("/comments/" + li.getAttribute("data-shortid") + "/" + action, {
method: 'post',
body: formData });
}
}
const Lobster = new _LobstersFunction();
onPageLoad(() => {
Lobster.curUser = document.body.getAttribute('data-username'); // hack
autosize(document.querySelectorAll('textarea'));
// replace csrf token in forms that may be fragment caches with page token
for (const i of document.querySelectorAll('form input[name="authenticity_token"]')) {
i.value = csrfToken();
}
// Global Functions
on('click', '.markdown_help_label', (event) => {
parentSelector(event.target, '.markdown_help_toggler').querySelector('.markdown_help').classList.toggle('display-block');
});
on('click', '#modal_backdrop', () => {
Lobster.removeFlagModal()
});
on('click', '[data-confirm]', (event) => {
if (!confirm(event.target.dataset.confirm)) {
event.preventDefault();
}
});
// 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
Lobster.checkStoryTitle()
Lobster.tomSelect();
if (document.getElementById('story_url') && document.getElementById('story_preview') && !document.getElementById('story_preview').firstElementChild) {
document.getElementById('story_url').focus()
}
on('change', '#story_title', Lobster.checkStoryTitle);
on('click', '.story #flag_dropdown a', (event) => {
event.preventDefault();
if (event.target.getAttribute('data') != '') {
Lobster.voteStory(parentSelector(event.target, '.story'), -1, event.target.getAttribute('data'));
}
Lobster.removeFlagModal();
});
on('click', '#story_fetch_title', (event) => {
Lobster.fetchURLTitle(event.target);
});
on('click', 'li.story a.upvoter', (event) => {
event.preventDefault();
Lobster.upvoteStory(event.target);
});
on('click', 'li.story a.flagger', (event) => {
event.preventDefault();
const reasons = Lobster.storyFlagReasons;
Lobster.modalFlaggingDropDown("story", event.target, reasons);
});
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) => {
Lobster.previewStory(parentSelector(event.target, 'form'));
});
on('focusout', '#story_url', () => {
let url_tags = {
"\.pdf$": "pdf",
"[\/\.]((youtube|vimeo)\.com|youtu\.be|twitch.tv)\/": "video",
"[\/\.](slideshare\.net|speakerdeck\.com)\/": "slides",
"[\/\.](soundcloud\.com)\/": "audio",
};
const storyUrlEl = document.getElementById('story_url');
for (const [match, tag] of Object.entries(url_tags)) {
if (storyUrlEl.value.match(new RegExp(match, "i"))) {
Lobster.tom.addItem(tag.toLowerCase());
}
}
// check for dupe if there's a URL, but not when editing existing
if (storyUrlEl.value !== "" &&
(!document.querySelector('input[name="_method"]') ||
document.querySelector('input[name="_method"]').getAttribute('value') === 'put')) {
Lobster.checkStoryDuplicate(parentSelector(storyUrlEl, 'form'));
}
});
// Comment Related Functions
on('click', 'a.comment-disowner', (event) => {
if (confirm("Are you sure you want to disown this comment?")) {
let li = parentSelector(event.target, '.comment');
fetchWithCSRF('/comments/' + li.getAttribute('data-shortid') + '/disown', { method: 'post' })
.then(response => {
response.text().then(text => replace(li, text));
});
}
});
on("click", "a.comment_replier", (event) => {
if (!Lobster.curUser) {
Lobster.bounceToLogin();
}
const comment = parentSelector(event.target, '.comment');
const commentId = comment.getAttribute('id');
// Check to make sure we don't already have a reply box.
if (document.getElementById('reply_form_' + commentId)) {
return false;
}
// Inserts "> " on quoted text.
let sel = document.getSelection().toString();
if (sel != "") {
sel = sel.split("\n").map(s => "> " + s + '\n\n').join('');
sel += "\n";
}
let div = document.createElement('div');
if (document.querySelector('.comments_subtree')) {
parentSelector(comment, '.comments_subtree').lastElementChild.prepend(div);
} else {
comment.parentElement.lastElementChild.prepend(div);
}
fetch('/comments/' + comment.getAttribute('data-shortid') + '/reply')
.then(response => {
response.text().then(text => {
div.innerHTML = text;
div.setAttribute('id', 'reply_form_' + commentId );
var ta = div.querySelector("textarea");
ta.textContent = sel;
// This will place the cursor at the end of the quoted string and focus on your reply.
ta.setSelectionRange(sel.length, sel.length);
ta.focus();
autosize(ta);
})
});
});
on('click', '.comment a.flagger', (event) => {
event.preventDefault();
const reasons = Lobster.commentFlagReasons
Lobster.modalFlaggingDropDown("comment", event.target, reasons);
});
on('click', '.comment #flag_dropdown a', (event) => {
event.preventDefault();
if (event.target.getAttribute('data') != '') {
Lobster.voteComment(parentSelector(event.target, '.comment'), -1, event.target.getAttribute('data'));
}
Lobster.removeFlagModal()
});
on("click", '.comment a.upvoter', (event) => {
event.preventDefault();
Lobster.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.parentElement.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));
});
});
on('click', '.comment_unread', (event) => {
const nodes = document.getElementsByClassName('comment_unread')
const foundIndex = Array.from(nodes).findIndex(node => node === event.target)
const targetIndex = (foundIndex + 1) % nodes.length;
const targetY = nodes[targetIndex].getBoundingClientRect().top + window.scrollY
window.scrollTo({ top: targetY, behavior: 'smooth' })
});
});