Files
pyrom/data/static/js/bitties/pyrom-bitty.js
2025-12-09 03:33:27 +03:00

478 lines
14 KiB
JavaScript

const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
const badgeEditorEndpoint = '/hyperapi/badge-editor';
const previewEndpoint = '/api/babycode-preview';
const userEndpoint = '/api/current-user';
const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
export default class {
async showBookmarkMenu(ev, el) {
if ((el.sender.dataset.bookmarkId === el.ds('bookmarkId')) && el.childElementCount === 0) {
const searchParams = new URLSearchParams({
'id': el.sender.dataset.conceptId,
'require_reload': el.dataset.requireReload,
});
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${el.sender.dataset.bookmarkType}?${searchParams}`;
const res = await this.api.getHTML(bookmarkMenuHref);
if (res.error) {
return;
}
const frag = res.value;
el.appendChild(frag);
const menu = el.childNodes[0];
menu.showPopover();
const bRect = el.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
const preferredLeft = bRect.right - menuRect.width;
const preferredRight = bRect.right;
const enoughSpace = preferredLeft >= 0;
const scrollY = window.scrollY || window.pageYOffset;
if (enoughSpace) {
menu.style.left = `${preferredLeft}px`;
} else {
menu.style.left = `${bRect.left}px`;
}
menu.style.top = `${bRect.bottom + scrollY}px`;
menu.addEventListener('beforetoggle', (e) => {
if (e.newState === 'closed') {
// if it's still in the tree, remove it
// the delay is required to make sure its removed instantly when
// clicking the button when the menu is open
setTimeout(() => {menu.remove()}, 100);
};
}, { once: true });
} else if (el.childElementCount > 0) {
el.removeChild(el.childNodes[0]);
}
}
selectBookmarkCollection(ev, el) {
const clicked = el.sender;
if (el.sender === el) {
if (clicked.classList.contains('selected')) {
clicked.classList.remove('selected');
} else {
clicked.classList.add('selected');
}
} else {
el.classList.remove('selected');
}
}
async saveBookmarks(ev, el) {
const bookmarkHref = el.ds('bookmarkEndpoint');
const collection = el.querySelector('.bookmark-dropdown-item.selected');
let data = {};
if (collection) {
data['operation'] = 'move';
data['collection_id'] = collection.dataset.collectionId;
data['memo'] = el.querySelector('.bookmark-memo-input').value;
} else {
data['operation'] = 'remove';
data['collection_id'] = el.ds('originallyContainedIn');
}
const options = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
}
const requireReload = el.dsInt('requireReload') !== 0;
el.remove();
await fetch(bookmarkHref, options);
if (requireReload) {
window.location.reload();
}
}
async copyCode(ev, el) {
if (!el.isSender) {
return;
}
await navigator.clipboard.writeText(el.value);
el.textContent = 'Copied!'
await delay(1000);
el.textContent = 'Copy';
}
toggleAccordion(ev, el) {
const accordion = el;
const header = accordion.querySelector('.accordion-header');
if (!header.contains(el.sender)){
return;
}
const btn = el.sender;
const content = el.querySelector('.accordion-content');
// these are all meant to be in sync
accordion.classList.toggle('hidden');
content.classList.toggle('hidden');
btn.textContent = accordion.classList.contains('hidden') ? '+' : '-';
}
toggleTab(ev, el) {
const tabButtonsContainer = el.querySelector('.tab-buttons');
if (!el.contains(el.sender)) {
return;
}
if (el.sender.classList.contains('active')) {
return;
}
const targetId = el.senderDs('targetId');
const contents = el.querySelectorAll('.tab-content');
for (let content of contents) {
if (content.id === targetId) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
}
for (let button of tabButtonsContainer.children) {
if (button.dataset.targetId === targetId) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
}
}
#previousMarkup = null;
async babycodePreview(ev, el) {
if (el.sender.classList.contains('active')) {
return;
}
const previewErrorsContainer = el.querySelector('#babycode-preview-errors-container');
const previewContainer = el.querySelector('#babycode-preview-container');
const ta = document.getElementById('babycode-content');
const markup = ta.value.trim();
if (markup === '') {
previewErrorsContainer.textContent = 'Type something!';
previewContainer.textContent = '';
this.#previousMarkup = '';
return;
}
if (markup === this.#previousMarkup) {
return;
}
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
this.#previousMarkup = markup;
const res = await this.api.getJSON(previewEndpoint, [], {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
markup: markup,
banned_tags: bannedTags,
}),
});
if (res.error) {
switch (res.error.status) {
case 429:
previewErrorsContainer.textContent = '(Old preview, try again in a few seconds.)'
this.#previousMarkup = '';
break;
case 400:
previewErrorsContainer.textContent = '(Request got malformed.)'
break;
case 401:
previewErrorsContainer.textContent = '(You are not logged in.)'
break;
default:
previewErrorsContainer.textContent = '(Error. Check console.)'
break;
}
} else {
previewErrorsContainer.textContent = '';
previewContainer.innerHTML = res.value.html;
}
}
insertBabycodeTag(ev, el) {
const tagStart = el.senderDs('tag');
const breakLine = 'breakLine' in el.sender.dataset;
const prefill = 'prefill' in el.sender.dataset ? el.sender.dataset.prefill : '';
const hasAttr = tagStart[tagStart.length - 1] === '=';
let tagEnd = tagStart;
let tagInsertStart = `[${tagStart}]${breakLine ? '\n' : ''}`;
if (hasAttr) {
tagEnd = tagEnd.slice(0, -1);
}
const tagInsertEnd = `${breakLine ? '\n' : ''}[/${tagEnd}]`;
const hasSelection = el.selectionStart !== el.selectionEnd;
const text = el.value;
if (hasSelection) {
const realStart = Math.min(el.selectionStart, el.selectionEnd);
const realEnd = Math.max(el.selectionStart, el.selectionEnd);
const selectionLength = realEnd - realStart;
const strStart = text.slice(0, realStart);
const strEnd = text.substring(realEnd);
const frag = `${tagInsertStart}${text.slice(realStart, realEnd)}${tagInsertEnd}`;
const reconst = `${strStart}${frag}${strEnd}`;
el.value = reconst;
if (!hasAttr) {
el.setSelectionRange(realStart + tagInsertStart.length, realStart + tagInsertEnd.length + selectionLength - 1);
} else {
const attrCursor = realStart + tagInsertEnd.length - (1 + (breakLine ? 1 : 0))
el.setSelectionRange(attrCursor, attrCursor); // cursor on attr
}
} else {
if (hasAttr) {
tagInsertStart += prefill;
}
const cursor = el.selectionStart;
const strStart = text.slice(0, cursor);
const strEnd = text.substr(cursor);
let newCursor = strStart.length + tagInsertStart.length;
if (hasAttr) {
newCursor = cursor + tagInsertStart.length - prefill.length - (1 + (breakLine ? 1 : 0)) //cursor on attr
}
const reconst = `${strStart}${tagInsertStart}${tagInsertEnd}${strEnd}`;
el.value = reconst;
el.setSelectionRange(newCursor, newCursor);
}
el.focus();
}
addQuote(ev, el) {
el.value += el.sender.value;
el.scrollIntoView();
el.focus();
}
convertTimestamps(ev, el) {
const timestamp = el.dsInt('utc');
if (!isNaN(timestamp)) {
const date = new Date(timestamp * 1000);
el.textContent = date.toLocaleString();
}
}
#currentUsername = undefined;
async highlightMentions(ev, el) {
if (this.#currentUsername === undefined) {
const userInfo = await this.api.getJSON(userEndpoint);
if (!userInfo.value) {
return;
}
this.#currentUsername = userInfo.value.user.username;
}
if (el.ds('username') === this.#currentUsername) {
el.classList.add('me');
}
}
}
export class BadgeEditorForm {
#badgeTemplate = undefined;
async loadBadgeEditor(ev, el) {
const badges = await this.api.getHTML(badgeEditorEndpoint);
if (!badges.value) {
return;
}
if (this.#badgeTemplate === undefined){
const badge = await this.api.getHTML(`${badgeEditorEndpoint}/template`)
if (!badge.value){
return;
}
this.#badgeTemplate= badge.value;
}
el.replaceChildren();
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
const submitButton = `<input data-receive="updateBadgeCount" type="submit" value="Save badges">`;
const controls = `<span>${addButton} ${submitButton} <span data-count="1" data-receive="updateBadgeCount">BADGECOUNT/10</span></span>`
const badgeCount = badges.value.querySelectorAll('.settings-badge-container').length;
const subs = [
['BADGECOUNT', badgeCount],
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
];
el.appendChild(this.api.makeHTML(controls, subs));
el.appendChild(badges.value);
}
addBadge(ev, el) {
if (this.#badgeTemplate === undefined) {
return;
}
const badge = this.#badgeTemplate.cloneNode(true);
el.appendChild(badge);
this.api.localTrigger('updateBadgeCount');
}
deleteBadge(ev, el) {
if (!el.contains(el.sender)) {
return;
}
el.remove();
this.api.localTrigger('updateBadgeCount');
}
updateBadgeCount(_ev, el) {
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
if (el.dsInt('disableIfMax') === 1) {
el.disabled = badgeCount === 10;
} else if (el.dsInt('count') === 1) {
el.textContent = `${badgeCount}/10`;
}
}
badgeEditorPrepareSubmit(ev, el) {
if (ev.type !== 'submit') {
return;
}
ev.preventDefault();
const badges = el.querySelectorAll('.settings-badge-container').length;
const noUploads = el.querySelectorAll('.settings-badge-file-picker.hidden input[type=file]');
noUploads.forEach(e => {
e.value = null;
})
// console.log(noUploads);
el.submit();
// console.log('would submit now');
}
}
const validateBase64Img = dataURL => new Promise(resolve => {
const img = new Image();
img.onload = () => {
resolve(img.width === 88 && img.height === 31);
};
img.src = dataURL;
});
export class BadgeEditorBadge {
#badgeCustomImageData = null;
badgeUpdatePreview(ev, el) {
if (ev.type !== 'change') {
return;
}
// TODO: el.sender doesn't have a bittyParentBittyId
const selectBittyParent = el.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
return;
}
if (ev.val === 'custom') {
if (this.#badgeCustomImageData) {
el.src = this.#badgeCustomImageData;
} else {
el.removeAttribute('src');
}
return;
}
const option = el.sender.selectedOptions[0];
el.src = option.dataset.filePath;
}
async badgeUpdatePreviewCustom(ev, el) {
if (ev.type !== 'change') {
return;
}
if (el.bittyParentBittyId !== el.sender.bittyParentBittyId) {
return;
}
const file = ev.target.files[0];
if (file.size >= 1000 * 500) {
this.api.trigger('badgeErrorSize');
this.#badgeCustomImageData = null;
el.removeAttribute('src');
return;
}
const reader = new FileReader();
reader.onload = async e => {
const dimsValid = await validateBase64Img(e.target.result);
if (!dimsValid) {
this.api.trigger('badgeErrorDim');
this.#badgeCustomImageData = null;
el.removeAttribute('src');
return;
}
this.#badgeCustomImageData = e.target.result;
el.src = this.#badgeCustomImageData;
this.api.trigger('badgeHideErrors');
}
reader.readAsDataURL(file);
}
badgeToggleFilePicker(ev, el) {
if (ev.type !== 'change') {
return;
}
// TODO: el.sender doesn't have a bittyParentBittyId
const selectBittyParent = el.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) {
return;
}
const filePicker = el.querySelector('input[type=file]');
if (ev.val === 'custom') {
el.classList.remove('hidden');
if (filePicker.dataset.validity) {
filePicker.setCustomValidity(filePicker.dataset.validity);
}
filePicker.required = true;
} else {
el.classList.add('hidden');
filePicker.setCustomValidity('');
filePicker.required = false;
}
}
openBadgeFilePicker(ev, el) {
// TODO: el.sender doesn't have a bittyParentBittyId
if (el.sender.parentNode !== el.parentNode) {
return;
}
el.click();
}
badgeErrorSize(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image can't be over 500KB."
el.dataset.validity = validity;
el.setCustomValidity(validity);
el.reportValidity();
}
badgeErrorDim(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image must be exactly 88x31 pixels."
el.dataset.validity = validity;
el.setCustomValidity(validity);
el.reportValidity();
}
badgeHideErrors(_ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
delete el.dataset.validity;
el.setCustomValidity('');
}
}