const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown'; const previewEndpoint = '/api/babycode-preview'; const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))} export default class { async showBookmarkMenu(ev, el) { if ((ev.sender.dataset.bookmarkId === el.getString('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.getString('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.getString('originallyContainedIn'); } const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', }, } const requireReload = el.getInt('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.getString('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.getString('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.getInt('utc'); if (!isNaN(timestamp)) { const date = new Date(timestamp * 1000); el.textContent = date.toLocaleString(); } } }