diff --git a/data/static/js/babycode-editor.js b/data/static/js/babycode-editor.js deleted file mode 100644 index a095c12..0000000 --- a/data/static/js/babycode-editor.js +++ /dev/null @@ -1,39 +0,0 @@ -{ - let ta = document.getElementById("babycode-content"); - - ta.addEventListener("keydown", (e) => { - if(e.key === "Enter" && e.ctrlKey) { - if (inThread()) { - localStorage.removeItem(window.location.pathname); - } - e.target.form?.submit(); - } - }) - - const inThread = () => { - const scheme = window.location.pathname.split("/"); - return scheme[1] === "threads" && scheme[2] !== "create"; - } - - ta.addEventListener("input", () => { - if (!inThread()) return; - - localStorage.setItem(window.location.pathname, ta.value); - }) - - if (inThread()) { - const form = ta.closest('.post-edit-form'); - if (form){ - form.addEventListener("submit", () => { - localStorage.removeItem(window.location.pathname); - }) - } - } - - document.addEventListener("DOMContentLoaded", () => { - if (!inThread()) return; - const prevContent = localStorage.getItem(window.location.pathname); - if (!prevContent) return; - ta.value = prevContent; - }) -} diff --git a/data/static/js/bitties/pyrom-bitty.js b/data/static/js/bitties/pyrom-bitty.js deleted file mode 100644 index 9ffb006..0000000 --- a/data/static/js/bitties/pyrom-bitty.js +++ /dev/null @@ -1,543 +0,0 @@ -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 ((ev.sender.dataset.bookmarkId === el.prop('bookmarkId')) && el.childElementCount === 0) { - const searchParams = new URLSearchParams({ - 'id': ev.sender.dataset.conceptId, - 'require_reload': el.dataset.requireReload, - }); - const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.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 = ev.sender; - - if (ev.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.prop('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.prop('originallyContainedIn'); - } - - const options = { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - } - const requireReload = el.propToInt('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(ev.sender)){ - return; - } - const btn = ev.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(ev.sender)) { - return; - } - - if (ev.sender.classList.contains('active')) { - return; - } - - const targetId = ev.sender.prop('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 (ev.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 = ev.sender.prop('tag'); - const breakLine = 'breakLine' in ev.sender.dataset; - const prefill = 'prefill' in ev.sender.dataset ? ev.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 += ev.sender.value; - el.scrollIntoView(); - el.focus(); - } - - convertTimestamps(ev, el) { - const timestamp = el.propToInt('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.prop('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){ - this.#badgeTemplate = document.getElementById('badge-editor-template').content.firstElementChild.outerHTML; - } - el.replaceChildren(); - const addButton = ``; - const submitButton = ``; - const controls = `${addButton} ${submitButton} BADGECOUNT/10` - 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)); - - const listTemplate = document.getElementById('badges-list-template').content.firstElementChild.outerHTML; - const list = this.api.makeHTML(listTemplate).firstElementChild; - list.appendChild(badges.value); - el.appendChild(list); - } - - addBadge(ev, el) { - if (this.#badgeTemplate === undefined) { - return; - } - const badge = this.api.makeHTML(this.#badgeTemplate).firstElementChild; - el.appendChild(badge); - this.api.localTrigger('updateBadgeCount'); - } - - deleteBadge(ev, el) { - if (!el.contains(ev.sender)) { - return; - } - el.remove(); - this.api.localTrigger('updateBadgeCount'); - } - - updateBadgeCount(ev, el) { - const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length; - if (el.propToInt('disableIfMax') === 1) { - el.disabled = badgeCount === 10; - } else if (el.propToInt('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; - }) - el.submit(); - } -} - -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: ev.sender doesn't have a bittyParent - const selectBittyParent = ev.sender.closest('bitty-7-0'); - if (el.bittyParent !== selectBittyParent) { - return; - } - - if (ev.value === 'custom') { - if (this.#badgeCustomImageData) { - el.src = this.#badgeCustomImageData; - } else { - el.removeAttribute('src'); - } - return; - } - const option = ev.sender.selectedOptions[0]; - el.src = option.dataset.filePath; - } - - async badgeUpdatePreviewCustom(ev, el) { - if (ev.type !== 'change') { - return; - } - if (el.bittyParent !== ev.sender.bittyParent) { - return; - } - - const file = ev.target.files[0]; - if (file.size >= 1000 * 500) { - this.api.localTrigger('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.localTrigger('badgeErrorDim'); - this.#badgeCustomImageData = null; - el.removeAttribute('src'); - return; - } - this.#badgeCustomImageData = e.target.result; - el.src = this.#badgeCustomImageData; - this.api.localTrigger('badgeHideErrors'); - } - - reader.readAsDataURL(file); - } - - badgeToggleFilePicker(ev, el) { - if (ev.type !== 'change') { - return; - } - // TODO: ev.sender doesn't have a bittyParent - const selectBittyParent = ev.sender.closest('bitty-7-0'); - if (el.bittyParent !== selectBittyParent) { - return; - } - const filePicker = el.querySelector('input[type=file]'); - if (ev.value === '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: ev.sender doesn't have a bittyParent - if (ev.sender.parentNode !== el.parentNode) { - return; - } - el.click(); - } - - badgeErrorSize(ev, el) { - const validity = "Image can't be over 500KB." - el.dataset.validity = validity; - el.setCustomValidity(validity); - el.reportValidity(); - } - - badgeErrorDim(ev, el) { - const validity = "Image must be exactly 88x31 pixels." - el.dataset.validity = validity; - el.setCustomValidity(validity); - el.reportValidity(); - } - - badgeHideErrors(ev, el) { - delete el.dataset.validity; - el.setCustomValidity(''); - } -} - -const getCollectionDataForEl = el => { - const nameInput = el.querySelector(".collection-name"); - const collectionId = el.dataset.collectionId; - - return { - id: collectionId, - name: nameInput.value, - is_new: !('collectionId' in el.dataset), - }; -} - -export class CollectionsEditor { - #collectionTemplate = undefined; - #collectionsData = []; - #removedCollections = []; - #valid = true; - - addCollection(ev, el) { - if (this.#collectionTemplate === undefined) { - this.#collectionTemplate = document.getElementById('new-collection-template').content; - } - // interesting - const newCollection = this.api.makeHTML(this.#collectionTemplate.firstElementChild.outerHTML); - el.appendChild(newCollection); - } - - deleteCollection(ev, el) { - if (!el.contains(ev.sender)) { - return; - } - if ('collectionId' in el.dataset) { - this.#removedCollections.push(el.dataset.collectionId); - } - el.remove(); - } - - async saveCollections(ev, el) { - this.#valid = true; - this.api.localTrigger('testValidity'); - if (!this.#valid) { - return; - } - this.#collectionsData = []; - this.api.localTrigger('getCollectionData'); - - const data = { - collections: this.#collectionsData, - removed_collections: this.#removedCollections, - }; - - const res = await this.api.getJSON(el.prop('submitHref'), [], { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - if (res.error) { - return; - } - - window.location.reload(); - } - - getCollectionData(ev, el) { - this.#collectionsData.push(getCollectionDataForEl(el)); - } - - testValidity(ev, el) { - const input = el.querySelector('input'); - if (!input.validity.valid) { - input.reportValidity(); - this.#valid = false; - } - } -} diff --git a/data/static/js/thread.js b/data/static/js/thread.js deleted file mode 100644 index 113d5b0..0000000 --- a/data/static/js/thread.js +++ /dev/null @@ -1,360 +0,0 @@ -{ - const ta = document.getElementById("babycode-content"); - - function supportsPopover() { - return Object.hasOwn(HTMLElement.prototype, "popover"); - } - - if (supportsPopover()){ - let quotedPostContainer = null; - function isQuoteSelectionValid() { - const selection = document.getSelection(); - - if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { - return false; - } - - const range = selection.getRangeAt(0); - const commonAncestor = range.commonAncestorContainer; - - const ancestorElement = commonAncestor.nodeType === Node.TEXT_NODE - ? commonAncestor.parentNode - : commonAncestor; - - const container = ancestorElement.closest(".post-inner"); - if (!container) { - return false; - } - const success = container.contains(ancestorElement); - if (success) { - quotedPostContainer = container; - } - return success; - } - - let quotePopover = null; - let isSelecting = false; - - document.addEventListener("mousedown", () => { - isSelecting = true; - }) - - document.addEventListener("mouseup", () => { - isSelecting = false; - handlePossibleSelection(); - }) - - document.addEventListener("keyup", (e) => { - if (e.shiftKey && (e.key.startsWith('Arrow') || e.key === 'Home' || e.key === 'End')) { - handlePossibleSelection(); - } - }) - - function handlePossibleSelection() { - setTimeout(() => { - const valid = isQuoteSelectionValid(); - if (isSelecting || !valid) { - removeQuotePopover(); - return; - } - - const selection = document.getSelection(); - const selectionStr = selection.toString().trim(); - if (selection.isCollapsed || selectionStr === "") { - removeQuotePopover(); - return; - } - - showQuotePopover(); - }, 50) - } - - function removeQuotePopover() { - quotePopover?.hidePopover(); - } - - function createQuotePopover() { - quotePopover = document.createElement("div"); - quotePopover.popover = "auto"; - quotePopover.className = "quote-popover"; - - const quoteButton = document.createElement("button"); - quoteButton.textContent = "Quote fragment" - quoteButton.className = "reduced" - quotePopover.appendChild(quoteButton); - document.body.appendChild(quotePopover); - return quoteButton; - } - - function showQuotePopover() { - if (!quotePopover) { - const quoteButton = createQuotePopover(); - quoteButton.addEventListener("click", () => { - console.log("Quoting:", document.getSelection().toString()); - const postPermalink = quotedPostContainer.dataset.postPermalink; - const authorUsername = quotedPostContainer.dataset.authorUsername; - console.log(postPermalink, authorUsername); - if (ta.value.trim() !== "") { - ta.value += "\n" - } - ta.value += `@${authorUsername} [url=${postPermalink}]said:[/url]\n[quote]<:scissors:> ${document.getSelection().toString()} <:scissors:>[/quote]\n`; - ta.scrollIntoView() - ta.focus(); - - document.getSelection().empty(); - removeQuotePopover(); - }) - } - - const range = document.getSelection().getRangeAt(0); - const rect = range.getBoundingClientRect(); - const scrollY = window.scrollY || window.pageYOffset; - - quotePopover.style.setProperty("top", `${rect.top + scrollY - 55}px`) - quotePopover.style.setProperty("left", `${rect.left + rect.width/2}px`) - - if (!quotePopover.matches(':popover-open')) { - quotePopover.showPopover(); - } - } - } - - const deleteDialog = document.getElementById("delete-dialog"); - const deleteDialogCloseButton = document.getElementById("post-delete-dialog-close"); - let deletionTargetPostContainer; - - function closeDeleteDialog() { - deletionTargetPostContainer.style.removeProperty("background-color"); - deleteDialog.close(); - } - - deleteDialogCloseButton.addEventListener("click", (e) => { - closeDeleteDialog(); - }) - deleteDialog.addEventListener("click", (e) => { - if (e.target === deleteDialog) { - closeDeleteDialog(); - } - }) - for (let button of document.querySelectorAll(".post-delete-button")) { - button.addEventListener("click", (e) => { - deleteDialog.showModal(); - const postId = button.value; - deletionTargetPostContainer = document.getElementById("post-" + postId).querySelector(".post-content-container"); - deletionTargetPostContainer.style.setProperty("background-color", "#fff"); - const form = document.getElementById("post-delete-form"); - form.action = `/post/${postId}/delete` - }) - } - - const threadEndpoint = document.getElementById("thread-subscribe-endpoint").value; - let now = Math.floor(new Date() / 1000); - function hideNotification() { - const notification = document.getElementById('new-post-notification'); - notification.classList.add('hidden'); - } - - function showNewPostNotification(url) { - const notification = document.getElementById("new-post-notification"); - - notification.classList.remove("hidden"); - - document.getElementById("dismiss-new-post-button").onclick = () => { - now = Math.floor(new Date() / 1000); - hideNotification(); - tryFetchUpdate(); - } - - document.getElementById("go-to-new-post-button").href = url; - - document.getElementById("unsub-new-post-button").onclick = () => { - hideNotification(); - } - } - - function tryFetchUpdate() { - if (!threadEndpoint) return; - const body = JSON.stringify({'since': now}); - fetch(threadEndpoint, {method: "POST", headers: {"Content-Type": "application/json"}, body: body}) - .then(res => res.json()) - .then(json => { - if (json.status === "none") { - setTimeout(tryFetchUpdate, 5000); - } else if (json.status === "new_post") { - showNewPostNotification(json.url); - } - }) - .catch(error => console.log(error)) - } - tryFetchUpdate(); - - if (supportsPopover()){ - const reactionEmoji = document.getElementById("allowed-reaction-emoji").value.split(" "); - let reactionPopover = null; - let reactionTargetPostId = null; - - function tryAddReaction(emoji, postId = reactionTargetPostId) { - const body = JSON.stringify({ - "emoji": emoji, - }); - fetch(`/api/add-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body}) - .then(res => res.json()) - .then(json => { - if (json.status === "added") { - const post = document.getElementById(`post-${postId}`); - const spans = Array.from(post.querySelectorAll(".reaction-count")).filter((span) => { - return span.dataset.emoji === emoji - }); - if (spans.length > 0) { - const currentValue = spans[0].textContent; - spans[0].textContent = `${parseInt(currentValue) + 1}`; - const button = spans[0].closest(".reaction-button"); - button.classList.add("active"); - } else { - const span = document.createElement("span"); - span.classList = "reaction-container"; - span.dataset.emoji = emoji; - - const button = document.createElement("button"); - button.type = "button"; - button.className = "reduced reaction-button active"; - - button.addEventListener("click", () => { - tryAddReaction(emoji, postId); - }) - - const img = document.createElement("img"); - img.src = `/static/emoji/${emoji}.png`; - - button.textContent = " x"; - - const reactionCountSpan = document.createElement("span") - reactionCountSpan.className = "reaction-count" - reactionCountSpan.textContent = "1" - - button.insertAdjacentElement("afterbegin", img); - button.appendChild(reactionCountSpan); - - span.appendChild(button); - - const post = document.getElementById(`post-${postId}`); - post.querySelector(".post-reactions").insertBefore(span, post.querySelector(".add-reaction-button")); - } - } else if (json.error_code === 409) { - console.log("reaction exists, gonna try and remove"); - tryRemoveReaction(emoji, postId); - } else { - console.warn(json) - } - }) - .catch(error => console.error(error)); - } - - function tryRemoveReaction(emoji, postId = reactionTargetPostId) { - const body = JSON.stringify({ - "emoji": emoji, - }); - - fetch(`/api/remove-reaction/${postId}`, {method: "POST", headers: {"Content-Type": "application/json"}, body: body}) - .then(res => res.json()) - .then(json => { - if (json.status === "removed") { - const post = document.getElementById(`post-${postId}`); - const spans = Array.from(post.querySelectorAll(".reaction-container")).filter((span) => { - return span.dataset.emoji === emoji - }); - if (spans.length > 0) { - const reactionCountSpan = spans[0].querySelector(".reaction-count"); - const currentValue = parseInt(reactionCountSpan.textContent); - if (currentValue - 1 === 0) { - spans[0].remove(); - } else { - reactionCountSpan.textContent = `${parseInt(currentValue) - 1}`; - const button = reactionCountSpan.closest(".reaction-button"); - button.classList.remove("active"); - } - } - } else { - console.warn(json) - } - }) - .catch(error => console.error(error)); - } - - function createReactionPopover() { - reactionPopover = document.createElement("div"); - reactionPopover.className = "reaction-popover"; - reactionPopover.popover = "auto"; - - const inner = document.createElement("div"); - inner.className = "reaction-popover-inner"; - - reactionPopover.appendChild(inner); - - for (let emoji of reactionEmoji) { - const img = document.createElement("img"); - img.src = `/static/emoji/${emoji}.png`; - - const button = document.createElement("button"); - button.type = "button"; - button.className = "reduced"; - button.appendChild(img); - button.addEventListener("click", () => { - tryAddReaction(emoji); - }) - - button.dataset.emojiName = emoji; - - inner.appendChild(button); - } - - reactionPopover.addEventListener("beforetoggle", (e) => { - if (e.newState === "closed") { - reactionTargetPostId = null; - } - }) - - document.body.appendChild(reactionPopover); - } - - function showReactionPopover() { - if (!reactionPopover) { - createReactionPopover(); - } - - if (!reactionPopover.matches(':popover-open')) { - reactionPopover.showPopover(); - } - } - - for (let button of document.querySelectorAll(".add-reaction-button")) { - button.addEventListener("click", (e) => { - showReactionPopover(); - reactionTargetPostId = e.target.dataset.postId; - - const rect = e.target.getBoundingClientRect(); - const popoverRect = reactionPopover.getBoundingClientRect(); - const scrollY = window.scrollY || window.pageYOffset; - - reactionPopover.style.setProperty("top", `${rect.top + scrollY + rect.height}px`) - reactionPopover.style.setProperty("left", `${rect.left + rect.width/2 - popoverRect.width/2}px`) - }) - } - - for (let button of document.querySelectorAll(".reaction-button")) { - button.addEventListener("click", () => { - const reactionContainer = button.closest(".reaction-container") - const emoji = reactionContainer.dataset.emoji; - const postId = reactionContainer.dataset.postId; - console.log(reactionContainer); - tryAddReaction(emoji, postId); - }) - } - - } else { - for (let button of document.querySelectorAll(".add-reaction-button")) { - button.disabled = true; - button.title = "Enable JS to add reactions." - } - } -} diff --git a/data/static/js/topic.js b/data/static/js/topic.js deleted file mode 100644 index 6489843..0000000 --- a/data/static/js/topic.js +++ /dev/null @@ -1,16 +0,0 @@ -{ - const deleteDialog = document.getElementById("delete-dialog"); - const deleteDialogOpenButton = document.getElementById("topic-delete-dialog-open"); - deleteDialogOpenButton.addEventListener("click", (e) => { - deleteDialog.showModal(); - }); - const deleteDialogCloseButton = document.getElementById("topic-delete-dialog-close"); - deleteDialogCloseButton.addEventListener("click", (e) => { - deleteDialog.close(); - }) - deleteDialog.addEventListener("click", (e) => { - if (e.target === deleteDialog) { - deleteDialog.close(); - } - }) -} diff --git a/data/static/js/ui.js b/data/static/js/ui.js deleted file mode 100644 index 7f6ed1a..0000000 --- a/data/static/js/ui.js +++ /dev/null @@ -1,235 +0,0 @@ -function openLightbox(post, idx) { - lightboxCurrentPost = post; - lightboxCurrentIdx = idx; - lightboxObj.img.src = lightboxImages.get(post)[idx].src; - lightboxObj.openOriginalAnchor.href = lightboxImages.get(post)[idx].src - lightboxObj.prevButton.disabled = lightboxImages.get(post).length === 1 - lightboxObj.nextButton.disabled = lightboxImages.get(post).length === 1 - lightboxObj.imageCount.textContent = `Image ${idx + 1} of ${lightboxImages.get(post).length}` - - if (!lightboxObj.dialog.open) { - lightboxObj.dialog.showModal(); - } -} - -const modulo = (n, m) => ((n % m) + m) % m - -function lightboxNext() { - const l = lightboxImages.get(lightboxCurrentPost).length; - const target = modulo(lightboxCurrentIdx + 1, l); - openLightbox(lightboxCurrentPost, target); -} - -function lightboxPrev() { - const l = lightboxImages.get(lightboxCurrentPost).length; - const target = modulo(lightboxCurrentIdx - 1, l); - openLightbox(lightboxCurrentPost, target); -} - -function constructLightbox() { - const dialog = document.createElement("dialog"); - dialog.classList.add("lightbox-dialog"); - dialog.addEventListener("click", (e) => { - if (e.target === dialog) { - dialog.close(); - } - }) - const dialogInner = document.createElement("div"); - dialogInner.classList.add("lightbox-inner"); - dialog.appendChild(dialogInner); - const img = document.createElement("img"); - img.classList.add("lightbox-image") - dialogInner.appendChild(img); - const openOriginalAnchor = document.createElement("a") - openOriginalAnchor.text = "Open original in new window" - openOriginalAnchor.target = "_blank" - openOriginalAnchor.rel = "noopener noreferrer nofollow" - dialogInner.appendChild(openOriginalAnchor); - - const navSpan = document.createElement("span"); - navSpan.classList.add("lightbox-nav"); - const prevButton = document.createElement("button"); - prevButton.type = "button"; - prevButton.textContent = "Previous"; - prevButton.addEventListener("click", lightboxPrev); - const nextButton = document.createElement("button"); - nextButton.type = "button"; - nextButton.textContent = "Next"; - nextButton.addEventListener("click", lightboxNext); - const imageCount = document.createElement("span"); - imageCount.textContent = "Image of "; - navSpan.appendChild(prevButton); - navSpan.appendChild(imageCount); - navSpan.appendChild(nextButton); - - dialogInner.appendChild(navSpan); - return { - img: img, - dialog: dialog, - openOriginalAnchor: openOriginalAnchor, - prevButton: prevButton, - nextButton: nextButton, - imageCount: imageCount, - } -} - -let lightboxImages = new Map(); //.post-inner : Array -let lightboxObj = null; -let lightboxCurrentPost = null; -let lightboxCurrentIdx = -1; - -document.addEventListener("DOMContentLoaded", () => { - //lightboxes - lightboxObj = constructLightbox(); - document.body.appendChild(lightboxObj.dialog); - - function setImageMaxSize(img) { - const { - maxWidth: origMaxWidth, - maxHeight: origMaxHeight, - minWidth: origMinWidth, - minHeight: origMinHeight, - } = getComputedStyle(img); - if (img.naturalWidth < parseInt(origMinWidth)) { - img.style.minWidth = img.naturalWidth + "px"; - } - if (img.naturalHeight < parseInt(origMinHeight)) { - img.style.minHeight = img.naturalHeight + "px"; - } - if (img.naturalWidth < parseInt(origMaxWidth)) { - img.style.maxWidth = img.naturalWidth + "px"; - } - if (img.naturalHeight < parseInt(origMaxHeight)) { - img.style.maxHeight = img.naturalHeight + "px"; - } - } - - const postImages = document.querySelectorAll(".post-inner img.post-image"); - postImages.forEach(postImage => { - const belongingTo = postImage.closest(".post-inner"); - const images = lightboxImages.get(belongingTo) ?? []; - images.push({ - src: postImage.src, - alt: postImage.alt, - }); - const idx = images.length - 1; - lightboxImages.set(belongingTo, images); - postImage.style.cursor = "pointer"; - postImage.addEventListener("click", () => { - openLightbox(belongingTo, idx); - }); - }); - const postAndSigImages = document.querySelectorAll("img.post-image"); - postAndSigImages.forEach(image => { - if (image.complete) { - setImageMaxSize(image); - } else { - image.addEventListener("load", () => setImageMaxSize(image)); - } - }) -}); - - -{ - function isBefore(el1, el2) { - if (el2.parentNode === el1.parentNode) { - for (let cur = el1.previousSibling; cur; cur = cur.previousSibling) { - if (cur === el2) return true; - } - } - return false; - } - - let draggedItem = null; - - function sortableItemDragStart(e, item) { - const box = item.getBoundingClientRect(); - const oX = e.clientX - box.left; - const oY = e.clientY - box.top; - draggedItem = item; - item.classList.add('dragged'); - e.dataTransfer.setDragImage(item, oX, oY); - e.dataTransfer.effectAllowed = 'move'; - } - - function sortableItemDragEnd(e, item) { - draggedItem = null; - item.classList.remove('dragged'); - } - - function sortableItemDragOver(e, item) { - const target = e.target.closest('.sortable-item'); - if (!target || target === draggedItem) { - return; - } - const inSameList = draggedItem.dataset.sortableListKey === target.dataset.sortableListKey; - if (!inSameList) { - return; - } - const targetList = draggedItem.closest('.sortable-list'); - if (isBefore(draggedItem, target)) { - targetList.insertBefore(draggedItem, target); - } else { - targetList.insertBefore(draggedItem, target.nextSibling); - } - } - - const listItemsHandled = new Map(); - - const getListItemsHandled = (list) => { - return listItemsHandled.get(list) || new Set(); - } - - function registerSortableList(list) { - list.querySelectorAll('li:not(.immovable)').forEach(item => { - const listItems = getListItemsHandled(list); - listItems.add(item); - listItemsHandled.set(list, listItems); - const dragger = item.querySelector('.dragger'); - dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, item)}); - dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, item)}); - item.addEventListener('dragover', e => {sortableItemDragOver(e, item)}); - }); - - const obs = new MutationObserver(records => { - for (const mutation of records) { - mutation.addedNodes.forEach(node => { - if (!(node instanceof HTMLElement)) { - return; - } - if (!node.classList.contains('sortable-item')) { - return; - } - const listItems = getListItemsHandled(list) - if (listItems.has(node)) { - return; - } - const dragger = node.querySelector('.dragger'); - dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, node)}); - dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, node)}); - node.addEventListener('dragover', e => {sortableItemDragOver(e, node)}); - listItems.add(node); - listItemsHandled.set(list, listItems); - }); - } - }); - obs.observe(list, {childList: true}); - } - - document.querySelectorAll('.sortable-list').forEach(registerSortableList); - - listsObs = new MutationObserver(records => { - for (const mutation of records) { - mutation.addedNodes.forEach(node => { - if (!(node instanceof HTMLElement)) { - return; - } - if (!node.classList.contains('sortable-list')) { - return; - } - registerSortableList(node); - }) - } - }) - listsObs.observe(document.body, {childList: true, subtree: true}); -} diff --git a/data/static/js/vnd/bitty-7.0.0.js b/data/static/js/vnd/bitty-7.0.0.js deleted file mode 100644 index adfe260..0000000 --- a/data/static/js/vnd/bitty-7.0.0.js +++ /dev/null @@ -1 +0,0 @@ -const t=[7,0,0],e=`bitty-${t[0]}-${t[1]}`;class n extends Error{constructor(t){super(),Error.captureStackTrace&&Error.captureStackTrace(this,n),this.name="BittyError";for(let[e,n]of Object.entries(t))this[e]=n}}class s extends Event{constructor(t,e){super("bittyforward",{bubbles:!0}),this.forwardedEvent=t,this.forwardedSignal=e}}class a extends Event{constructor(t,e){super("bittylocaltrigger",{bubbles:!0}),this.signal=t,this.localId=e}}class r extends Event{constructor(t){super("bittytrigger",{bubbles:!0}),this.signal=t}}function i(t,e){if(void 0!==t.dataset)return void 0!==t.dataset[e]?t.dataset[e]:t.parentNode?i(t.parentNode,e):void 0}class o extends HTMLElement{constructor(){super(),this.config={listeners:["click","input"],license:"CC0",version:t}}async connectedCallback(){this.dataset.bittyid=self.crypto.randomUUID(),this.bittyId=this.dataset.bittyid,await this.makeConnection(),this.conn&&(this.conn.api=this,this.setIds(this),this.handleEventBridge=this.handleEvent.bind(this),this.addEventListeners(),await this.runBittyInit(),await this.runDataInits(),await this.runBittyReady())}addEventListeners(){const t=["bittyforward","bittylocaltrigger","bittytrigger"];this.dataset.listeners?this.trimInput(this.dataset.listeners).forEach(e=>{t.push(e)}):(t.push("click"),t.push("input")),t.forEach(t=>{window.addEventListener(t,t=>{this.handleEventBridge.call(this,t)})})}connectedMoveCallback(){}doSubs(t,e){return e.forEach(e=>{const n=typeof e[1],s=Object.prototype.toString.call(e[1]);if("object"===n&&"[object Array]"===s){const n=e[1].map(t=>{const e=typeof t,n=Object.prototype.toString.call(t);return"object"===e&&"[object DocumentFragment]"===n?[...t.childNodes].map(t=>"[object Text]"===Object.prototype.toString.call(t)?t.wholeText:t.outerHTML).join(""):"object"===e?t.outerHTML:t}).join("");t=t.replaceAll(e[0],n)}else if("object"===n&&"[object DocumentFragment]"===s){const n=[];[...e[1].childNodes].forEach(t=>{"[object Text]"===Object.prototype.toString.call(t)?n.push(t.wholeText):n.push(t.outerHTML)}),t=t.replaceAll(e[0],n.join(""))}else t="object"==typeof e[1]?t.replaceAll(e[0],e[1].outerHTML):t.replaceAll(e[0],e[1])}),t}expandElement(t,e){null!==t&&(e.isTarget=t.target.dataset.bittyid===e.dataset.bittyid,e.isSender=t.sender.dataset.bittyid===e.dataset.bittyid),e.bittyParent=this.getBittyParent(e),e.prop=t=>i.call(null,e,t),e.propToInt=t=>parseInt(i.call(null,e,t)),e.propToFloat=t=>parseFloat(i.call(null,e,t)),e.value&&(e.valueToInt=parseInt(e.value,10),e.valueToFloat=parseFloat(e.value)),e.propMatchesTarget=n=>{const s=i.call(null,t.target,n),a=i.call(null,e,n);return void 0!==s&&void 0!==a&&s===a},e.propMatchesSender=n=>{const s=i.call(null,t.sender,n),a=i.call(null,e,n);return void 0!==s&&void 0!==a&&s===a}}expandEvent(t){t.sender=this.findSender(t.target),t.sender&&t.sender.dataset&&t.sender.dataset.bittyid&&(t.sender.bittyId=t.sender.dataset.bittyid),t.sender.dataset&&t.sender.dataset.send&&(t.sendPayload=t.sender.dataset.send),t.sender&&t.sender.value&&(t.sender.valueToInt=parseInt(t.sender.value,10),t.sender.valueToFloat=parseFloat(t.sender.value)),t.sender&&(t.sender.prop=e=>i.call(null,t.sender,e),t.sender.propToInt=e=>parseInt(i.call(null,t.sender,e),10),t.sender.propToFloat=e=>parseFloat(i.call(null,t.sender,e))),void 0!==t.target.value&&(t.value=t.target.value,t.valueToInt=parseInt(t.target.value,10),t.valueToFloat=parseFloat(event.target.value)),t.bittyId=t.target.dataset.bittyid,t.prop=e=>i.call(null,t.target,e),t.propToInt=e=>parseInt(i.call(null,t.target,e)),t.propToFloat=e=>parseFloat(i.call(null,t.target,e))}findSender(t){return t.dataset&&t.dataset.send||t.dataset&&t.dataset.use?t:t.parentNode?this.findSender(t.parentNode):this}forward(t,e){const n=new s(t,e);this.dispatchEvent(n)}getBittyParent(t){return t.localName.toLowerCase()===e?t:t.parentNode?this.getBittyParent(t.parentNode):this}async getElement(t,e=[],n={}){const s=await this.getHTML(t,e,n,"getElement");if(s.value){return{value:s.value.firstChild}}return s}async getHTML(t,e=[],n={}){const s=await this.getTXT(t,e,n,"getHTML");return s.error?s:{value:this.makeHTML(s.value,e)}}async getJSON(t,e=[],s={}){const a=await this.getTXT(t,e,s,"getJSON");if(a.error)return a;try{const t=JSON.parse(a.value);return{value:t}}catch(t){return{error:new n({type:"parsing"})}}}async getSVG(t,e=[],n={}){const s=await this.getTXT(t,e,n,"getSVG");if(s.error)return s;{const t=document.createElement("template");t.innerHTML=s.value;return{value:t.content.cloneNode(!0).querySelector("svg")}}}async getTXT(t,e=[],s={},a="getTXT"){let r=await fetch(t,s);try{if(r.ok){return{value:this.doSubs(await r.text(),e)}}throw new n({type:"fetching",message:`${a}() returned ${r.status} [${r.statusText}] in:\n${a}(${r.url}, ${JSON.stringify(e)}, ${JSON.stringify(s)})`,statusText:r.statusText,status:r.status,url:r.url,incomingMethod:a,subs:e,options:s})}catch(t){return console.error(`BittyError: ${t.message}`),{error:t}}}async handleEvent(t){if("bittyforward"===t.type){const e=t.forwardedEvent;e.sendPayload=t.forwardedSignal,await this.processEvent(e)}else if(this.expandEvent(t),"bittylocaltrigger"===t.type)t.sendPayload=t.signal,await this.processEvent(t);else if("bittytrigger"===t.type)t.sendPayload=t.signal,await this.processEvent(t);else{if(t.sender.dataset.use){const e=this.trimInput(t.sender.dataset.use);for(let n of e){let e=!1;const s=n.split(":");2===s.length&&"await"===s[0]&&(e=!0,n=s[1]),this.conn[n]&&(this.expandElement(t,t.sender),e?await this.conn[n](t,t.sender):this.conn[n](t,t.sender))}}t.sender.dataset.send&&(t.sendPayload=t.sender.dataset.send,await this.processEvent(t))}}async loadCSS(t,e=[],n={}){const s=await this.getTXT(t,e,n,"loadCSS");if(s.error)return s;{const t=new CSSStyleSheet;return t.replaceSync(s.value),document.adoptedStyleSheets.push(t),{value:s.value}}}localTrigger(t){const e=new a(t,this.bittyId);this.dispatchEvent(e)}async makeConnection(){try{if(this.dataset.connect){let t=this.trimInput(this.dataset.connect);if(void 0!==window[t[0]])this.conn=new window[t[0]];else{if("/"===t[0].substring(0,1)){const e=new URL(window.location.href);t[0]=new URL(t[0],e.origin).toString()}if("http"===t[0].substring(0,4)){const n=await import(t[0]);if(void 0===t[1])try{this.conn=new n.default}catch(t){console.error(`${e} error [${t}] - data-connect="${this.dataset.connect}" failed - Check the file "${this.dataset.connect}" to make sure it has an "export default class {}"`)}else try{this.conn=new n[t[1]]}catch(n){console.error(`${e} error [${n}] - data-connect="${this.dataset.connect}" failed - Check the file "${t[0]}" to make sure it has an "export class ${t[1]} {}"`)}}else console.error(`${e} error: Tried to use 'data-connect="${this.dataset.connect}" which did not match a class on the page which means an attempt to use it as a URL was made. It failed becasue the URL version of 'data-connect' must start with 'http' or '/'. Other URLs are not currently supported`)}}else window.BittyClass?this.conn=new window.BittyClass:console.error(`${e} error: Could not find "window.BittyClass" on the page to connect to (which is needed because there is no "data-connect" attribute).`)}catch(t){console.error(`${e} error: [${t}] - ${this.dataset.connect}`)}}makeElement(t,e=[]){return this.makeHTML(t,e).firstChild}makeHTML(t,e=[]){const n=document.createElement("template");n.innerHTML=this.makeTXT(t,e).trim();const s=n.content.cloneNode(!0);return this.setIds(s),s}makeSVG(t,e=[]){const n=document.createElement("template");n.innerHTML=this.makeTXT(t,e).trim();return n.content.cloneNode(!0).querySelector("svg")}makeTXT(t,e=[]){return this.doSubs(t,e)}async processEvent(t){if(t.localId&&t.localId!==this.bittyId)return null;if(t.sendPayload){const e=this.trimInput(t.sendPayload);for(let n of e){let e=!1;const s=n.split(":");if(2===s.length&&"await"===s[0]&&(e=!0,n=s[1]),this.conn[n]){let s=!1;const a=this.querySelectorAll("[data-receive]");for(let r of a){const a=this.trimInput(r.dataset.receive);for(let i of a){const a=i.split(":");2===a.length&&"await"===a[0]&&(i=a[1]),i===n&&(s=!0,this.expandElement(t,r),e?await this.conn[n](t,r):this.conn[n](t,r))}}!1===s&&(e?await this.conn[n](t,null):this.conn[n](t,null))}}}}async runBittyInit(){"function"==typeof this.conn.bittyInit&&("AsyncFunction"===this.conn.bittyInit[Symbol.toStringTag]?await this.conn.bittyInit():this.conn.bittyInit())}async runBittyReady(){"function"==typeof this.conn.bittyReady&&("AsyncFunction"===this.conn.bittyReady[Symbol.toStringTag]?await this.conn.bittyReady():this.conn.bittyReady())}async runDataInits(){if(this.dataset.init){const t=this.trimInput(this.dataset.init);for(let e of t)"function"==typeof this.conn[e]&&("AsyncFunction"===this.conn[e][Symbol.toStringTag]?await this.conn[e](null,this):this.conn[e](null,this))}for(let t of this.querySelectorAll("[data-init]"))if(t.dataset.init){this.expandElement(null,t);const e=this.trimInput(t.dataset.init);for(let n of e)"function"==typeof this.conn[n]&&("AsyncFunction"===this.conn[n][Symbol.toStringTag]?await this.conn[n](null,t):this.conn[n](null,t))}}setProp(t,e){document.documentElement.style.setProperty(t,e)}setIds(t){t.querySelectorAll("*").forEach(t=>{t.dataset.bittyid||(t.dataset.bittyid=self.crypto.randomUUID()),t.bittyId||(t.bittyId=t.dataset.bittyid)})}trigger(t){const e=new r(t);this.dispatchEvent(e)}trimInput(t){return t.trim().split(/\s+/m).map(t=>t.trim())}async getQuickElement(t,e=[],n={}){const s=await this.getElement(t,e,n);return s.value?s.value:this.makeElement('Error (check console)')}async getQuickHTML(t,e=[],n={}){const s=await this.getElement(t,e,n);return s.value?s.value:this.makeHTML('Error (check console)')}async getQuickJSON(t,e=[],n={}){const s=await this.getJSON(t,e,n);return s.value?s.value:s.error}async getQuickSVG(t,e=[],n={}){const s=await this.getSVG(t,e,n);if(s.value)return s.value;{const t=document.createElement("template");t.innerHTML='error (check console)';return t.content.cloneNode(!0).querySelector("svg")}}async getQuickTXT(t,e=[],n={}){const s=await this.getTXT(t,e,n);return s.value?s.value:s.error}}customElements.define(e,o); \ No newline at end of file