export const b = { babycodePreviewEndpoint: '/api/babycode-preview/', bookmarksCollectionEndpoint: '/hyperapi/bookmarks/dropdown/', init: 'babycodeEditorCharCountInit localizeTimestamps', bookmarkState: {}, } const getThreadId = () => { const scheme = window.location.pathname.split("/"); if (scheme[1] !== 'threads' || scheme[2] === 'new') { return -1; } return parseInt(scheme[2]); } async function getHTML(endpoint, options = {}) { let query = {}; if (options._query !== undefined) { query = options._query; delete options._query; } const params = new URLSearchParams(query); const res = await fetch(`${endpoint}?${params}`, options); if (!res.ok) { console.error(res); } return { body: await res.text(), status: res.status }; } export function setTab(_, sender, el) { if (sender.ariaSelected === 'true') { return; } if (!el.contains(sender)) { return; } const tabIndex = parseInt(sender.dataset.tabIndex); const tabPanels = el.querySelectorAll('.tab-content'); const tabButtons = el.querySelectorAll('.tab-bar button'); for (let i = 0; i < tabPanels.length; i++) { const tabPanel = tabPanels[i]; const tabButton = tabButtons[i]; if (i === tabIndex) { tabPanel.classList.remove('hidden'); tabButton.ariaSelected = 'true'; } else if (!tabPanel.classList.contains('hidden')) { tabPanel.classList.add('hidden'); tabButton.ariaSelected = 'false'; } } } export function insertBabycode(_, sender, el) { if (!el.parentNode.contains(sender)) { return; } const tagStart = sender.dataset.babycodeTag; const breakLine = 'breakLine' in sender.dataset; const prefill = 'prefill' in sender.dataset ? 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.substring(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(); b.send({ sender: el }, 'babycodeEditorCharCount'); } export function babycodeEditorCharCount(evOrPayload, sender, el) { if (!sender) { // sent from bitty, not input sender = evOrPayload.sender; } if (!sender.parentNode.contains(el)) { return; } const maxLength = sender.maxLength; const currentLength = sender.value.length; el.innerText = `${currentLength}/${maxLength}`; const threadId = getThreadId(); if (threadId !== -1) { localStorage.setItem(`thread-${threadId}`, sender.value); } } export function clearThreadDraft(_, __, ___) { const threadId = getThreadId(); localStorage.removeItem(`thread-${threadId}`); } export function babycodeEditorCharCountInit(_, __, el) { if (el === undefined) { // no editors on page return; } const threadId = getThreadId(); if (threadId !== -1) { el.value = localStorage.getItem(`thread-${threadId}`) || ''; } b.send({ sender: el }, 'babycodeEditorCharCount'); } export function babycodePreviewInit(ev, sender, el) { if (!sender.parentNode.parentNode.contains(el)) { // tab container > tab bar > button return; } b.send({ text: el.value, sender: sender, bannedTags: JSON.parse(el.dataset.bannedTags) }, 'babycodePreview'); } export async function babycodePreview(payload, _, el) { if (!payload.sender.parentNode.parentNode.contains(el)) { return; } if (!payload.text.trim()) { b.send({ plain: 'Type something to get a preview.', sender: el }, 'showBabycodePreview'); return; } const options = { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify({ markup: payload.text, banned_tags: payload.bannedTags, }), } const f = await fetch(b.babycodePreviewEndpoint, options); try { if (!f.ok) { console.error(f); let msg = ''; switch (f.status) { case 429: return; default: msg = '(Something went wrong. Try again later.)' } b.send({ plain: msg, sender: el }, 'showBabycodePreview'); return; } b.send({ ...(await f.json()), sender: el }, 'showBabycodePreview'); } catch (error) { b.send({ plain: '(Something went wrong. Try again later.)', sender: el }, 'showBabycodePreview'); console.error(error); return; } } export function showBabycodePreview(payload, _, el) { if (!payload.sender.parentNode.contains(el)) { return; } if (payload.plain) { el.innerHTML = `

${payload.plain}

`; } else { el.innerHTML = payload.html; } } export function babycodeEditorQuote(ev, sender, el) { console.log(sender.dataset.quote); const newline = el.value.length === 0 ? '' : '\n' el.value += `${newline}[quote=${sender.dataset.posterName}]\n${sender.dataset.quote}\n[/quote]\n\n` b.send({ sender: el }, 'babycodeEditorCharCount'); el.focus(); } export async function copyCode(ev, sender, el) { if (!el.contains(sender)) { return; } const originalText = sender.textContent; const doneText = 'Copied!'; const code = el.dataset.code; try { await navigator.clipboard.writeText(code); sender.textContent = doneText; setTimeout(() => { sender.textContent = originalText }, 2000); } catch (error) { console.log(error); } } export function localizeTimestamps(_, __, el) { if (el === undefined) { return; } const d = new Date(el.dateTime); el.innerText = d.toLocaleString(); } export async function showBookmarkMenu(ev, sender, el) { if (b.bookmarkState.state === undefined) { el.addEventListener('toggle', e => { if (e.newState === 'closed') { b.bookmarkState.state = 'closed'; } }); } // dismiss if open and last invoker is the same button that opened it if (b.bookmarkState.state === 'open' && b.bookmarkState.invoker === sender) { el.hidePopover(); return; } b.bookmarkState.invoker = sender; b.bookmarkState.state = 'open'; b.send({ 'plain': 'Loading…' }, 'fillBookmarkMenu'); el.showPopover(); const bRect = sender.getBoundingClientRect(); const menuRect = el.getBoundingClientRect(); const preferredLeft = bRect.right - menuRect.width; const enoughSpace = preferredLeft >= 0; const scrollY = window.scrollY; if (enoughSpace) { el.style.left = `${preferredLeft}px`; } else { el.style.left = `${bRect.left}px`; } el.style.top = `${bRect.bottom + scrollY}px`; const conceptKind = sender.prop('conceptKind'); const conceptId = sender.prop('conceptId'); const bookmarkCollections = await getHTML(b.bookmarksCollectionEndpoint, { _query: { concept_kind: conceptKind, concept_id: conceptId, } }); b.send({ 'html': bookmarkCollections.body }, 'fillBookmarkMenu'); } export function fillBookmarkMenu(payload, __, el) { if (payload.plain) { el.innerText = payload.plain; return; } el.innerHTML = payload.html; } export async function bookmarkMenuSubmit(ev, _, el) { ev.preventDefault(); const url = el.action; const body = new URLSearchParams(new FormData(el)); const options = { body: body, method: 'POST' }; const status = (await getHTML(url, options)).status; if (status !== 204) { b.trigger('bookmarkMenuShowError'); return; } b.trigger('bookmarkMenuShowSavedButton'); } export function bookmarkMenuShowSavedButton(_, __, el) { el.value = 'Saved!'; } export function bookmarkMenuResetSavedButton(_, __, el) { el.value = 'Save'; } export function bookmarkMenuShowError(_, __, el) { if (el === undefined) { return; } if (el.classList.contains('hidden')) { el.classList.remove('hidden'); setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000); } } export function bookmarkMenuHideError(_, __, el) { if (el === undefined) { return; } if (!el.classList.contains('hidden')) { el.classList.add('hidden'); } }