assets/controllers/subject_controller.js
// SPDX-FileCopyrightText: 2023 /kbin contributors <https://kbin.pub/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import {Controller} from '@hotwired/stimulus';
import {fetch, ok} from "../utils/http";
import {useIntersection} from 'stimulus-use'
import router from "../utils/routing";
import getIntIdFromElement, {getLevel, getTypeFromNotification} from "../utils/kbin";
import GLightbox from 'glightbox';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static previewInit = false;
static targets = ['loader', 'more', 'container', 'commentsCounter', 'favCounter', 'upvoteCounter', 'downvoteCounter']
static values = {
loading: Boolean,
isExpandedValue: Boolean
};
static sendBtnLabel = null;
connect() {
const params = {selector: '.thumb', openEffect: 'none', closeEffect: 'none', slideEffect: 'none'};
GLightbox(params);
const self = this;
if (this.hasMoreTarget) {
this.moreTarget.addEventListener('focusin', () => {
self.element.parentNode
.querySelectorAll('.z-5')
.forEach((el) => {
el.classList.remove('z-5');
});
this.element.classList.add('z-5');
});
}
if (this.element.classList.contains('show-preview')) {
useIntersection(this)
}
this.handleSpoilers()
this.checkHeight();
this.handleAdultThumbs()
}
async getForm(event) {
event.preventDefault();
if ('' !== this.containerTarget.innerHTML.trim()) {
if (false === confirm('Do you really want to leave?')) {
return;
}
}
try {
this.loadingValue = true;
let response = await fetch(event.target.href, {method: 'GET'});
response = await ok(response);
response = await response.json();
this.containerTarget.style.display = 'block';
this.containerTarget.innerHTML = response.form;
const textarea = this.containerTarget.querySelector('textarea');
if (textarea) {
if (textarea.value !== "") {
let firstLineEnd = textarea.value.indexOf("\n");
if (-1 === firstLineEnd) {
firstLineEnd = textarea.value.length;
textarea.value = textarea.value.slice(0, firstLineEnd) + " " + textarea.value.slice(firstLineEnd);
textarea.selectionStart = firstLineEnd + 1;
textarea.selectionEnd = firstLineEnd + 1;
} else {
textarea.value = textarea.value.slice(0, firstLineEnd) + " " + textarea.value.slice(firstLineEnd);
textarea.selectionStart = firstLineEnd + 1;
textarea.selectionEnd = firstLineEnd + 1;
}
}
textarea.focus();
}
} catch (e) {
window.location.href = event.target.href;
} finally {
this.loadingValue = false;
popover.togglePopover(false);
}
}
async sendForm(event) {
event.preventDefault();
const form = event.target.closest('form');
const url = form.action;
try {
this.loadingValue = true;
self.sendBtnLabel = event.target.innerHTML;
event.target.disabled = true;
event.target.innerHTML = 'Sending...';
let response = await fetch(url, {
method: 'POST',
body: new FormData(form)
});
response = await ok(response);
response = await response.json();
if (response.form) {
this.containerTarget.style.display = 'block';
this.containerTarget.innerHTML = response.form;
} else if (form.classList.contains('replace')) {
const div = document.createElement('div');
div.innerHTML = response.html;
div.firstElementChild.className = this.element.className;
this.element.innerHTML = div.firstElementChild.innerHTML;
} else {
const div = document.createElement('div');
div.innerHTML = response.html;
let level = getLevel(this.element);
div.firstElementChild.classList.add('comment-level--' + (level >= 10 ? 10 : level + 1));
if (this.element.nextElementSibling && this.element.nextElementSibling.classList.contains('comments')) {
this.element.nextElementSibling.appendChild(div.firstElementChild);
this.element.classList.add('mb-0');
} else {
this.element.parentNode.insertBefore(div.firstElementChild, this.element.nextSibling);
}
this.containerTarget.style.display = 'none';
this.containerTarget.innerHTML = '';
}
} catch (e) {
// this.containerTarget.innerHTML = '';
} finally {
this.application
.getControllerForElementAndIdentifier(document.getElementById('main'), 'lightbox')
.connect();
this.application
.getControllerForElementAndIdentifier(document.getElementById('main'), 'timeago')
.connect();
this.loadingValue = false;
event.target.disabled = false;
event.target.innerHTML = self.sendBtnLabel;
}
}
async favourite(event) {
event.preventDefault();
const form = event.target.closest('form');
try {
this.loadingValue = true;
let response = await fetch(form.action, {
method: 'POST',
body: new FormData(form)
});
response = await ok(response);
response = await response.json();
form.innerHTML = response.html;
} catch (e) {
form.submit();
} finally {
this.loadingValue = false;
}
}
async vote(event) {
event.preventDefault();
const form = event.target.closest('form');
try {
this.loadingValue = true;
let response = await fetch(form.action, {
method: 'POST',
body: new FormData(form)
});
response = await ok(response);
response = await response.json();
event.target.closest('.vote').outerHTML = response.html;
} catch (e) {
form.submit();
} finally {
this.loadingValue = false;
}
}
loadingValueChanged(val) {
const submitButton = this.containerTarget.querySelector('form button[type="submit"]');
if (true === val) {
if (submitButton) {
submitButton.disabled = true;
}
this.loaderTarget.style.display = 'block';
} else {
if (submitButton) {
submitButton.disabled = false;
}
this.loaderTarget.style.display = 'none';
}
}
async showModPanel(event) {
event.preventDefault();
let container = this.element.nextElementSibling && this.element.nextElementSibling.classList.contains('js-container') ? this.element.nextElementSibling : null;
if (null === container) {
container = document.createElement('div');
container.classList.add('js-container');
this.element.insertAdjacentHTML('afterend', container.outerHTML);
} else {
if (container.querySelector('.moderate-panel')) {
return;
}
}
try {
this.loadingValue = true;
let response = await fetch(event.target.href);
response = await ok(response);
response = await response.json();
this.element.nextElementSibling.insertAdjacentHTML('afterbegin', response.html);
} catch (e) {
window.location.href = event.target.href;
} finally {
this.loadingValue = false;
}
}
notification(data) {
if (data.detail.parentSubject && this.element.id === data.detail.parentSubject.htmlId) {
if (data.detail.op.endsWith('CommentDeletedNotification') || data.detail.op.endsWith('CommentCreatedNotification')) {
this.updateCommentCounter(data);
}
}
if (this.element.id !== data.detail.htmlId) {
return;
}
if (data.detail.op.endsWith('EditedNotification')) {
this.refresh(data);
return;
}
if (data.detail.op.endsWith('DeletedNotification')) {
this.element.remove();
return;
}
if (data.detail.op.endsWith('Vote')) {
this.updateVotes(data);
return;
}
if (data.detail.op.endsWith('Favourite')) {
this.updateFavourites(data);
return;
}
}
async refresh(data) {
try {
this.loadingValue = true;
const url = router().generate(`ajax_fetch_${getTypeFromNotification(data)}`, {id: getIntIdFromElement(this.element)});
let response = await fetch(url);
response = await ok(response);
response = await response.json();
const div = document.createElement('div');
div.innerHTML = response.html;
div.firstElementChild.className = this.element.className;
this.element.outerHTML = div.firstElementChild.outerHTML;
} catch (e) {
} finally {
this.loadingValue = false;
}
}
updateVotes(data) {
this.upvoteCounterTarget.innerText = `(${data.detail.up})`;
if (data.detail.up > 0) {
this.upvoteCounterTarget.classList.remove('hidden');
} else {
this.upvoteCounterTarget.classList.add('hidden');
}
if (this.hasDownvoteCounterTarget) {
this.downvoteCounterTarget.innerText = data.detail.down;
}
}
updateFavourites(data) {
if (this.hasFavCounterTarget) {
this.favCounterTarget.innerText = data.detail.count;
}
}
updateCommentCounter(data) {
if (data.detail.op.endsWith('CommentCreatedNotification') && this.hasCommentsCounterTarget) {
this.commentsCounterTarget.innerText = parseInt(this.commentsCounterTarget.innerText) + 1;
}
if (data.detail.op.endsWith('CommentDeletedNotification') && this.hasCommentsCounterTarget) {
this.commentsCounterTarget.innerText = parseInt(this.commentsCounterTarget.innerText) - 1;
}
}
async removeImage(event) {
event.preventDefault();
try {
this.loadingValue = true;
let response = await fetch(event.target.parentNode.formAction, {method: 'POST'});
response = await ok(response);
response = await response.json();
event.target.parentNode.previousElementSibling.remove();
event.target.parentNode.nextElementSibling.classList.remove('hidden');
event.target.parentNode.remove();
} catch (e) {
} finally {
this.loadingValue = false;
}
}
appear() {
if (this.previewInit) {
return;
}
const prev = this.element.querySelectorAll('.show-preview');
prev.forEach((el) => {
el.click();
});
this.previewInit = true;
}
checkHeight() {
this.isExpandedValue = false;
const elem = this.element.querySelector('.content');
if (elem) {
elem.style.maxHeight = '25rem'
if (elem.scrollHeight - 30 > elem.clientHeight
|| elem.scrollWidth > elem.clientWidth) {
this.moreBtn = this.createMoreBtn(elem);
this.more();
} else {
elem.style.maxHeight = null;
}
}
}
createMoreBtn(elem) {
let moreBtn = document.createElement('div')
moreBtn.innerHTML = '<i class="fa-solid fa-angles-down"></i>';
moreBtn.classList.add('more');
elem.parentNode.insertBefore(moreBtn, elem.nextSibling);
return moreBtn;
}
more() {
this.moreBtn.addEventListener('click', e => {
if (e.target.previousSibling.style.maxHeight) {
e.target.previousSibling.setAttribute('style', 'margin-bottom: 2rem !important');
e.target.previousSibling.style.maxHeight = null;
e.target.innerHTML = '<i class="fa-solid fa-angles-up"></i>';
this.isExpandedValue = true;
} else {
e.target.previousSibling.style.maxHeight = '25rem';
e.target.previousSibling.style.marginBottom = null;
e.target.innerHTML = '<i class="fa-solid fa-angles-down"></i>';
e.target.previousSibling.scrollIntoView();
this.isExpandedValue = false;
}
})
}
expand() {
if (!this.isExpandedValue) {
this.moreBtn.click();
}
}
handleAdultThumbs() {
// @todo temporary fix
const adultBadge = this.element.querySelector('.danger');
if (adultBadge && (adultBadge.textContent.includes('18') || adultBadge.textContent.toLowerCase().includes('nsfw'))) {
const image = this.element.querySelector('.thumb-subject');
if (image) {
image.style.filter = 'blur(8px)';
image.addEventListener('mouseenter', () => {
image.style.filter = 'none';
});
image.addEventListener('mouseleave', () => {
image.style.filter = 'blur(8px)';
});
}
}
}
handleSpoilers() {
const regexp = /(\s|^)(:::|<p>:::)\s+spoiler\s+([^\n]+)\n((?:.*(?:.*\n)+?))(:::(?:<br\/>|<\/p>)?|$)/gm;
let content = this.element.querySelector('.content');
if (!content) {
return;
}
content = content.innerHTML;
let matches;
while ((matches = regexp.exec(content)) !== null) {
const prefix = matches[1];
const title = matches[3].trim();
const body = matches[4].trim();
const replacement = `${prefix}<details><summary>${title}</summary>${body}</details>`;
content = content.replace(matches[0], replacement);
}
this.element.querySelector('.content').innerHTML = content;
}
}