{"version":3,"file":"bundle.js","sources":["../frontend/behaviors/index.js","../frontend/behaviors/bookmark-page.js","../frontend/behaviors/bulk-edit.js","../frontend/behaviors/confirm-button.js","../frontend/behaviors/dropdown.js","../frontend/behaviors/form.js","../frontend/behaviors/modal.js","../../node_modules/svelte/src/runtime/internal/utils.js","../../node_modules/svelte/src/runtime/internal/dom.js","../../node_modules/svelte/src/runtime/internal/lifecycle.js","../frontend/behaviors/global-shortcuts.js","../../node_modules/svelte/src/runtime/internal/scheduler.js","../../node_modules/svelte/src/runtime/internal/transitions.js","../../node_modules/svelte/src/runtime/internal/each.js","../../node_modules/svelte/src/runtime/internal/Component.js","../frontend/util.js","../frontend/components/TagAutocomplete.svelte","../../node_modules/svelte/src/runtime/internal/disclose-version/index.js","../../node_modules/svelte/src/shared/version.js","../frontend/api.js","../frontend/behaviors/tag-autocomplete.js","../frontend/components/SearchHistory.js","../frontend/components/SearchAutoComplete.svelte"],"sourcesContent":["const behaviorRegistry = {};\n\nexport function registerBehavior(name, behavior) {\n behaviorRegistry[name] = behavior;\n applyBehaviors(document, [name]);\n}\n\nexport function applyBehaviors(container, behaviorNames = null) {\n if (!behaviorNames) {\n behaviorNames = Object.keys(behaviorRegistry);\n }\n\n behaviorNames.forEach((behaviorName) => {\n const behavior = behaviorRegistry[behaviorName];\n const elements = Array.from(\n container.querySelectorAll(`[${behaviorName}]`),\n );\n\n // Include the container element if it has the behavior\n if (container.hasAttribute && container.hasAttribute(behaviorName)) {\n elements.push(container);\n }\n\n elements.forEach((element) => {\n element.__behaviors = element.__behaviors || [];\n const hasBehavior = element.__behaviors.some(\n (b) => b instanceof behavior,\n );\n\n if (hasBehavior) {\n return;\n }\n\n const behaviorInstance = new behavior(element);\n element.__behaviors.push(behaviorInstance);\n });\n });\n}\n\nexport function swap(element, html) {\n const dom = new DOMParser().parseFromString(html, \"text/html\");\n const newElement = dom.body.firstChild;\n element.replaceWith(newElement);\n applyBehaviors(newElement);\n}\n\nexport function swapContent(element, html) {\n element.innerHTML = html;\n applyBehaviors(element);\n}\n","import { registerBehavior, swapContent } from \"./index\";\n\nclass BookmarkPage {\n constructor(element) {\n this.element = element;\n this.form = element.querySelector(\"form.bookmark-actions\");\n this.form.addEventListener(\"submit\", this.onFormSubmit.bind(this));\n\n this.bookmarkList = element.querySelector(\".bookmark-list-container\");\n this.tagCloud = element.querySelector(\".tag-cloud-container\");\n\n document.addEventListener(\"bookmark-page-refresh\", () => {\n this.refresh();\n });\n }\n\n async onFormSubmit(event) {\n event.preventDefault();\n\n const url = this.form.action;\n const formData = new FormData(this.form);\n formData.append(event.submitter.name, event.submitter.value);\n\n await fetch(url, {\n method: \"POST\",\n body: formData,\n redirect: \"manual\", // ignore redirect\n });\n await this.refresh();\n }\n\n async refresh() {\n const query = window.location.search;\n const bookmarksUrl = this.element.getAttribute(\"bookmarks-url\");\n const tagsUrl = this.element.getAttribute(\"tags-url\");\n Promise.all([\n fetch(`${bookmarksUrl}${query}`).then((response) => response.text()),\n fetch(`${tagsUrl}${query}`).then((response) => response.text()),\n ]).then(([bookmarkListHtml, tagCloudHtml]) => {\n swapContent(this.bookmarkList, bookmarkListHtml);\n swapContent(this.tagCloud, tagCloudHtml);\n\n // Dispatch list updated event\n const listElement = this.bookmarkList.querySelector(\n \"ul[data-bookmarks-total]\",\n );\n const bookmarksTotal =\n (listElement && listElement.dataset.bookmarksTotal) || 0;\n\n this.bookmarkList.dispatchEvent(\n new CustomEvent(\"bookmark-list-updated\", {\n bubbles: true,\n detail: { bookmarksTotal },\n }),\n );\n });\n }\n}\n\nregisterBehavior(\"ld-bookmark-page\", BookmarkPage);\n\nclass BookmarkItem {\n constructor(element) {\n this.element = element;\n\n // Toggle notes\n const notesToggle = element.querySelector(\".toggle-notes\");\n if (notesToggle) {\n notesToggle.addEventListener(\"click\", this.onToggleNotes.bind(this));\n }\n\n // Add tooltip to title if it is truncated\n const titleAnchor = element.querySelector(\".title > a\");\n const titleSpan = titleAnchor.querySelector(\"span\");\n if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {\n titleAnchor.dataset.tooltip = titleSpan.textContent;\n }\n }\n\n onToggleNotes(event) {\n event.preventDefault();\n event.stopPropagation();\n this.element.classList.toggle(\"show-notes\");\n }\n}\n\nregisterBehavior(\"ld-bookmark-item\", BookmarkItem);\n","import { registerBehavior } from \"./index\";\n\nclass BulkEdit {\n constructor(element) {\n this.element = element;\n this.active = false;\n this.actionSelect = element.querySelector(\"select[name='bulk_action']\");\n this.tagAutoComplete = element.querySelector(\".tag-autocomplete\");\n this.selectAcross = element.querySelector(\"label.select-across\");\n\n element.addEventListener(\n \"bulk-edit-toggle-active\",\n this.onToggleActive.bind(this),\n );\n element.addEventListener(\n \"bulk-edit-toggle-all\",\n this.onToggleAll.bind(this),\n );\n element.addEventListener(\n \"bulk-edit-toggle-bookmark\",\n this.onToggleBookmark.bind(this),\n );\n element.addEventListener(\n \"bookmark-list-updated\",\n this.onListUpdated.bind(this),\n );\n\n this.actionSelect.addEventListener(\n \"change\",\n this.onActionSelected.bind(this),\n );\n }\n\n get allCheckbox() {\n return this.element.querySelector(\"[ld-bulk-edit-checkbox][all] input\");\n }\n\n get bookmarkCheckboxes() {\n return [\n ...this.element.querySelectorAll(\n \"[ld-bulk-edit-checkbox]:not([all]) input\",\n ),\n ];\n }\n\n onToggleActive() {\n this.active = !this.active;\n if (this.active) {\n this.element.classList.add(\"active\", \"activating\");\n setTimeout(() => {\n this.element.classList.remove(\"activating\");\n }, 500);\n } else {\n this.element.classList.remove(\"active\");\n }\n }\n\n onToggleBookmark() {\n const allChecked = this.bookmarkCheckboxes.every((checkbox) => {\n return checkbox.checked;\n });\n this.allCheckbox.checked = allChecked;\n this.updateSelectAcross(allChecked);\n }\n\n onToggleAll() {\n const allChecked = this.allCheckbox.checked;\n this.bookmarkCheckboxes.forEach((checkbox) => {\n checkbox.checked = allChecked;\n });\n this.updateSelectAcross(allChecked);\n }\n\n onActionSelected() {\n const action = this.actionSelect.value;\n\n if (action === \"bulk_tag\" || action === \"bulk_untag\") {\n this.tagAutoComplete.classList.remove(\"d-none\");\n } else {\n this.tagAutoComplete.classList.add(\"d-none\");\n }\n }\n\n onListUpdated(event) {\n // Reset checkbox states\n this.reset();\n\n // Update total number of bookmarks\n const total = event.detail.bookmarksTotal;\n const totalSpan = this.selectAcross.querySelector(\"span.total\");\n totalSpan.textContent = total;\n }\n\n updateSelectAcross(allChecked) {\n if (allChecked) {\n this.selectAcross.classList.remove(\"d-none\");\n } else {\n this.selectAcross.classList.add(\"d-none\");\n this.selectAcross.querySelector(\"input\").checked = false;\n }\n }\n\n reset() {\n this.allCheckbox.checked = false;\n this.bookmarkCheckboxes.forEach((checkbox) => {\n checkbox.checked = false;\n });\n this.updateSelectAcross(false);\n }\n}\n\nclass BulkEditActiveToggle {\n constructor(element) {\n this.element = element;\n element.addEventListener(\"click\", this.onClick.bind(this));\n }\n\n onClick() {\n this.element.dispatchEvent(\n new CustomEvent(\"bulk-edit-toggle-active\", { bubbles: true }),\n );\n }\n}\n\nclass BulkEditCheckbox {\n constructor(element) {\n this.element = element;\n element.addEventListener(\"change\", this.onChange.bind(this));\n }\n\n onChange() {\n const type = this.element.hasAttribute(\"all\") ? \"all\" : \"bookmark\";\n this.element.dispatchEvent(\n new CustomEvent(`bulk-edit-toggle-${type}`, { bubbles: true }),\n );\n }\n}\n\nregisterBehavior(\"ld-bulk-edit\", BulkEdit);\nregisterBehavior(\"ld-bulk-edit-active-toggle\", BulkEditActiveToggle);\nregisterBehavior(\"ld-bulk-edit-checkbox\", BulkEditCheckbox);\n","import { registerBehavior } from \"./index\";\n\nclass ConfirmButtonBehavior {\n constructor(element) {\n const button = element;\n button.dataset.type = button.type;\n button.dataset.name = button.name;\n button.dataset.value = button.value;\n button.removeAttribute(\"type\");\n button.removeAttribute(\"name\");\n button.removeAttribute(\"value\");\n button.addEventListener(\"click\", this.onClick.bind(this));\n this.button = button;\n }\n\n onClick(event) {\n event.preventDefault();\n\n const container = document.createElement(\"span\");\n container.className = \"confirmation\";\n\n const icon = this.button.getAttribute(\"confirm-icon\");\n if (icon) {\n const iconElement = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"svg\",\n );\n iconElement.style.width = \"16px\";\n iconElement.style.height = \"16px\";\n iconElement.innerHTML = ``;\n container.append(iconElement);\n }\n\n const question = this.button.getAttribute(\"confirm-question\");\n if (question) {\n const questionElement = document.createElement(\"span\");\n questionElement.innerText = question;\n container.append(question);\n }\n\n const buttonClasses = Array.from(this.button.classList.values())\n .filter((cls) => cls.startsWith(\"btn\"))\n .join(\" \");\n\n const cancelButton = document.createElement(this.button.nodeName);\n cancelButton.type = \"button\";\n cancelButton.innerText = question ? \"No\" : \"Cancel\";\n cancelButton.className = `${buttonClasses} mr-1`;\n cancelButton.addEventListener(\"click\", this.reset.bind(this));\n\n const confirmButton = document.createElement(this.button.nodeName);\n confirmButton.type = this.button.dataset.type;\n confirmButton.name = this.button.dataset.name;\n confirmButton.value = this.button.dataset.value;\n confirmButton.innerText = question ? \"Yes\" : \"Confirm\";\n confirmButton.className = buttonClasses;\n confirmButton.addEventListener(\"click\", this.reset.bind(this));\n\n container.append(cancelButton, confirmButton);\n this.container = container;\n\n this.button.before(container);\n this.button.classList.add(\"d-none\");\n }\n\n reset() {\n setTimeout(() => {\n this.container.remove();\n this.button.classList.remove(\"d-none\");\n });\n }\n}\n\nregisterBehavior(\"ld-confirm-button\", ConfirmButtonBehavior);\n","import { registerBehavior } from \"./index\";\n\nclass DropdownBehavior {\n constructor(element) {\n this.element = element;\n this.opened = false;\n this.onOutsideClick = this.onOutsideClick.bind(this);\n\n const toggle = element.querySelector(\".dropdown-toggle\");\n toggle.addEventListener(\"click\", () => {\n if (this.opened) {\n this.close();\n } else {\n this.open();\n }\n });\n }\n\n open() {\n this.element.classList.add(\"active\");\n document.addEventListener(\"click\", this.onOutsideClick);\n }\n\n close() {\n this.element.classList.remove(\"active\");\n document.removeEventListener(\"click\", this.onOutsideClick);\n }\n\n onOutsideClick(event) {\n if (!this.element.contains(event.target)) {\n this.close();\n }\n }\n}\n\nregisterBehavior(\"ld-dropdown\", DropdownBehavior);\n","import { registerBehavior, swap } from \"./index\";\n\nclass FormBehavior {\n constructor(element) {\n this.element = element;\n element.addEventListener(\"submit\", this.onFormSubmit.bind(this));\n }\n\n async onFormSubmit(event) {\n event.preventDefault();\n\n const url = this.element.action;\n const formData = new FormData(this.element);\n if (event.submitter) {\n formData.append(event.submitter.name, event.submitter.value);\n }\n\n await fetch(url, {\n method: \"POST\",\n body: formData,\n redirect: \"manual\", // ignore redirect\n });\n\n // Dispatch refresh events\n const refreshEvents = this.element.getAttribute(\"refresh-events\");\n if (refreshEvents) {\n refreshEvents.split(\",\").forEach((eventName) => {\n document.dispatchEvent(new CustomEvent(eventName));\n });\n }\n\n // Refresh form\n await this.refresh();\n }\n\n async refresh() {\n const refreshUrl = this.element.getAttribute(\"refresh-url\");\n const html = await fetch(refreshUrl).then((response) => response.text());\n swap(this.element, html);\n }\n}\n\nclass FormAutoSubmitBehavior {\n constructor(element) {\n this.element = element;\n this.element.addEventListener(\"change\", () => {\n const form = this.element.closest(\"form\");\n form.dispatchEvent(new Event(\"submit\", { cancelable: true }));\n });\n }\n}\n\nregisterBehavior(\"ld-form\", FormBehavior);\nregisterBehavior(\"ld-form-auto-submit\", FormAutoSubmitBehavior);\n","import { applyBehaviors, registerBehavior } from \"./index\";\n\nclass ModalBehavior {\n constructor(element) {\n const toggle = element;\n toggle.addEventListener(\"click\", this.onToggleClick.bind(this));\n this.toggle = toggle;\n }\n\n async onToggleClick(event) {\n // Ignore Ctrl + click\n if (event.ctrlKey || event.metaKey) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n // Create modal either by teleporting existing content or fetching from URL\n const modal = this.toggle.hasAttribute(\"modal-content\")\n ? this.createFromContent()\n : await this.createFromUrl();\n\n if (!modal) {\n return;\n }\n\n // Register close handlers\n const modalOverlay = modal.querySelector(\".modal-overlay\");\n const closeButton = modal.querySelector(\"button.close\");\n modalOverlay.addEventListener(\"click\", this.onClose.bind(this));\n closeButton.addEventListener(\"click\", this.onClose.bind(this));\n\n document.body.append(modal);\n applyBehaviors(document.body);\n this.modal = modal;\n }\n\n async createFromUrl() {\n const url = this.toggle.getAttribute(\"modal-url\");\n const modalHtml = await fetch(url).then((response) => response.text());\n const parser = new DOMParser();\n const doc = parser.parseFromString(modalHtml, \"text/html\");\n return doc.querySelector(\".modal\");\n }\n\n createFromContent() {\n const contentSelector = this.toggle.getAttribute(\"modal-content\");\n const content = document.querySelector(contentSelector);\n if (!content) {\n return;\n }\n\n // Todo: make title configurable, only used for tag cloud for now\n const modal = document.createElement(\"div\");\n modal.classList.add(\"modal\", \"active\");\n modal.innerHTML = `\n
\n