diff --git a/.github/workflows/publish-to-s3.yml b/.github/workflows/publish-to-s3.yml new file mode 100644 index 0000000..56f48a8 --- /dev/null +++ b/.github/workflows/publish-to-s3.yml @@ -0,0 +1,74 @@ +name: Publish components to S3 + +# Uploads the built component bundles and stylesheets to the S3 bucket named by +# the AWS_BUCKET_NAME secret, then invalidates the CloudFront paths for this +# project so the change is visible. +# +# The bucket is a secret so it is not written down here. public-base-url below +# is not the bucket: it is where a browser reaches the files, which is public +# already. The two happen to resemble each other today and need not on the next +# bucket, so do not infer one from the other. +# +# Replaces publish_to_s3.bash and invalidate_cdn.bash, which needed an +# uncommitted media.env to exist on someone's machine and invalidated /* rather +# than this project's own prefix. +# +# Authentication is OIDC, so no long-lived AWS keys exist anywhere. The role's +# trust policy names this repository's subject claim: +# +# repo:caltechlibrary@15038637/CL-web-components@943425900:ref:refs/heads/main +# +# Run it with dry_run first. It reports what would be uploaded and invalidated +# without changing anything. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + dry_run: + description: "Report what would happen without uploading or invalidating" + type: boolean + default: true + +run-name: >- + Publish to S3${{ github.event_name == 'workflow_dispatch' && inputs.dry_run + && ' (dry run)' || '' }} + +permissions: + contents: read + # Required for OIDC. A composite action cannot declare this. + id-token: write + +concurrency: + group: publish-to-s3 + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Build the bundles + run: deno task build + + - uses: caltechlibrary/workflows/.github/actions/publish-to-s3@v1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME_ARN }} + bucket: ${{ secrets.AWS_BUCKET_NAME }} + distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} + prefix: cl-webcomponents + public-base-url: https://media.library.caltech.edu/cl-webcomponents/ + # The bucket requires per-object ACLs. Without this, objects + # upload successfully and are unreadable. + acl: public-read + # Just dist. Component stylesheets build into it alongside the + # bundles, and the docs theme's CSS is no longer this project's + # to publish. + sources: dist + dry-run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} diff --git a/card-layout.js b/card-layout.js deleted file mode 100644 index 642b700..0000000 --- a/card-layout.js +++ /dev/null @@ -1,254 +0,0 @@ -// src/card-layout.js -if (!document.getElementById("hind-fonts-card")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-card"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var CardLayout = class extends HTMLElement { - static get observedAttributes() { - return ["layout", "count"]; - } - constructor() { - super(); - const shadow = this.attachShadow({ mode: "open" }); - const template = document.createElement("template"); - template.innerHTML = ` - - -
- `; - shadow.appendChild(template.content.cloneNode(true)); - } - connectedCallback() { - this.applyLayout(); - this.renderCards(); - } - attributeChangedCallback(name, oldValue, newValue) { - if (oldValue === newValue) return; - if (name === "layout") this.applyLayout(); - if (name === "count") this.renderCards(); - } - renderCards() { - const container = this.shadowRoot.querySelector(".cards-container"); - if (!container) return; - const count = parseInt(this.getAttribute("count"), 10) || 3; - container.innerHTML = ""; - for (let i = 1; i <= count; i++) { - const card = document.createElement("div"); - card.className = "card"; - card.id = `card${i}`; - card.innerHTML = ` -
Image
-
-

Card ${i}

-

Add your content here.

- Read more -
- `; - container.appendChild(card); - this.setupCard(i); - } - } - applyLayout() { - const container = this.shadowRoot.querySelector(".cards-container"); - if (!container) return; - const layout = this.getAttribute("layout") || "horizontal"; - if (layout === "vertical") { - container.classList.add("vertical"); - } else { - container.classList.remove("vertical"); - } - } - setupCard(cardNum) { - const card = this.shadowRoot.getElementById(`card${cardNum}`); - if (!card) return; - const prefix = `card${cardNum}`; - const imageUrl = this.getAttribute(`${prefix}-image`); - const imageContainer = card.querySelector(".card-image-placeholder, .card-image"); - if (imageUrl && imageContainer) { - const img = document.createElement("img"); - img.src = imageUrl; - img.alt = this.getAttribute(`${prefix}-alt`) || this.getAttribute(`${prefix}-title`) || `Card ${cardNum}`; - img.className = "card-image"; - imageContainer.replaceWith(img); - } - const title = this.getAttribute(`${prefix}-title`); - const titleEl = card.querySelector(".card-title"); - if (title && titleEl) { - titleEl.textContent = title; - } - const description = this.getAttribute(`${prefix}-description`); - const descEl = card.querySelector(".card-description"); - if (description && descEl) { - descEl.textContent = description; - } - const link = this.getAttribute(`${prefix}-link`); - const linkText = this.getAttribute(`${prefix}-link-text`) || "Read more"; - const linkEl = card.querySelector(".card-link"); - if (linkEl) { - if (link) { - linkEl.href = link; - linkEl.textContent = linkText; - } else { - linkEl.style.display = "none"; - } - } - } -}; -customElements.define("card-layout", CardLayout); -export { - CardLayout -}; diff --git a/cl-web-components.js b/cl-web-components.js deleted file mode 100644 index 4f39131..0000000 --- a/cl-web-components.js +++ /dev/null @@ -1,3113 +0,0 @@ -// src/version.js -var version = "0.0.12"; -var releaseDate = "2025-07-17"; -var releaseHash = "9729d57"; -var licenseText = ` - -Copyright (c) 2025, Caltech -All rights not granted herein are expressly reserved by Caltech. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -`; - -// src/ul-a-to-z-list.js -var ULAtoZList = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ - mode: "open" - }); - } - connectedCallback() { - this.render(); - this.setupMutationObserver(); - } - setupMutationObserver() { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - this.render(); - } - }); - }); - observer.observe(this, { - attributes: false, - childList: true, - subtree: true - }); - } - render() { - const template = document.createElement("template"); - template.innerHTML = ` - - -
- ${this.hasAttribute("long") ? 'Back to Menu' : ""} - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - const listContainer = this.shadowRoot.querySelector("#list-container"); - const menu = this.shadowRoot.querySelector("#menu"); - const ulElement = this.querySelector("ul"); - if (!ulElement) return; - const items = Array.from(ulElement.querySelectorAll("li")); - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const sections = {}; - items.forEach((item) => { - const firstLetter = item.textContent.trim()[0].toUpperCase(); - if (!sections[firstLetter]) { - sections[firstLetter] = []; - } - sections[firstLetter].push(item); - }); - alphabet.split("").forEach((letter) => { - if (sections[letter]) { - const menuItem = document.createElement("li"); - const menuLink = document.createElement("a"); - menuLink.href = `#section-${letter}`; - menuLink.textContent = letter; - menuLink.addEventListener("click", (event) => { - event.preventDefault(); - const targetSection = this.shadowRoot.querySelector(`#section-${letter}`); - this.scrollToSection(targetSection); - }); - menuItem.appendChild(menuLink); - menu.appendChild(menuItem); - const section = document.createElement("ul"); - section.classList.add("letter-section"); - section.id = `section-${letter}`; - const sectionHeading = document.createElement("li"); - const sectionHeadingLink = document.createElement("a"); - sectionHeadingLink.href = `#menu`; - sectionHeadingLink.textContent = letter; - sectionHeadingLink.addEventListener("click", (event) => { - event.preventDefault(); - this.scrollToSection(menu); - }); - sectionHeading.appendChild(sectionHeadingLink); - section.appendChild(sectionHeading); - sections[letter].forEach((item) => { - const clonedItem = item.cloneNode(true); - section.appendChild(clonedItem); - }); - listContainer.appendChild(section); - } - }); - const backToMenuLink = this.shadowRoot.querySelector(".back-to-menu"); - if (backToMenuLink) { - backToMenuLink.addEventListener("click", (event) => { - event.preventDefault(); - this.scrollToSection(menu); - }); - } - } - scrollToSection(section) { - const yOffset = -100; - const y = section.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ - top: y, - behavior: "smooth" - }); - } -}; -customElements.define("ul-a-to-z-list", ULAtoZList); - -// src/textarea-agent-list.js -var TextareaAgentList = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ - mode: "open" - }); - } - connectedCallback() { - this.loadStyles(); - this.renderContent(); - this.renderList(); - this.setupMutationObserver(); - } - loadStyles() { - const cssHref = this.getAttribute("css-href"); - if (cssHref) { - const link = document.createElement("link"); - link.setAttribute("rel", "stylesheet"); - link.setAttribute("href", cssHref); - this.shadowRoot.appendChild(link); - } else { - const style = document.createElement("style"); - style.textContent = ` - ul { - list-style-type: none; - padding: 0; - } - li { - padding: 8px; - margin-bottom: 5px; - background-color: #f4f4f4; - border-radius: 4px; - display: flex; - justify-content: space-between; - align-items: center; - } - input { - width: 100%; - padding: 8px; - margin: 4px 0; - box-sizing: border-box; - } - button { - margin: 4px; - padding: 8px; - cursor: pointer; - } - `; - this.shadowRoot.appendChild(style); - } - } - renderContent() { - const container = document.createElement("div"); - const styleAttribute = this.getAttribute("style"); - if (styleAttribute) { - container.setAttribute("style", styleAttribute); - } - const personOnly = this.hasAttribute("people-only"); - const organizationOnly = this.hasAttribute("organization-only"); - let buttonsHTML = ""; - if (!personOnly) { - buttonsHTML += ``; - } - if (!organizationOnly) { - buttonsHTML += ``; - } - container.innerHTML = ` - - ${buttonsHTML} - `; - this.shadowRoot.appendChild(container); - this.agentList = this.shadowRoot.querySelector("#agentList"); - if (!personOnly) { - const addOrganizationButton = this.shadowRoot.getElementById("addOrganization"); - if (addOrganizationButton) { - addOrganizationButton.addEventListener("click", () => this.addAgent("organization")); - } - } - if (!organizationOnly) { - const addPersonButton = this.shadowRoot.getElementById("addPerson"); - if (addPersonButton) { - addPersonButton.addEventListener("click", () => this.addAgent("person")); - } - } - } - renderList() { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - this.agentList.innerHTML = ""; - const personOnly = this.hasAttribute("person-only"); - const organizationOnly = this.hasAttribute("organization-only"); - parsedContent.forEach((agent, index) => { - const li = document.createElement("li"); - const styleAttribute = this.getAttribute("style"); - if (styleAttribute) { - li.setAttribute("style", styleAttribute); - } - if (!organizationOnly && (agent.family_name !== void 0 || agent.given_name !== void 0 || agent.orcid !== void 0)) { - li.innerHTML = ` -
- - - -
- - `; - } else if (!personOnly && (agent.name !== void 0 || agent.ror !== void 0)) { - li.innerHTML = ` -
- - -
- - `; - } - this.agentList.appendChild(li); - }); - this.setupEventListeners(); - } catch (e) { - console.error("Error parsing JSON:", e); - } - } - } - setupMutationObserver() { - const textarea = this.querySelector("textarea"); - if (textarea) { - this.handleJsonChange({ - target: textarea - }); - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "characterData") { - this.handleJsonChange({ - target: textarea - }); - } - }); - }); - const config = { - characterData: true, - childList: false, - subtree: true - }; - observer.observe(textarea, config); - } - } - handleJsonChange(event) { - if (event.target.value !== "") { - try { - const jsonContent = event.target.value; - const parsedContent = JSON.parse(jsonContent); - console.log("Parsed JSON:", parsedContent); - } catch (e) { - console.error("Error parsing JSON:", e); - } - } - } - setupEventListeners() { - this.shadowRoot.querySelectorAll("input").forEach((input) => { - input.addEventListener("change", this.handleInputChange.bind(this)); - input.addEventListener("input", this.validateInput.bind(this)); - }); - this.shadowRoot.querySelectorAll(".removeAgent").forEach((button) => { - button.addEventListener("click", this.handleRemoveAgent.bind(this)); - }); - } - validateInput(event) { - const input = event.target; - const isValid = input.checkValidity(); - if (input.hasAttribute("pattern")) { - if (isValid) { - input.style.borderColor = "green"; - } else { - input.style.borderColor = "red"; - } - } - } - handleInputChange(event) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - const index = event.target.getAttribute("data-index"); - const field = event.target.getAttribute("data-field"); - parsedContent[index][field] = event.target.value; - textarea.value = JSON.stringify(parsedContent, null, 2); - } catch (e) { - console.error("Error updating JSON:", e); - } - } - } - handleRemoveAgent(event) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const index = event.target.getAttribute("data-index"); - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - parsedContent.splice(index, 1); - textarea.value = JSON.stringify(parsedContent, null, 2); - this.renderList(); - } catch (e) { - console.error("Error removing agent:", e); - } - } - } - addAgent(type) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - const personOnly = this.hasAttribute("people-only"); - const organizationOnly = this.hasAttribute("organization-only"); - if (!organizationOnly && type === "person" || personOnly) { - parsedContent.push({ - family_name: "", - given_name: "", - orcid: "" - }); - } else if (!personOnly && type === "organization" || organizationOnly) { - parsedContent.push({ - name: "", - ror: "" - }); - } - textarea.value = JSON.stringify(parsedContent, null, 2); - this.renderList(); - } catch (e) { - console.error("Error adding agent:", e); - } - } - } -}; -customElements.define("textarea-agent-list", TextareaAgentList); - -// deno:https://jsr.io/@std/csv/1.0.6/_shared.ts -function codePointLength(s) { - return Array.from(s).length; -} - -// deno:https://jsr.io/@std/csv/1.0.6/_io.ts -function createBareQuoteErrorMessage(zeroBasedRecordStartLine, zeroBasedLine, zeroBasedColumn) { - return `Syntax error on line ${zeroBasedRecordStartLine + 1}; parse error on line ${zeroBasedLine + 1}, column ${zeroBasedColumn + 1}: bare " in non-quoted-field`; -} -function createQuoteErrorMessage(zeroBasedRecordStartLine, zeroBasedLine, zeroBasedColumn) { - return `Syntax error on line ${zeroBasedRecordStartLine + 1}; parse error on line ${zeroBasedLine + 1}, column ${zeroBasedColumn + 1}: extraneous or missing " in quoted-field`; -} -function convertRowToObject(row, headers, zeroBasedLine) { - if (row.length !== headers.length) { - throw new Error(`Syntax error on line ${zeroBasedLine + 1}: The record has ${row.length} fields, but the header has ${headers.length} fields`); - } - const out = {}; - for (const [index, header] of headers.entries()) { - out[header] = row[index]; - } - return out; -} - -// deno:https://jsr.io/@std/csv/1.0.6/parse.ts -var BYTE_ORDER_MARK = "\uFEFF"; -var Parser = class { - #input = ""; - #cursor = 0; - #options; - constructor({ separator = ",", trimLeadingSpace = false, comment, lazyQuotes, fieldsPerRecord } = {}) { - this.#options = { - separator, - trimLeadingSpace, - comment, - lazyQuotes, - fieldsPerRecord - }; - } - #readLine() { - if (this.#isEOF()) return null; - let buffer = ""; - let hadNewline = false; - while (this.#cursor < this.#input.length) { - if (this.#input.startsWith("\r\n", this.#cursor)) { - hadNewline = true; - this.#cursor += 2; - break; - } - if (this.#input.startsWith("\n", this.#cursor)) { - hadNewline = true; - this.#cursor += 1; - break; - } - buffer += this.#input[this.#cursor]; - this.#cursor += 1; - } - if (!hadNewline && buffer.endsWith("\r")) { - buffer = buffer.slice(0, -1); - } - return buffer; - } - #isEOF() { - return this.#cursor >= this.#input.length; - } - #parseRecord(zeroBasedStartLine) { - let fullLine = this.#readLine(); - if (fullLine === null) return null; - if (fullLine.length === 0) { - return []; - } - let zeroBasedLine = zeroBasedStartLine; - if (this.#options.comment && fullLine[0] === this.#options.comment) { - return []; - } - let line = fullLine; - const quote = '"'; - const quoteLen = quote.length; - const separatorLen = this.#options.separator.length; - let recordBuffer = ""; - const fieldIndexes = []; - parseField: while (true) { - if (this.#options.trimLeadingSpace) { - line = line.trimStart(); - } - if (line.length === 0 || !line.startsWith(quote)) { - const i = line.indexOf(this.#options.separator); - let field = line; - if (i >= 0) { - field = field.substring(0, i); - } - if (!this.#options.lazyQuotes) { - const j = field.indexOf(quote); - if (j >= 0) { - const col = codePointLength(fullLine.slice(0, fullLine.length - line.slice(j).length)); - throw new SyntaxError(createBareQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - } - recordBuffer += field; - fieldIndexes.push(recordBuffer.length); - if (i >= 0) { - line = line.substring(i + separatorLen); - continue parseField; - } - break parseField; - } else { - line = line.substring(quoteLen); - while (true) { - const i = line.indexOf(quote); - if (i >= 0) { - recordBuffer += line.substring(0, i); - line = line.substring(i + quoteLen); - if (line.startsWith(quote)) { - recordBuffer += quote; - line = line.substring(quoteLen); - } else if (line.startsWith(this.#options.separator)) { - line = line.substring(separatorLen); - fieldIndexes.push(recordBuffer.length); - continue parseField; - } else if (0 === line.length) { - fieldIndexes.push(recordBuffer.length); - break parseField; - } else if (this.#options.lazyQuotes) { - recordBuffer += quote; - } else { - const col = codePointLength(fullLine.slice(0, fullLine.length - line.length - quoteLen)); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - } else if (line.length > 0 || !this.#isEOF()) { - recordBuffer += line; - const r = this.#readLine(); - line = r ?? ""; - fullLine = line; - if (r === null) { - if (!this.#options.lazyQuotes) { - const col = codePointLength(fullLine); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - fieldIndexes.push(recordBuffer.length); - break parseField; - } - zeroBasedLine++; - recordBuffer += "\n"; - } else { - if (!this.#options.lazyQuotes) { - const col = codePointLength(fullLine); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - fieldIndexes.push(recordBuffer.length); - break parseField; - } - } - } - } - const result = []; - let preIdx = 0; - for (const i of fieldIndexes) { - result.push(recordBuffer.slice(preIdx, i)); - preIdx = i; - } - return result; - } - parse(input) { - this.#input = input.startsWith(BYTE_ORDER_MARK) ? input.slice(1) : input; - this.#cursor = 0; - const result = []; - let lineResult; - let first = true; - let lineIndex = 0; - const INVALID_RUNE = [ - "\r", - "\n", - '"' - ]; - const options = this.#options; - if (INVALID_RUNE.includes(options.separator) || typeof options.comment === "string" && INVALID_RUNE.includes(options.comment) || options.separator === options.comment) { - throw new Error("Cannot parse input: invalid delimiter"); - } - let _nbFields; - if (options.fieldsPerRecord === void 0 || options.fieldsPerRecord < 0) { - _nbFields = "ANY"; - } else if (options.fieldsPerRecord === 0) { - _nbFields = "UNINITIALIZED"; - } else { - _nbFields = options.fieldsPerRecord; - } - while (true) { - const r = this.#parseRecord(lineIndex); - if (r === null) break; - lineResult = r; - lineIndex++; - if (first) { - first = false; - if (_nbFields === "UNINITIALIZED") { - _nbFields = lineResult.length; - } - } - if (lineResult.length > 0) { - if (typeof _nbFields === "number" && _nbFields !== lineResult.length) { - throw new SyntaxError(`Syntax error on line ${lineIndex}: expected ${_nbFields} fields but got ${lineResult.length}`); - } - result.push(lineResult); - } - } - return result; - } -}; -function parse(input, options = { - skipFirstRow: false -}) { - const parser = new Parser(options); - const r = parser.parse(input); - if (options.skipFirstRow || options.columns) { - let headers = []; - if (options.skipFirstRow) { - const head = r.shift(); - if (head === void 0) { - throw new TypeError("Cannot parse input: headers must be defined"); - } - headers = head; - } - if (options.columns) { - headers = options.columns; - } - const zeroBasedFirstLineIndex = options.skipFirstRow ? 1 : 0; - return r.map((row, i) => { - return convertRowToObject(row, headers, zeroBasedFirstLineIndex + i); - }); - } - return r; -} - -// deno:https://jsr.io/@std/streams/1.0.10/_common.ts -var DEFAULT_BUFFER_SIZE = 32 * 1024; -function createLPS(pat) { - const length = pat.length; - const lps = new Uint8Array(length); - lps[0] = 0; - let prefixEnd = 0; - let i = 1; - while (i < length) { - if (pat[i] === pat[prefixEnd]) { - prefixEnd++; - lps[i] = prefixEnd; - i++; - } else if (prefixEnd === 0) { - lps[i] = 0; - i++; - } else { - prefixEnd = lps[prefixEnd - 1]; - } - } - return lps; -} - -// deno:https://jsr.io/@std/streams/1.0.10/text_delimiter_stream.ts -var TextDelimiterStream = class extends TransformStream { - #buf = ""; - #delimiter; - #inspectIndex = 0; - #matchIndex = 0; - #delimLPS; - #disp; - /** - * Constructs a new instance. - * - * @param delimiter A delimiter to split the stream by. - * @param options Options for the stream. - */ - constructor(delimiter, options) { - super({ - transform: (chunk, controller) => { - this.#handle(chunk, controller); - }, - flush: (controller) => { - controller.enqueue(this.#buf); - } - }); - this.#delimiter = delimiter; - this.#delimLPS = createLPS(new TextEncoder().encode(delimiter)); - this.#disp = options?.disposition ?? "discard"; - } - #handle(chunk, controller) { - this.#buf += chunk; - let localIndex = 0; - while (this.#inspectIndex < this.#buf.length) { - if (chunk[localIndex] === this.#delimiter[this.#matchIndex]) { - this.#inspectIndex++; - localIndex++; - this.#matchIndex++; - if (this.#matchIndex === this.#delimiter.length) { - const start = this.#inspectIndex - this.#delimiter.length; - const end = this.#disp === "suffix" ? this.#inspectIndex : start; - const copy = this.#buf.slice(0, end); - controller.enqueue(copy); - const shift = this.#disp === "prefix" ? start : this.#inspectIndex; - this.#buf = this.#buf.slice(shift); - this.#inspectIndex = this.#disp === "prefix" ? this.#delimiter.length : 0; - this.#matchIndex = 0; - } - } else { - if (this.#matchIndex === 0) { - this.#inspectIndex++; - localIndex++; - } else { - this.#matchIndex = this.#delimLPS[this.#matchIndex - 1]; - } - } - } - } -}; - -// deno:https://jsr.io/@std/csv/1.0.6/stringify.ts -var QUOTE = '"'; -var LF = "\n"; -var CRLF = "\r\n"; -var BYTE_ORDER_MARK2 = "\uFEFF"; -function getEscapedString(value, sep) { - if (value === void 0 || value === null) return ""; - let str = ""; - if (typeof value === "object") str = JSON.stringify(value); - else str = String(value); - if (str.includes(sep) || str.includes(LF) || str.includes(QUOTE)) { - return `${QUOTE}${str.replaceAll(QUOTE, `${QUOTE}${QUOTE}`)}${QUOTE}`; - } - return str; -} -function normalizeColumn(column) { - let header; - let prop; - if (typeof column === "object") { - if (Array.isArray(column)) { - header = String(column[column.length - 1]); - prop = column; - } else { - prop = Array.isArray(column.prop) ? column.prop : [ - column.prop - ]; - header = typeof column.header === "string" ? column.header : String(prop[prop.length - 1]); - } - } else { - header = String(column); - prop = [ - column - ]; - } - return { - header, - prop - }; -} -function getValuesFromItem(item, normalizedColumns) { - const values = []; - if (normalizedColumns.length) { - for (const column of normalizedColumns) { - let value = item; - for (const prop of column.prop) { - if (typeof value !== "object" || value === null) { - continue; - } - if (Array.isArray(value)) { - if (typeof prop === "number") value = value[prop]; - else { - throw new TypeError('Property accessor is not of type "number"'); - } - } else value = value[prop]; - } - values.push(value); - } - } else { - if (Array.isArray(item)) { - values.push(...item); - } else if (typeof item === "object") { - throw new TypeError("No property accessor function was provided for object"); - } else { - values.push(item); - } - } - return values; -} -function stringify(data, options) { - const { headers = true, separator: sep = ",", columns = [], bom = false } = options ?? {}; - if (sep.includes(QUOTE) || sep.includes(CRLF)) { - const message = [ - "Separator cannot include the following strings:", - ' - U+0022: Quotation mark (")', - " - U+000D U+000A: Carriage Return + Line Feed (\\r\\n)" - ].join("\n"); - throw new TypeError(message); - } - const normalizedColumns = columns.map(normalizeColumn); - let output = ""; - if (bom) { - output += BYTE_ORDER_MARK2; - } - if (headers && normalizedColumns.length > 0) { - output += normalizedColumns.map((column) => getEscapedString(column.header, sep)).join(sep); - output += CRLF; - } - for (const item of data) { - const values = getValuesFromItem(item, normalizedColumns); - output += values.map((value) => getEscapedString(value, sep)).join(sep); - output += CRLF; - } - return output; -} - -// deno:https://jsr.io/@std/csv/1.0.6/stringify_stream.ts -var CsvStringifyStream = class extends TransformStream { - /** - * Construct a new instance. - * - * @param options Options for the stream. - */ - constructor(options) { - const { separator, columns = [] } = options ?? {}; - super({ - start(controller) { - if (columns && columns.length > 0) { - try { - controller.enqueue(stringify([ - columns - ], separator !== void 0 ? { - separator, - headers: false - } : { - headers: false - })); - } catch (error) { - controller.error(error); - } - } - }, - transform(chunk, controller) { - try { - controller.enqueue(stringify([ - chunk - ], separator !== void 0 ? { - separator, - headers: false, - columns - } : { - headers: false, - columns - })); - } catch (error) { - controller.error(error); - } - } - }); - } -}; - -// src/textarea-csv.js -var TextareaCSV = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ - mode: "open" - }); - this.isComponentInitialized = false; - this.custcomCleanupFilter = void 0; - } - static get observedAttributes() { - return [ - "column-headings", - "id", - "class", - "caption", - "text", - "placeholder", - "css-href", - "debug" - ]; - } - attributeChangedCallback(name) { - if (name === "column-headings" && this.isComponentInitialized) { - this.initializeTable(); - } - } - connectedCallback() { - this.initializeComponent(); - this.isComponentInitialized = true; - this.initializeTable(); - this.setupEventListeners(); - this.setupMutationObserver(); - } - setupMutationObserver() { - const targetNode = this.shadowRoot.querySelector("tbody"); - const config = { - attributes: true, - childList: true, - subtree: true, - attributeFilter: [ - "value" - ] - // This ensures we only observe changes to the 'value' attribute - }; - const callback = (mutationsList, _observer) => { - for (const mutation of mutationsList) { - if (mutation.type === "attributes" && mutation.attributeName === "value") { - this.toTextarea(); - } else if (mutation.type === "childList") { - this.toTextarea(); - } - } - }; - this.observer = new MutationObserver(callback); - if (targetNode) { - this.observer.observe(targetNode, config); - } - } - disconnectedCallback() { - if (this.observer) { - this.observer.disconnect(); - } - } - initializeComponent() { - const template = document.createElement("template"); - template.innerHTML = ` - - - - -
- - - ${this.hasAttribute("debug") ? '' : ""} - ${this.hasAttribute("title") || this.hasAttribute("help-description") ? '\u24D8' : ""} - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - if (this.hasAttribute("css-href")) { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = this.getAttribute("css-href"); - this.shadowRoot.appendChild(link); - } - if (this.hasAttribute("title") || this.hasAttribute("help-description")) { - const helpIcon = this.shadowRoot.getElementById("help-icon"); - helpIcon.addEventListener("click", () => { - alert(`${this.getAttribute("title") || ""} -${this.getAttribute("help-description") || ""}`); - }); - } - } - initializeTable() { - const headings = parseCSVRow(this.getAttribute("column-headings")); - const table = this.shadowRoot.querySelector("table"); - const thead = table.querySelector("thead"); - thead.innerHTML = ""; - const tr = document.createElement("tr"); - headings.forEach((heading) => { - const th = document.createElement("th"); - th.textContent = heading; - tr.appendChild(th); - }); - thead.appendChild(tr); - const textarea = this.querySelector("textarea"); - if (textarea && textarea.value.trim()) { - this.fromTextarea(); - } else { - this.appendRow(); - } - const datalists = this.querySelectorAll("datalist"); - datalists.forEach((datalist) => { - this.shadowRoot.appendChild(datalist.cloneNode(true)); - }); - headings.forEach((heading, index) => { - const datalist = this.shadowRoot.querySelector(`datalist#${heading.toLowerCase()}`); - if (datalist) { - this.setAutocomplete(index, Array.from(datalist.options).map((option) => ({ - value: option.value - }))); - } - }); - } - setupEventListeners() { - this.shadowRoot.querySelector("#append-row").addEventListener("click", () => this.appendRow()); - this.shadowRoot.querySelector("#cleanup").addEventListener("click", () => this.cleanupTable()); - if (this.hasAttribute("debug")) { - this.shadowRoot.querySelector("#debug").addEventListener("click", () => { - console.log(this.toCSV()); - }); - } - this.shadowRoot.querySelector("tbody").addEventListener("input", (event) => { - if (event.target.tagName === "INPUT") { - const rowIndex = event.target.closest("tr").rowIndex - 1; - const colIndex = event.target.closest("td").cellIndex; - const value = event.target.value; - this.dispatchEvent(new CustomEvent("changed", { - detail: { - rowIndex, - colIndex, - value - } - })); - if (this.hasAttribute("debug")) { - console.log(`Cell changed: Row ${rowIndex}, Col ${colIndex}, Value ${value}`); - } - } - }); - this.shadowRoot.querySelector("tbody").addEventListener("focus", (event) => { - if (event.target.tagName === "INPUT") { - const rowIndex = event.target.closest("tr").rowIndex - 1; - const colIndex = event.target.closest("td").cellIndex; - const value = event.target.value; - this.dispatchEvent(new CustomEvent("focused", { - detail: { - rowIndex, - colIndex, - value - } - })); - if (this.hasAttribute("debug")) { - console.log(`Cell focused: Row ${rowIndex}, Col ${colIndex}, Value ${value}`); - } - } - }, true); - this.shadowRoot.querySelector("tbody").addEventListener("keydown", (event) => { - if (event.key === "Backspace" && event.target.tagName === "INPUT") { - const input = event.target; - const start = input.selectionStart; - const end = input.selectionEnd; - if (start === end && start > 0) { - input.value = input.value.slice(0, start - 1) + input.value.slice(end); - input.selectionStart = input.selectionEnd = start - 1; - event.preventDefault(); - } - } - }); - } - rowCount() { - return this.shadowRoot.querySelector("tbody").rows.length; - } - columnCount() { - return this.shadowRoot.querySelector("thead").rows[0].cells.length; - } - isEmptyRow(rowIndex) { - const row = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1})`); - return Array.from(row.cells).every((cell) => cell.querySelector("input").value === ""); - } - appendRow() { - const tbody = this.shadowRoot.querySelector("tbody"); - const row = document.createElement("tr"); - for (let i = 0; i < this.columnCount(); i++) { - const td = document.createElement("td"); - const input = document.createElement("input"); - input.type = "text"; - input.placeholder = this.getAttribute("placeholder") || ""; - const datalistId = `column-${i}`; - const datalist = this.shadowRoot.querySelector(`datalist#${datalistId}`); - if (datalist) { - input.setAttribute("list", datalistId); - } - td.appendChild(input); - row.appendChild(td); - } - tbody.appendChild(row); - row.cells[0].querySelector("input").focus(); - } - cleanupTable() { - const tbody = this.shadowRoot.querySelector("tbody"); - const rows = tbody.rows; - for (let i = rows.length - 1; i >= 0; i--) { - if (this.isEmptyRow(i)) { - tbody.deleteRow(i); - } - if (this.customCleanupFilter !== void 0 && this.customCleanupFilter(rows[i]) === false) { - tbody.deleteRow(i); - } - } - } - toCSV() { - const rows = []; - const tbody = this.shadowRoot.querySelector("tbody").rows; - console.log(this.innertHTML); - for (let i = 0; i < tbody.length; i++) { - const cells = tbody[i].cells; - const row = []; - for (let j = 0; j < cells.length; j++) { - row.push(cells[j].querySelector("input").value); - } - rows.push(row); - } - return stringifyCSV(rows); - } - fromCSV(csvText) { - const rows = parse(csvText); - const tbody = this.shadowRoot.querySelector("tbody"); - tbody.innerHTML = ""; - rows.forEach((row) => { - const tr = document.createElement("tr"); - row.forEach((cell) => { - const td = document.createElement("td"); - const input = document.createElement("input"); - input.type = "text"; - input.value = cell; - td.appendChild(input); - tr.appendChild(td); - }); - tbody.appendChild(tr); - }); - } - toObjects() { - const objects = []; - const tbody = this.shadowRoot.querySelector("tbody").rows; - for (let i = 0; i < tbody.length; i++) { - const cells = tbody[i].cells; - for (let j = 0; j < cells.length; j++) { - objects.push({ - rowIndex: i, - colIndex: j, - value: cells[j].querySelector("input").value - }); - } - } - return objects; - } - fromObjects(objects) { - const tbody = this.shadowRoot.querySelector("tbody"); - tbody.innerHTML = ""; - objects.forEach((obj) => { - let row = tbody.rows[obj.rowIndex]; - if (!row) { - row = document.createElement("tr"); - tbody.appendChild(row); - } - let cell = row.cells[obj.colIndex]; - if (!cell) { - cell = document.createElement("td"); - row.appendChild(cell); - } - let input = cell.querySelector("input"); - if (!input) { - input = document.createElement("input"); - input.type = "text"; - cell.appendChild(input); - } - input.value = obj.value; - }); - } - fromTextarea() { - const textarea = this.querySelector("textarea"); - if (textarea) { - this.fromCSV(textarea.value.trim()); - } - } - toTextarea() { - const textarea = this.querySelector("textarea"); - if (textarea) { - textarea.value = this.toCSV(); - console.log("Textarea updated with CSV data:", textarea.value); - } - } - setupFormListener() { - const form = this.closest("form"); - if (form) { - form.addEventListener("submit", (_event) => { - this.toTextarea(); - }); - } - } - getCellValue(rowIndex, colIndexOrName) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const cell = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1}) td:nth-child(${colIndex + 1}) input`); - return cell ? cell.value : ""; - } - setCellValue(rowIndex, colIndexOrName, value) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const cell = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1}) td:nth-child(${colIndex + 1}) input`); - if (cell) { - cell.value = value; - } - } - toJSON() { - return JSON.stringify(this.toObjects()); - } - fromJSON(jsonString) { - this.fromObjects(JSON.parse(jsonString)); - } - setAutocomplete(colIndexOrName, options) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const datalistId = `column-${colIndex}`; - let datalist = this.shadowRoot.querySelector(`datalist#${datalistId}`); - if (!datalist) { - datalist = document.createElement("datalist"); - datalist.id = datalistId; - this.shadowRoot.appendChild(datalist); - } - datalist.innerHTML = ""; - options.forEach((option) => { - const opt = document.createElement("option"); - opt.value = option.value; - datalist.appendChild(opt); - }); - const inputs = this.shadowRoot.querySelectorAll(`tbody td:nth-child(${colIndex + 1}) input`); - inputs.forEach((input) => input.setAttribute("list", datalistId)); - } - getAutocomplete(colIndexOrName) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const datalist = this.shadowRoot.querySelector(`datalist#column-${colIndex}`); - if (datalist) { - const options = []; - datalist.querySelectorAll("option").forEach((option) => { - options.push({ - value: option.value - }); - }); - return options; - } - return void 0; - } - getColumnIndexByName(colName) { - const headings = parseCSVRow(this.getAttribute("column-headings")); - return headings.indexOf(colName); - } -}; -function parseCSVRow(csvRowString) { - const data = parse(csvRowString); - return data[0]; -} -function stringifyCSV(data) { - return stringify(data); -} -customElements.define("textarea-csv", TextareaCSV); - -// src/table-sortable.js -var TableSortable = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ - mode: "open" - }); - this.shadowRoot.innerHTML = ` - -
- - -
-
- `; - this.tableContainer = this.shadowRoot.getElementById("tableContainer"); - this.searchInput = this.shadowRoot.getElementById("searchInput"); - this.columnSelect = this.shadowRoot.getElementById("columnSelect"); - } - connectedCallback() { - const table = this.querySelector("table"); - if (table) { - const tableClone = table.cloneNode(true); - this.tableContainer.appendChild(tableClone); - this.setupTableSortable(tableClone); - this.setupSearch(tableClone); - } - } - setupTableSortable(table) { - const headers = table.querySelectorAll("thead th"); - const tbody = table.querySelector("tbody"); - const rows = tbody.querySelectorAll("tr"); - headers.forEach((header, index) => { - const option = document.createElement("option"); - option.value = index; - option.textContent = header.textContent.trim(); - this.columnSelect.appendChild(option); - }); - headers.forEach((header, index) => { - header.addEventListener("click", () => { - this.sortTable(index, rows); - }); - }); - } - setupSearch(table) { - this.searchInput.addEventListener("input", () => { - const searchTerm = this.searchInput.value.toLowerCase(); - const columnIndex = parseInt(this.columnSelect.value); - const rows = table.querySelectorAll("tbody tr"); - rows.forEach((row) => { - const cell = row.cells[columnIndex]; - const cellText = cell.textContent.toLowerCase(); - if (cellText.includes(searchTerm)) { - row.style.display = ""; - } else { - row.style.display = "none"; - } - }); - }); - } - sortTable(columnIndex, rows) { - const tbody = this.tableContainer.querySelector("tbody"); - const rowsArray = Array.from(rows); - const isAscending = !this.isSortedAscending(columnIndex); - rowsArray.sort((rowA, rowB) => { - const cellA = rowA.cells[columnIndex].textContent; - const cellB = rowB.cells[columnIndex].textContent; - return isAscending ? cellA.localeCompare(cellB) : cellB.localeCompare(cellA); - }); - while (tbody.firstChild) { - tbody.removeChild(tbody.firstChild); - } - rowsArray.forEach((row) => tbody.appendChild(row)); - } - isSortedAscending(columnIndex) { - const table = this.tableContainer.querySelector("table"); - const rows = table.querySelectorAll("tbody tr"); - for (let i = 0; i < rows.length - 1; i++) { - const cellA = rows[i].cells[columnIndex].textContent; - const cellB = rows[i + 1].cells[columnIndex].textContent; - if (cellA.localeCompare(cellB) > 0) { - return false; - } - } - return true; - } -}; -customElements.define("table-sortable", TableSortable); - -// src/footer-global.js -if (!document.getElementById("hind-fonts-global")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-global"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var PRESETS = { - library: { - email: "library@caltech.edu", - phone: "626-395-3405", - libraryName: "Caltech Library", - libraryLink: "https://library.caltech.edu/", - mailCode: "Mail Code 1-43", - instagram: "https://www.instagram.com/caltechlibrary/", - youtube: "https://www.youtube.com/channel/UCQbC4mcNNqypGMRtjgcN0SA", - logo: "library", - donateLink: "https://library.caltech.edu/about/support", - donateText: "Donate" - }, - archives: { - email: "archives@caltech.edu", - phone: "626-395-2704", - libraryName: "Caltech Archives & Special Collections", - libraryLink: "https://library.caltech.edu/archives", - mailCode: "Mail Code B215A-74", - instagram: "https://www.instagram.com/caltecharchives/", - youtube: "https://www.youtube.com/channel/UCQbC4mcNNqypGMRtjgcN0SA", - logo: "archives", - donateLink: "https://library.caltech.edu/archives/about/donate", - donateText: "Donate & Transfer" - } -}; -var FooterGlobal = class extends HTMLElement { - constructor() { - super(); - const shadow = this.attachShadow({ - mode: "open" - }); - const template = document.createElement("template"); - template.innerHTML = ` - - - - - - - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - this.addEventListener("mouseover", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "underline"; - } - }); - this.addEventListener("mouseout", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "none"; - } - }); - } - // Gets the active preset configuration based on attributes - // Library is the default; archives overrides when attribute is present - getPreset() { - if (this.hasAttribute("archives")) return PRESETS.archives; - return PRESETS.library; - } - // Gets attribute value with preset fallback - // Priority: 1) explicit attribute, 2) preset value - getAttr(attrName, presetKey) { - if (this.hasAttribute(attrName)) { - return this.getAttribute(attrName); - } - const preset = this.getPreset(); - return preset[presetKey]; - } - // Handles slot fallback, attribute wiring, and dynamic content. - connectedCallback() { - console.log("\u{1F4E6} FooterGlobal connected"); - this.hoursUrl = "https://libcal.caltech.edu/api_hours_today.php?lid=0&format=json&systemTime=0"; - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (!slot1) { - console.warn("\u26A0\uFE0F slot[name='custom-links'] not found"); - return; - } - const assignedNodes = slot1.assignedNodes({ - flatten: true - }); - if (assignedNodes.length === 0 && !this.hasAttribute("custom")) { - console.log("\u{1FAB6} No slotted content detected at load \u2014 using default"); - this.loadDefaultColumn1(); - } - if (this.getAttribute("breadcrumbs") === "true") { - const breadcrumbEl = document.getElementById("s-lib-bc"); - const breadcrumbTarget = this.shadowRoot.getElementById("footer-breadcrumbs"); - if (breadcrumbEl && breadcrumbTarget) { - const clone = breadcrumbEl.cloneNode(true); - clone.id = "s-lib-bc-footer"; - breadcrumbTarget.appendChild(clone); - console.log("\u{1F517} Breadcrumb mirrored into footer component"); - } else { - console.log("\u2139\uFE0F Breadcrumb not found or target missing"); - } - } - if (this.getAttribute("libguides-login") === "true") { - console.log("\u{1F9EA} Detected Libguides-login=true"); - const sourceWrapper = document.getElementById("s-lib-footer-login-link"); - if (!sourceWrapper) { - console.warn("\u274C s-lib-footer-login-link not found"); - return; - } - const sourceLink = sourceWrapper.querySelector("a"); - if (!sourceLink) { - console.warn("\u{1F6AB} Login not found inside s-lib-footer-login-link"); - return; - } - console.log("\u2705 Login link found:", sourceLink.href); - const login_link = document.createElement("a"); - login_link.setAttribute("href", sourceLink.href); - login_link.setAttribute("aria-label", "Staff Login"); - login_link.innerHTML = ` - - `; - const shadowTarget = this.shadowRoot?.getElementById("footer-login"); - if (!shadowTarget) { - console.warn("\u26A0\uFE0F Could not find #footer-login inside shadow DOM"); - return; - } - console.log("\u{1F680} Injecting login link into shadow DOM"); - shadowTarget.appendChild(login_link); - } - slot1.addEventListener("slotchange", () => { - const hasContent = slot1.assignedNodes().length > 0; - const hasCustomAttr = this.hasAttribute("custom"); - const fallback = this.shadowRoot.querySelector("#default-column1"); - const shouldHideHours = hasContent || hasCustomAttr; - fallback.style.display = shouldHideHours ? "none" : "block"; - fallback.setAttribute("aria-hidden", shouldHideHours ? "true" : "false"); - if (!shouldHideHours) { - this.loadDefaultColumn1(); - } - }); - const email = this.getAttr("email", "email"); - const emailLink = this.shadowRoot.getElementById("email-link"); - if (emailLink) { - emailLink.href = `mailto:${email}`; - emailLink.textContent = email; - } - const phone = this.getAttr("phone", "phone"); - const phoneLink = this.shadowRoot.getElementById("phone-link"); - if (phoneLink) { - phoneLink.href = `tel:${phone.replace(/\D/g, "")}`; - phoneLink.textContent = phone; - } - const libraryName = this.getAttr("library-name", "libraryName"); - const libraryLink = this.getAttr("library-link", "libraryLink"); - const libraryNameLink = this.shadowRoot.getElementById("library-name"); - if (libraryNameLink) { - libraryNameLink.innerHTML = libraryName; - libraryNameLink.href = libraryLink; - } - const mailcode = this.getAttr("mail-code", "mailCode"); - const mailCode = this.shadowRoot.getElementById("mail-code"); - if (mailCode) { - mailCode.textContent = mailcode; - } - const headerEl = this.shadowRoot.getElementById("column1-header"); - if (headerEl) { - const hasCustomAttr = this.hasAttribute("custom"); - const hasHeaderAttr = this.hasAttribute("header"); - if (hasCustomAttr && !hasHeaderAttr) { - headerEl.style.display = "none"; - } else { - const headerText = this.getAttribute("header") || "Hours"; - headerEl.textContent = headerText; - headerEl.style.display = "block"; - } - } - const instagramAnchor = this.shadowRoot.getElementById("instagram-link"); - const instagramHref = this.getAttr("instagram", "instagram"); - const youtubeHref = this.getAttr("youtube", "youtube"); - const youtubeAnchor = this.shadowRoot.getElementById("youtube-link"); - const rssHref = this.getAttribute("rss"); - if (instagramAnchor) instagramAnchor.setAttribute("href", instagramHref); - if (youtubeAnchor) youtubeAnchor.setAttribute("href", youtubeHref); - if (rssHref) { - const rssAnchor = document.createElement("a"); - rssAnchor.setAttribute("id", "rss-link"); - rssAnchor.setAttribute("href", rssHref); - rssAnchor.innerHTML = ` - - RSS Feed - - - `; - if (this.shadowRoot) { - const socialDiv = this.shadowRoot.querySelector(".social"); - if (socialDiv) { - socialDiv.appendChild(rssAnchor); - } - } - } - const logoType = this.getAttr("logo", "logo").toLowerCase(); - const logoContainer = this.shadowRoot.querySelector(".footer-logo-container"); - if (logoContainer) { - const templateId = logoType === "archives" ? "archives-logo" : "library-logo"; - const logoTemplate = this.shadowRoot.getElementById(templateId); - console.log("Logo type:", logoType); - console.log("Template ID:", templateId); - console.log("Logo template found:", !!logoTemplate); - if (logoTemplate) { - logoContainer.innerHTML = ""; - logoContainer.appendChild(logoTemplate.content.cloneNode(true)); - } - } - const donateLink = this.getAttr("donate-link", "donateLink"); - const donateText = this.getAttr("donate-text", "donateText"); - const donateLinkEl = this.shadowRoot.getElementById("donate-link"); - if (donateLinkEl) { - donateLinkEl.href = donateLink; - donateLinkEl.textContent = donateText; - } - } - // If no Slot, load column1 default content - Library Hours - async loadDefaultColumn1() { - const container = this.shadowRoot.querySelector("#default-column1"); - try { - const response = await fetch(this.hoursUrl); - const data = await response.json(); - const locations = data.locations; - const column1Content = document.createElement("div"); - column1Content.id = "column1"; - column1Content.classList.add("column1-default"); - const rows = locations.map((loc) => { - const name = loc.name; - const url = loc.url; - const hours = loc.rendered || "\u2014"; - return ` -
- ${name} - ${hours} -
`; - }).join(""); - const todayString = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric" - }); - column1Content.innerHTML = ` -
-
${todayString}
- ${rows} - -
- `; - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (slot1 && slot1.parentNode) { - slot1.parentNode.insertBefore(column1Content, slot1); - } else { - container.appendChild(column1Content); - } - } catch (error) { - console.error("Error loading library hours:", error); - container.innerHTML = `

Unable to load library hours at this time.

`; - } - } -}; -customElements.define("footer-global", FooterGlobal); - -// src/footer-global-lite.js -if (!document.getElementById("hind-fonts-global")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-global"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var PRESETS2 = { - library: { - email: "library@caltech.edu", - phone: "626-395-3405", - libraryName: "Caltech Library", - libraryLink: "https://library.caltech.edu/", - logo: "library", - donateLink: "https://library.caltech.edu/about/support", - donateText: "Donate" - }, - archives: { - email: "archives@caltech.edu", - phone: "626-395-2704", - libraryName: "Caltech Archives & Special Collections", - libraryLink: "https://library.caltech.edu/archives", - logo: "archives", - donateLink: "https://library.caltech.edu/archives/about/donate", - donateText: "Donate & Transfer" - } -}; -var FooterGlobalLite = class extends HTMLElement { - constructor() { - super(); - const shadow = this.attachShadow({ - mode: "open" - }); - const template = document.createElement("template"); - template.innerHTML = ` - - - - - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - this.addEventListener("mouseover", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "underline"; - } - }); - this.addEventListener("mouseout", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "none"; - } - }); - } - // Gets the active preset configuration based on attributes - // Library is the default; archives overrides when attribute is present - getPreset() { - if (this.hasAttribute("archives")) return PRESETS2.archives; - return PRESETS2.library; - } - // Gets attribute value with preset fallback - // Priority: 1) explicit attribute, 2) preset value - getAttr(attrName, presetKey) { - if (this.hasAttribute(attrName)) { - return this.getAttribute(attrName); - } - const preset = this.getPreset(); - return preset[presetKey]; - } - // Handles slot fallback, attribute wiring, and dynamic content. - connectedCallback() { - console.log("\u{1F4E6} FooterGlobalLite connected"); - this.shadowRoot.querySelector("#back-to-top")?.addEventListener("click", (e) => { - e.preventDefault(); - window.scrollTo({ - top: 0, - behavior: "smooth" - }); - }); - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (!slot1) { - console.warn("\u26A0\uFE0F slot[name='custom-links'] not found"); - return; - } - const email = this.getAttr("email", "email"); - const phone = this.getAttr("phone", "phone"); - const emailLink = this.shadowRoot.getElementById("email-link"); - const phoneLink = this.shadowRoot.getElementById("phone-link"); - if (emailLink) { - emailLink.href = `mailto:${email}`; - emailLink.textContent = email; - } - if (phoneLink) { - phoneLink.href = `tel:${phone.replace(/\D/g, "")}`; - phoneLink.textContent = phone; - } - const libraryName = this.getAttr("library-name", "libraryName"); - const libraryLink = this.getAttr("library-link", "libraryLink"); - const libraryNameLink = this.shadowRoot.getElementById("library-name"); - if (libraryNameLink) { - libraryNameLink.innerHTML = libraryName; - libraryNameLink.href = libraryLink; - } - const headerEl = this.shadowRoot.getElementById("column1-header"); - if (headerEl) { - const hasCustomAttr = this.hasAttribute("custom"); - const hasHeaderAttr = this.hasAttribute("header"); - if (hasCustomAttr && !hasHeaderAttr) { - headerEl.style.display = "none"; - } else { - const headerText = this.getAttribute("header") || "Quick Links"; - headerEl.textContent = headerText; - headerEl.style.display = "block"; - } - } - const logoType = this.getAttr("logo", "logo").toLowerCase(); - const logoContainer = this.shadowRoot.querySelector(".footer-logo-container"); - if (logoContainer) { - const templateId = logoType === "archives" ? "archives-logo" : "library-logo"; - const logoTemplate = this.shadowRoot.getElementById(templateId); - console.log("Logo type:", logoType); - console.log("Template ID:", templateId); - console.log("Logo template found:", !!logoTemplate); - if (logoTemplate) { - logoContainer.innerHTML = ""; - logoContainer.appendChild(logoTemplate.content.cloneNode(true)); - } - } - const donateLink = this.getAttr("donate-link", "donateLink"); - const donateText = this.getAttr("donate-text", "donateText"); - const donateLinkEl = this.shadowRoot.getElementById("donate-link"); - if (donateLinkEl) { - donateLinkEl.href = donateLink; - donateLinkEl.textContent = donateText; - } - if (this.hasAttribute("archives")) { - const missionStatementItem = this.shadowRoot.getElementById("mission-statement-item"); - if (missionStatementItem) { - missionStatementItem.style.display = "none"; - } - } - } -}; -customElements.define("footer-global-lite", FooterGlobalLite); - -// src/card-layout.js -if (!document.getElementById("hind-fonts-card")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-card"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var CardLayout = class extends HTMLElement { - static get observedAttributes() { - return [ - "layout", - "count" - ]; - } - constructor() { - super(); - const shadow = this.attachShadow({ - mode: "open" - }); - const template = document.createElement("template"); - template.innerHTML = ` - - -
- `; - shadow.appendChild(template.content.cloneNode(true)); - } - connectedCallback() { - this.applyLayout(); - this.renderCards(); - } - attributeChangedCallback(name, oldValue, newValue) { - if (oldValue === newValue) return; - if (name === "layout") this.applyLayout(); - if (name === "count") this.renderCards(); - } - renderCards() { - const container = this.shadowRoot.querySelector(".cards-container"); - if (!container) return; - const count = parseInt(this.getAttribute("count"), 10) || 3; - container.innerHTML = ""; - for (let i = 1; i <= count; i++) { - const card = document.createElement("div"); - card.className = "card"; - card.id = `card${i}`; - card.innerHTML = ` -
Image
-
-

Card ${i}

-

Add your content here.

- Read more -
- `; - container.appendChild(card); - this.setupCard(i); - } - } - applyLayout() { - const container = this.shadowRoot.querySelector(".cards-container"); - if (!container) return; - const layout = this.getAttribute("layout") || "horizontal"; - if (layout === "vertical") { - container.classList.add("vertical"); - } else { - container.classList.remove("vertical"); - } - } - setupCard(cardNum) { - const card = this.shadowRoot.getElementById(`card${cardNum}`); - if (!card) return; - const prefix = `card${cardNum}`; - const imageUrl = this.getAttribute(`${prefix}-image`); - const imageContainer = card.querySelector(".card-image-placeholder, .card-image"); - if (imageUrl && imageContainer) { - const img = document.createElement("img"); - img.src = imageUrl; - img.alt = this.getAttribute(`${prefix}-alt`) || this.getAttribute(`${prefix}-title`) || `Card ${cardNum}`; - img.className = "card-image"; - imageContainer.replaceWith(img); - } - const title = this.getAttribute(`${prefix}-title`); - const titleEl = card.querySelector(".card-title"); - if (title && titleEl) { - titleEl.textContent = title; - } - const description = this.getAttribute(`${prefix}-description`); - const descEl = card.querySelector(".card-description"); - if (description && descEl) { - descEl.textContent = description; - } - const link = this.getAttribute(`${prefix}-link`); - const linkText = this.getAttribute(`${prefix}-link-text`) || "Read more"; - const linkEl = card.querySelector(".card-link"); - if (linkEl) { - if (link) { - linkEl.href = link; - linkEl.textContent = linkText; - } else { - linkEl.style.display = "none"; - } - } - } -}; -customElements.define("card-layout", CardLayout); -export { - CardLayout, - FooterGlobal, - FooterGlobalLite, - TableSortable, - TextareaAgentList, - TextareaCSV, - ULAtoZList, - licenseText, - releaseDate, - releaseHash, - version -}; diff --git a/copyToClipboard.js b/copyToClipboard.js deleted file mode 100644 index 2f2e912..0000000 --- a/copyToClipboard.js +++ /dev/null @@ -1,26 +0,0 @@ -// src/copyToClipboard.js -document.addEventListener("DOMContentLoaded", function() { - const preElements = document.querySelectorAll("pre"); - preElements.forEach((pre) => { - const codeElement = pre.querySelector("code"); - if (codeElement) { - const copyButton = document.createElement("button"); - copyButton.className = "copy-button"; - copyButton.innerHTML = "\u{1F4CB}"; - copyButton.setAttribute("title", "Copy to Clipboard"); - copyButton.addEventListener("click", () => { - const textToCopy = codeElement.textContent; - navigator.clipboard.writeText(textToCopy).then(() => { - copyButton.textContent = "Copied!"; - setTimeout(() => { - copyButton.innerHTML = "\u{1F4CB}"; - }, 2e3); - }).catch((err) => { - console.error("Failed to copy text: ", err); - }); - }); - pre.style.position = "relative"; - pre.appendChild(copyButton); - } - }); -}); diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 5f7f20c..a84dbdf 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -19,10 +19,10 @@ DISTRIBUTION_ID This file is included in gitignore and is **not committed to git**. --- - + # Deploy Documentation Changes - + Use this workflow when **only documentation (`.md`) files have changed**. ## Step 1. Edit and push @@ -63,65 +63,39 @@ open _site/index.html That is the same script CI runs. --- - -# Deploy Updated Web Component Code - - -Use this workflow when **component code in `src/` has changed** and needs to be deployed to the CDN. - - -## Step 1. Build compiled JavaScript - -```bash -make build -``` - -This command runs `deno task build` and bundles: - -- `src/*.js` > matching root-level `.js` files -- `mod.js` > `cl-web-components.js` (combined build) - -## Step 2. Preview the S3 upload (optional) - -```bash -./publish_to_s3.bash dry-run -``` - -This shows which files will be uploaded without making changes. -## Step 4. Save and push your working branch - -If you added **new files**, stage them first: +# Deploy Updated Web Component Code -```bash -git add -``` +Use this workflow when **component code in `src/` has changed**. -Then commit and push: +## Step 1. Edit, commit and open a pull request -```bash -make save msg="your commit message" -``` +Sources live in `src/`. Nothing compiled is committed — the bundles in `dist/` +are built by CI and gitignored, so there is no build step to run before +pushing. -> `make save` uses `git commit -am` which only commits already-tracked files. New files must be staged with `git add` first. +## Step 2. Merge -## Step 5. Deploy to S3 and refresh the CDN +Once the pull request has been merged, the documentation site rebuilds on its +own and serves the new bundles at +. That happens within +a couple of minutes and needs nothing from you. -```bash -./publish_to_s3.bash -``` +## Step 3. Publish to the CDN -This script: +The CDN copy at updates +when a **release is published**, not on every merge. That is deliberate: +consumers loading from the CDN get a version someone chose to ship. -- Uploads root `*.js` and `css/*.css` files -- Places them under `/cl-webcomponents/` in the S3 bucket -- Creates a **CloudFront cache invalidation** so the CDN serves the new files +To publish outside a release, run the **Publish to S3** workflow manually from +the Actions tab. It defaults to a dry run, which reports exactly what would be +uploaded and invalidated without changing anything. Uncheck `dry_run` to +publish for real. -The documentation site redeploys on its own when the push lands on `main`. +Credentials are OIDC — no AWS keys exist in the repository or on anyone's +machine. ---- - -# Deploy a New Release +# Deploy a New Release Use this workflow when creating a **versioned GitHub release**. @@ -208,12 +182,11 @@ https://github.com/caltechlibrary/CL-web-components/releases | Task | Command | |-----|---------| -| Compile source code | `make build` | +| Compile source code | `deno task build` | | Save and push working branch | `make save msg="your message"` | | Deploy the docs site | Automatic on push to `main` | -| Preview S3 deployment | `./publish_to_s3.bash dry-run` | -| Deploy JS to S3 and invalidate CDN cache | `./publish_to_s3.bash` | -| Invalidate CDN cache only | `./invalidate_cdn.bash` | +| Preview the S3 upload | **Publish to S3** workflow, `dry_run` checked | +| Publish to S3 and invalidate the CDN | Automatic when a release is published | | Build distribution bundle | `make dist` | | Create GitHub release | `./release.bash` | diff --git a/footer-global-lite.js b/footer-global-lite.js deleted file mode 100644 index 5c39559..0000000 --- a/footer-global-lite.js +++ /dev/null @@ -1,642 +0,0 @@ -// src/footer-global-lite.js -if (!document.getElementById("hind-fonts-global")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-global"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var PRESETS = { - library: { - email: "library@caltech.edu", - phone: "626-395-3405", - libraryName: "Caltech Library", - libraryLink: "https://library.caltech.edu/", - logo: "library", - donateLink: "https://library.caltech.edu/about/support", - donateText: "Donate" - }, - archives: { - email: "archives@caltech.edu", - phone: "626-395-2704", - libraryName: "Caltech Archives & Special Collections", - libraryLink: "https://library.caltech.edu/archives", - logo: "archives", - donateLink: "https://library.caltech.edu/archives/about/donate", - donateText: "Donate & Transfer" - } -}; -var FooterGlobalLite = class extends HTMLElement { - constructor() { - super(); - const shadow = this.attachShadow({ mode: "open" }); - const template = document.createElement("template"); - template.innerHTML = ` - - - - - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - this.addEventListener("mouseover", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "underline"; - } - }); - this.addEventListener("mouseout", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "none"; - } - }); - } - // Gets the active preset configuration based on attributes - // Library is the default; archives overrides when attribute is present - getPreset() { - if (this.hasAttribute("archives")) return PRESETS.archives; - return PRESETS.library; - } - // Gets attribute value with preset fallback - // Priority: 1) explicit attribute, 2) preset value - getAttr(attrName, presetKey) { - if (this.hasAttribute(attrName)) { - return this.getAttribute(attrName); - } - const preset = this.getPreset(); - return preset[presetKey]; - } - // Handles slot fallback, attribute wiring, and dynamic content. - connectedCallback() { - console.log("\u{1F4E6} FooterGlobalLite connected"); - this.shadowRoot.querySelector("#back-to-top")?.addEventListener("click", (e) => { - e.preventDefault(); - window.scrollTo({ - top: 0, - behavior: "smooth" - }); - }); - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (!slot1) { - console.warn("\u26A0\uFE0F slot[name='custom-links'] not found"); - return; - } - const email = this.getAttr("email", "email"); - const phone = this.getAttr("phone", "phone"); - const emailLink = this.shadowRoot.getElementById("email-link"); - const phoneLink = this.shadowRoot.getElementById("phone-link"); - if (emailLink) { - emailLink.href = `mailto:${email}`; - emailLink.textContent = email; - } - if (phoneLink) { - phoneLink.href = `tel:${phone.replace(/\D/g, "")}`; - phoneLink.textContent = phone; - } - const libraryName = this.getAttr("library-name", "libraryName"); - const libraryLink = this.getAttr("library-link", "libraryLink"); - const libraryNameLink = this.shadowRoot.getElementById("library-name"); - if (libraryNameLink) { - libraryNameLink.innerHTML = libraryName; - libraryNameLink.href = libraryLink; - } - const headerEl = this.shadowRoot.getElementById("column1-header"); - if (headerEl) { - const hasCustomAttr = this.hasAttribute("custom"); - const hasHeaderAttr = this.hasAttribute("header"); - if (hasCustomAttr && !hasHeaderAttr) { - headerEl.style.display = "none"; - } else { - const headerText = this.getAttribute("header") || "Quick Links"; - headerEl.textContent = headerText; - headerEl.style.display = "block"; - } - } - const logoType = this.getAttr("logo", "logo").toLowerCase(); - const logoContainer = this.shadowRoot.querySelector(".footer-logo-container"); - if (logoContainer) { - const templateId = logoType === "archives" ? "archives-logo" : "library-logo"; - const logoTemplate = this.shadowRoot.getElementById(templateId); - console.log("Logo type:", logoType); - console.log("Template ID:", templateId); - console.log("Logo template found:", !!logoTemplate); - if (logoTemplate) { - logoContainer.innerHTML = ""; - logoContainer.appendChild(logoTemplate.content.cloneNode(true)); - } - } - const donateLink = this.getAttr("donate-link", "donateLink"); - const donateText = this.getAttr("donate-text", "donateText"); - const donateLinkEl = this.shadowRoot.getElementById("donate-link"); - if (donateLinkEl) { - donateLinkEl.href = donateLink; - donateLinkEl.textContent = donateText; - } - if (this.hasAttribute("archives")) { - const missionStatementItem = this.shadowRoot.getElementById("mission-statement-item"); - if (missionStatementItem) { - missionStatementItem.style.display = "none"; - } - } - } -}; -customElements.define("footer-global-lite", FooterGlobalLite); -export { - FooterGlobalLite -}; diff --git a/footer-global.js b/footer-global.js deleted file mode 100644 index 5a01d58..0000000 --- a/footer-global.js +++ /dev/null @@ -1,857 +0,0 @@ -// src/footer-global.js -if (!document.getElementById("hind-fonts-global")) { - const fontStyles = document.createElement("style"); - fontStyles.id = "hind-fonts-global"; - fontStyles.textContent = ` - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-400.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-500.woff') format('woff'); - } - @font-face { - font-family: 'Hind'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://media.library.caltech.edu/cl-webcomponents/fonts/hind-600.woff') format('woff'); - } - `; - document.head.appendChild(fontStyles); -} -var PRESETS = { - library: { - email: "library@caltech.edu", - phone: "626-395-3405", - libraryName: "Caltech Library", - libraryLink: "https://library.caltech.edu/", - mailCode: "Mail Code 1-43", - instagram: "https://www.instagram.com/caltechlibrary/", - youtube: "https://www.youtube.com/channel/UCQbC4mcNNqypGMRtjgcN0SA", - logo: "library", - donateLink: "https://library.caltech.edu/about/support", - donateText: "Donate" - }, - archives: { - email: "archives@caltech.edu", - phone: "626-395-2704", - libraryName: "Caltech Archives & Special Collections", - libraryLink: "https://library.caltech.edu/archives", - mailCode: "Mail Code B215A-74", - instagram: "https://www.instagram.com/caltecharchives/", - youtube: "https://www.youtube.com/channel/UCQbC4mcNNqypGMRtjgcN0SA", - logo: "archives", - donateLink: "https://library.caltech.edu/archives/about/donate", - donateText: "Donate & Transfer" - } -}; -var FooterGlobal = class extends HTMLElement { - constructor() { - super(); - const shadow = this.attachShadow({ mode: "open" }); - const template = document.createElement("template"); - template.innerHTML = ` - - - - - - - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - this.addEventListener("mouseover", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "underline"; - } - }); - this.addEventListener("mouseout", (e) => { - if (e.target.classList.contains("custom-links")) { - e.target.style.textDecoration = "none"; - } - }); - } - // Gets the active preset configuration based on attributes - // Library is the default; archives overrides when attribute is present - getPreset() { - if (this.hasAttribute("archives")) return PRESETS.archives; - return PRESETS.library; - } - // Gets attribute value with preset fallback - // Priority: 1) explicit attribute, 2) preset value - getAttr(attrName, presetKey) { - if (this.hasAttribute(attrName)) { - return this.getAttribute(attrName); - } - const preset = this.getPreset(); - return preset[presetKey]; - } - // Handles slot fallback, attribute wiring, and dynamic content. - connectedCallback() { - console.log("\u{1F4E6} FooterGlobal connected"); - this.hoursUrl = "https://libcal.caltech.edu/api_hours_today.php?lid=0&format=json&systemTime=0"; - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (!slot1) { - console.warn("\u26A0\uFE0F slot[name='custom-links'] not found"); - return; - } - const assignedNodes = slot1.assignedNodes({ flatten: true }); - if (assignedNodes.length === 0 && !this.hasAttribute("custom")) { - console.log("\u{1FAB6} No slotted content detected at load \u2014 using default"); - this.loadDefaultColumn1(); - } - if (this.getAttribute("breadcrumbs") === "true") { - const breadcrumbEl = document.getElementById("s-lib-bc"); - const breadcrumbTarget = this.shadowRoot.getElementById("footer-breadcrumbs"); - if (breadcrumbEl && breadcrumbTarget) { - const clone = breadcrumbEl.cloneNode(true); - clone.id = "s-lib-bc-footer"; - breadcrumbTarget.appendChild(clone); - console.log("\u{1F517} Breadcrumb mirrored into footer component"); - } else { - console.log("\u2139\uFE0F Breadcrumb not found or target missing"); - } - } - if (this.getAttribute("libguides-login") === "true") { - console.log("\u{1F9EA} Detected Libguides-login=true"); - const sourceWrapper = document.getElementById("s-lib-footer-login-link"); - if (!sourceWrapper) { - console.warn("\u274C s-lib-footer-login-link not found"); - return; - } - const sourceLink = sourceWrapper.querySelector("a"); - if (!sourceLink) { - console.warn("\u{1F6AB} Login not found inside s-lib-footer-login-link"); - return; - } - console.log("\u2705 Login link found:", sourceLink.href); - const login_link = document.createElement("a"); - login_link.setAttribute("href", sourceLink.href); - login_link.setAttribute("aria-label", "Staff Login"); - login_link.innerHTML = ` - - `; - const shadowTarget = this.shadowRoot?.getElementById("footer-login"); - if (!shadowTarget) { - console.warn("\u26A0\uFE0F Could not find #footer-login inside shadow DOM"); - return; - } - console.log("\u{1F680} Injecting login link into shadow DOM"); - shadowTarget.appendChild(login_link); - } - slot1.addEventListener("slotchange", () => { - const hasContent = slot1.assignedNodes().length > 0; - const hasCustomAttr = this.hasAttribute("custom"); - const fallback = this.shadowRoot.querySelector("#default-column1"); - const shouldHideHours = hasContent || hasCustomAttr; - fallback.style.display = shouldHideHours ? "none" : "block"; - fallback.setAttribute("aria-hidden", shouldHideHours ? "true" : "false"); - if (!shouldHideHours) { - this.loadDefaultColumn1(); - } - }); - const email = this.getAttr("email", "email"); - const emailLink = this.shadowRoot.getElementById("email-link"); - if (emailLink) { - emailLink.href = `mailto:${email}`; - emailLink.textContent = email; - } - const phone = this.getAttr("phone", "phone"); - const phoneLink = this.shadowRoot.getElementById("phone-link"); - if (phoneLink) { - phoneLink.href = `tel:${phone.replace(/\D/g, "")}`; - phoneLink.textContent = phone; - } - const libraryName = this.getAttr("library-name", "libraryName"); - const libraryLink = this.getAttr("library-link", "libraryLink"); - const libraryNameLink = this.shadowRoot.getElementById("library-name"); - if (libraryNameLink) { - libraryNameLink.innerHTML = libraryName; - libraryNameLink.href = libraryLink; - } - const mailcode = this.getAttr("mail-code", "mailCode"); - const mailCode = this.shadowRoot.getElementById("mail-code"); - if (mailCode) { - mailCode.textContent = mailcode; - } - const headerEl = this.shadowRoot.getElementById("column1-header"); - if (headerEl) { - const hasCustomAttr = this.hasAttribute("custom"); - const hasHeaderAttr = this.hasAttribute("header"); - if (hasCustomAttr && !hasHeaderAttr) { - headerEl.style.display = "none"; - } else { - const headerText = this.getAttribute("header") || "Hours"; - headerEl.textContent = headerText; - headerEl.style.display = "block"; - } - } - const instagramAnchor = this.shadowRoot.getElementById("instagram-link"); - const instagramHref = this.getAttr("instagram", "instagram"); - const youtubeHref = this.getAttr("youtube", "youtube"); - const youtubeAnchor = this.shadowRoot.getElementById("youtube-link"); - const rssHref = this.getAttribute("rss"); - if (instagramAnchor) instagramAnchor.setAttribute("href", instagramHref); - if (youtubeAnchor) youtubeAnchor.setAttribute("href", youtubeHref); - if (rssHref) { - const rssAnchor = document.createElement("a"); - rssAnchor.setAttribute("id", "rss-link"); - rssAnchor.setAttribute("href", rssHref); - rssAnchor.innerHTML = ` - - RSS Feed - - - `; - if (this.shadowRoot) { - const socialDiv = this.shadowRoot.querySelector(".social"); - if (socialDiv) { - socialDiv.appendChild(rssAnchor); - } - } - } - const logoType = this.getAttr("logo", "logo").toLowerCase(); - const logoContainer = this.shadowRoot.querySelector(".footer-logo-container"); - if (logoContainer) { - const templateId = logoType === "archives" ? "archives-logo" : "library-logo"; - const logoTemplate = this.shadowRoot.getElementById(templateId); - console.log("Logo type:", logoType); - console.log("Template ID:", templateId); - console.log("Logo template found:", !!logoTemplate); - if (logoTemplate) { - logoContainer.innerHTML = ""; - logoContainer.appendChild(logoTemplate.content.cloneNode(true)); - } - } - const donateLink = this.getAttr("donate-link", "donateLink"); - const donateText = this.getAttr("donate-text", "donateText"); - const donateLinkEl = this.shadowRoot.getElementById("donate-link"); - if (donateLinkEl) { - donateLinkEl.href = donateLink; - donateLinkEl.textContent = donateText; - } - } - // If no Slot, load column1 default content - Library Hours - async loadDefaultColumn1() { - const container = this.shadowRoot.querySelector("#default-column1"); - try { - const response = await fetch(this.hoursUrl); - const data = await response.json(); - const locations = data.locations; - const column1Content = document.createElement("div"); - column1Content.id = "column1"; - column1Content.classList.add("column1-default"); - const rows = locations.map((loc) => { - const name = loc.name; - const url = loc.url; - const hours = loc.rendered || "\u2014"; - return ` -
- ${name} - ${hours} -
`; - }).join(""); - const todayString = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric" - }); - column1Content.innerHTML = ` -
-
${todayString}
- ${rows} - -
- `; - const slot1 = this.shadowRoot.querySelector('slot[name="custom-links"]'); - if (slot1 && slot1.parentNode) { - slot1.parentNode.insertBefore(column1Content, slot1); - } else { - container.appendChild(column1Content); - } - } catch (error) { - console.error("Error loading library hours:", error); - container.innerHTML = `

Unable to load library hours at this time.

`; - } - } -}; -customElements.define("footer-global", FooterGlobal); -export { - FooterGlobal -}; diff --git a/formHandler.js b/formHandler.js deleted file mode 100644 index e291b27..0000000 --- a/formHandler.js +++ /dev/null @@ -1,28 +0,0 @@ -// src/formHandler.js -async function submitHybridForm(formId, callback) { - const form = document.querySelector(`form#${formId}`); - if (!form) { - console.error(`Form with id "${formId}" not found.`); - return; - } - const formData = new FormData(form); - form.querySelectorAll("csv-textarea").forEach((csvTextarea) => { - formData.append(csvTextarea.getAttribute("name"), csvTextarea.innerHTML); - }); - try { - const response = await fetch(form.action, { - method: form.method, - body: formData - }); - if (response.ok) { - callback(response); - } else { - console.error(`Form submission failed with status: ${response.status}`); - } - } catch (error) { - console.error("Error submitting form:", error); - } -} -export { - submitHybridForm -}; diff --git a/invalidate_cdn.bash b/invalidate_cdn.bash deleted file mode 100755 index c5d5e7b..0000000 --- a/invalidate_cdn.bash +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# -# Publish takes the content in the htdocs directory and copies it to the S3 bucket indicated by the environment -# variable BUCKET. -# -BUCKET="s3://media.library.caltech.edu" -if [ -f "media.env" ]; then - echo "Loading environment from media.env" - . media.env -fi -if [ "$BUCKET_NAME" = "" ]; then - echo "No bucket to publish, set BUCKET_NAME" - exit 1 -fi - -# Invalidate cloud front -do_invalidate_cdn() { - INVALID_PATH="$1" - if [ "$DISTRIBUTION_ID" != "" ]; then - echo "Invalidating CDN" - aws cloudfront create-invalidation \ - --distribution-id "$DISTRIBUTION_ID" \ - --paths "${INVALID_PATH}" - else - echo "skipping invalidation, DISTRIBUTION_ID not set" - fi -} - - -echo "Invalidating CDN for $BUCKET_NAME" - -if [ "$1" != "" ]; then - do_invalidate_cdn "$1" -else - do_invalidate_cdn '/*' -fi diff --git a/publish_to_s3.bash b/publish_to_s3.bash deleted file mode 100755 index 0cd8345..0000000 --- a/publish_to_s3.bash +++ /dev/null @@ -1,203 +0,0 @@ -#!/bin/bash - -function usage() { - APP_NAME="$(basename "$0")" - cat < - table { - width: 100%; - border-collapse: collapse; - } - th, td { - border: 1px solid #ddd; - padding: 8px; - text-align: left; - } - th { - background-color: #f2f2f2; - cursor: pointer; - } - th:hover { - background-color: #ddd; - } - .search-container { - margin-bottom: 10px; - } - input[type="text"] { - padding: 6px; - margin-top: 8px; - margin-right: 8px; - border: 1px solid #ddd; - border-radius: 4px; - } - -
- - -
-
- `; - this.tableContainer = this.shadowRoot.getElementById("tableContainer"); - this.searchInput = this.shadowRoot.getElementById("searchInput"); - this.columnSelect = this.shadowRoot.getElementById("columnSelect"); - } - connectedCallback() { - const table = this.querySelector("table"); - if (table) { - const tableClone = table.cloneNode(true); - this.tableContainer.appendChild(tableClone); - this.setupTableSortable(tableClone); - this.setupSearch(tableClone); - } - } - setupTableSortable(table) { - const headers = table.querySelectorAll("thead th"); - const tbody = table.querySelector("tbody"); - const rows = tbody.querySelectorAll("tr"); - headers.forEach((header, index) => { - const option = document.createElement("option"); - option.value = index; - option.textContent = header.textContent.trim(); - this.columnSelect.appendChild(option); - }); - headers.forEach((header, index) => { - header.addEventListener("click", () => { - this.sortTable(index, rows); - }); - }); - } - setupSearch(table) { - this.searchInput.addEventListener("input", () => { - const searchTerm = this.searchInput.value.toLowerCase(); - const columnIndex = parseInt(this.columnSelect.value); - const rows = table.querySelectorAll("tbody tr"); - rows.forEach((row) => { - const cell = row.cells[columnIndex]; - const cellText = cell.textContent.toLowerCase(); - if (cellText.includes(searchTerm)) { - row.style.display = ""; - } else { - row.style.display = "none"; - } - }); - }); - } - sortTable(columnIndex, rows) { - const tbody = this.tableContainer.querySelector("tbody"); - const rowsArray = Array.from(rows); - const isAscending = !this.isSortedAscending(columnIndex); - rowsArray.sort((rowA, rowB) => { - const cellA = rowA.cells[columnIndex].textContent; - const cellB = rowB.cells[columnIndex].textContent; - return isAscending ? cellA.localeCompare(cellB) : cellB.localeCompare(cellA); - }); - while (tbody.firstChild) { - tbody.removeChild(tbody.firstChild); - } - rowsArray.forEach((row) => tbody.appendChild(row)); - } - isSortedAscending(columnIndex) { - const table = this.tableContainer.querySelector("table"); - const rows = table.querySelectorAll("tbody tr"); - for (let i = 0; i < rows.length - 1; i++) { - const cellA = rows[i].cells[columnIndex].textContent; - const cellB = rows[i + 1].cells[columnIndex].textContent; - if (cellA.localeCompare(cellB) > 0) { - return false; - } - } - return true; - } -}; -customElements.define("table-sortable", TableSortable); -export { - TableSortable -}; diff --git a/textarea-agent-list.js b/textarea-agent-list.js deleted file mode 100644 index 9a9fd55..0000000 --- a/textarea-agent-list.js +++ /dev/null @@ -1,245 +0,0 @@ -// src/textarea-agent-list.js -var TextareaAgentList = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: "open" }); - } - connectedCallback() { - this.loadStyles(); - this.renderContent(); - this.renderList(); - this.setupMutationObserver(); - } - loadStyles() { - const cssHref = this.getAttribute("css-href"); - if (cssHref) { - const link = document.createElement("link"); - link.setAttribute("rel", "stylesheet"); - link.setAttribute("href", cssHref); - this.shadowRoot.appendChild(link); - } else { - const style = document.createElement("style"); - style.textContent = ` - ul { - list-style-type: none; - padding: 0; - } - li { - padding: 8px; - margin-bottom: 5px; - background-color: #f4f4f4; - border-radius: 4px; - display: flex; - justify-content: space-between; - align-items: center; - } - input { - width: 100%; - padding: 8px; - margin: 4px 0; - box-sizing: border-box; - } - button { - margin: 4px; - padding: 8px; - cursor: pointer; - } - `; - this.shadowRoot.appendChild(style); - } - } - renderContent() { - const container = document.createElement("div"); - const styleAttribute = this.getAttribute("style"); - if (styleAttribute) { - container.setAttribute("style", styleAttribute); - } - const personOnly = this.hasAttribute("people-only"); - const organizationOnly = this.hasAttribute("organization-only"); - let buttonsHTML = ""; - if (!personOnly) { - buttonsHTML += ``; - } - if (!organizationOnly) { - buttonsHTML += ``; - } - container.innerHTML = ` -
    - ${buttonsHTML} - `; - this.shadowRoot.appendChild(container); - this.agentList = this.shadowRoot.querySelector("#agentList"); - if (!personOnly) { - const addOrganizationButton = this.shadowRoot.getElementById("addOrganization"); - if (addOrganizationButton) { - addOrganizationButton.addEventListener("click", () => this.addAgent("organization")); - } - } - if (!organizationOnly) { - const addPersonButton = this.shadowRoot.getElementById("addPerson"); - if (addPersonButton) { - addPersonButton.addEventListener("click", () => this.addAgent("person")); - } - } - } - renderList() { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - this.agentList.innerHTML = ""; - const personOnly = this.hasAttribute("person-only"); - const organizationOnly = this.hasAttribute("organization-only"); - parsedContent.forEach((agent, index) => { - const li = document.createElement("li"); - const styleAttribute = this.getAttribute("style"); - if (styleAttribute) { - li.setAttribute("style", styleAttribute); - } - if (!organizationOnly && (agent.family_name !== void 0 || agent.given_name !== void 0 || agent.orcid !== void 0)) { - li.innerHTML = ` -
    - - - -
    - - `; - } else if (!personOnly && (agent.name !== void 0 || agent.ror !== void 0)) { - li.innerHTML = ` -
    - - -
    - - `; - } - this.agentList.appendChild(li); - }); - this.setupEventListeners(); - } catch (e) { - console.error("Error parsing JSON:", e); - } - } - } - setupMutationObserver() { - const textarea = this.querySelector("textarea"); - if (textarea) { - this.handleJsonChange({ target: textarea }); - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "characterData") { - this.handleJsonChange({ target: textarea }); - } - }); - }); - const config = { characterData: true, childList: false, subtree: true }; - observer.observe(textarea, config); - } - } - handleJsonChange(event) { - if (event.target.value !== "") { - try { - const jsonContent = event.target.value; - const parsedContent = JSON.parse(jsonContent); - console.log("Parsed JSON:", parsedContent); - } catch (e) { - console.error("Error parsing JSON:", e); - } - } - } - setupEventListeners() { - this.shadowRoot.querySelectorAll("input").forEach((input) => { - input.addEventListener("change", this.handleInputChange.bind(this)); - input.addEventListener("input", this.validateInput.bind(this)); - }); - this.shadowRoot.querySelectorAll(".removeAgent").forEach((button) => { - button.addEventListener("click", this.handleRemoveAgent.bind(this)); - }); - } - validateInput(event) { - const input = event.target; - const isValid = input.checkValidity(); - if (input.hasAttribute("pattern")) { - if (isValid) { - input.style.borderColor = "green"; - } else { - input.style.borderColor = "red"; - } - } - } - handleInputChange(event) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - const index = event.target.getAttribute("data-index"); - const field = event.target.getAttribute("data-field"); - parsedContent[index][field] = event.target.value; - textarea.value = JSON.stringify(parsedContent, null, 2); - } catch (e) { - console.error("Error updating JSON:", e); - } - } - } - handleRemoveAgent(event) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const index = event.target.getAttribute("data-index"); - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - parsedContent.splice(index, 1); - textarea.value = JSON.stringify(parsedContent, null, 2); - this.renderList(); - } catch (e) { - console.error("Error removing agent:", e); - } - } - } - addAgent(type) { - const textarea = this.querySelector("textarea"); - if (textarea) { - if (textarea.value === "") { - textarea.value = "[]"; - } - try { - const jsonContent = textarea.value; - const parsedContent = JSON.parse(jsonContent); - const personOnly = this.hasAttribute("people-only"); - const organizationOnly = this.hasAttribute("organization-only"); - if (!organizationOnly && type === "person" || personOnly) { - parsedContent.push({ - family_name: "", - given_name: "", - orcid: "" - }); - } else if (!personOnly && type === "organization" || organizationOnly) { - parsedContent.push({ - name: "", - ror: "" - }); - } - textarea.value = JSON.stringify(parsedContent, null, 2); - this.renderList(); - } catch (e) { - console.error("Error adding agent:", e); - } - } - } -}; -customElements.define("textarea-agent-list", TextareaAgentList); -export { - TextareaAgentList -}; diff --git a/textarea-csv.js b/textarea-csv.js deleted file mode 100644 index aa1ebb5..0000000 --- a/textarea-csv.js +++ /dev/null @@ -1,805 +0,0 @@ -// deno:https://jsr.io/@std/csv/1.0.6/_shared.ts -function codePointLength(s) { - return Array.from(s).length; -} - -// deno:https://jsr.io/@std/csv/1.0.6/_io.ts -function createBareQuoteErrorMessage(zeroBasedRecordStartLine, zeroBasedLine, zeroBasedColumn) { - return `Syntax error on line ${zeroBasedRecordStartLine + 1}; parse error on line ${zeroBasedLine + 1}, column ${zeroBasedColumn + 1}: bare " in non-quoted-field`; -} -function createQuoteErrorMessage(zeroBasedRecordStartLine, zeroBasedLine, zeroBasedColumn) { - return `Syntax error on line ${zeroBasedRecordStartLine + 1}; parse error on line ${zeroBasedLine + 1}, column ${zeroBasedColumn + 1}: extraneous or missing " in quoted-field`; -} -function convertRowToObject(row, headers, zeroBasedLine) { - if (row.length !== headers.length) { - throw new Error(`Syntax error on line ${zeroBasedLine + 1}: The record has ${row.length} fields, but the header has ${headers.length} fields`); - } - const out = {}; - for (const [index, header] of headers.entries()) { - out[header] = row[index]; - } - return out; -} - -// deno:https://jsr.io/@std/csv/1.0.6/parse.ts -var BYTE_ORDER_MARK = "\uFEFF"; -var Parser = class { - #input = ""; - #cursor = 0; - #options; - constructor({ separator = ",", trimLeadingSpace = false, comment, lazyQuotes, fieldsPerRecord } = {}) { - this.#options = { - separator, - trimLeadingSpace, - comment, - lazyQuotes, - fieldsPerRecord - }; - } - #readLine() { - if (this.#isEOF()) return null; - let buffer = ""; - let hadNewline = false; - while (this.#cursor < this.#input.length) { - if (this.#input.startsWith("\r\n", this.#cursor)) { - hadNewline = true; - this.#cursor += 2; - break; - } - if (this.#input.startsWith("\n", this.#cursor)) { - hadNewline = true; - this.#cursor += 1; - break; - } - buffer += this.#input[this.#cursor]; - this.#cursor += 1; - } - if (!hadNewline && buffer.endsWith("\r")) { - buffer = buffer.slice(0, -1); - } - return buffer; - } - #isEOF() { - return this.#cursor >= this.#input.length; - } - #parseRecord(zeroBasedStartLine) { - let fullLine = this.#readLine(); - if (fullLine === null) return null; - if (fullLine.length === 0) { - return []; - } - let zeroBasedLine = zeroBasedStartLine; - if (this.#options.comment && fullLine[0] === this.#options.comment) { - return []; - } - let line = fullLine; - const quote = '"'; - const quoteLen = quote.length; - const separatorLen = this.#options.separator.length; - let recordBuffer = ""; - const fieldIndexes = []; - parseField: while (true) { - if (this.#options.trimLeadingSpace) { - line = line.trimStart(); - } - if (line.length === 0 || !line.startsWith(quote)) { - const i = line.indexOf(this.#options.separator); - let field = line; - if (i >= 0) { - field = field.substring(0, i); - } - if (!this.#options.lazyQuotes) { - const j = field.indexOf(quote); - if (j >= 0) { - const col = codePointLength(fullLine.slice(0, fullLine.length - line.slice(j).length)); - throw new SyntaxError(createBareQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - } - recordBuffer += field; - fieldIndexes.push(recordBuffer.length); - if (i >= 0) { - line = line.substring(i + separatorLen); - continue parseField; - } - break parseField; - } else { - line = line.substring(quoteLen); - while (true) { - const i = line.indexOf(quote); - if (i >= 0) { - recordBuffer += line.substring(0, i); - line = line.substring(i + quoteLen); - if (line.startsWith(quote)) { - recordBuffer += quote; - line = line.substring(quoteLen); - } else if (line.startsWith(this.#options.separator)) { - line = line.substring(separatorLen); - fieldIndexes.push(recordBuffer.length); - continue parseField; - } else if (0 === line.length) { - fieldIndexes.push(recordBuffer.length); - break parseField; - } else if (this.#options.lazyQuotes) { - recordBuffer += quote; - } else { - const col = codePointLength(fullLine.slice(0, fullLine.length - line.length - quoteLen)); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - } else if (line.length > 0 || !this.#isEOF()) { - recordBuffer += line; - const r = this.#readLine(); - line = r ?? ""; - fullLine = line; - if (r === null) { - if (!this.#options.lazyQuotes) { - const col = codePointLength(fullLine); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - fieldIndexes.push(recordBuffer.length); - break parseField; - } - zeroBasedLine++; - recordBuffer += "\n"; - } else { - if (!this.#options.lazyQuotes) { - const col = codePointLength(fullLine); - throw new SyntaxError(createQuoteErrorMessage(zeroBasedStartLine, zeroBasedLine, col)); - } - fieldIndexes.push(recordBuffer.length); - break parseField; - } - } - } - } - const result = []; - let preIdx = 0; - for (const i of fieldIndexes) { - result.push(recordBuffer.slice(preIdx, i)); - preIdx = i; - } - return result; - } - parse(input) { - this.#input = input.startsWith(BYTE_ORDER_MARK) ? input.slice(1) : input; - this.#cursor = 0; - const result = []; - let lineResult; - let first = true; - let lineIndex = 0; - const INVALID_RUNE = [ - "\r", - "\n", - '"' - ]; - const options = this.#options; - if (INVALID_RUNE.includes(options.separator) || typeof options.comment === "string" && INVALID_RUNE.includes(options.comment) || options.separator === options.comment) { - throw new Error("Cannot parse input: invalid delimiter"); - } - let _nbFields; - if (options.fieldsPerRecord === void 0 || options.fieldsPerRecord < 0) { - _nbFields = "ANY"; - } else if (options.fieldsPerRecord === 0) { - _nbFields = "UNINITIALIZED"; - } else { - _nbFields = options.fieldsPerRecord; - } - while (true) { - const r = this.#parseRecord(lineIndex); - if (r === null) break; - lineResult = r; - lineIndex++; - if (first) { - first = false; - if (_nbFields === "UNINITIALIZED") { - _nbFields = lineResult.length; - } - } - if (lineResult.length > 0) { - if (typeof _nbFields === "number" && _nbFields !== lineResult.length) { - throw new SyntaxError(`Syntax error on line ${lineIndex}: expected ${_nbFields} fields but got ${lineResult.length}`); - } - result.push(lineResult); - } - } - return result; - } -}; -function parse(input, options = { - skipFirstRow: false -}) { - const parser = new Parser(options); - const r = parser.parse(input); - if (options.skipFirstRow || options.columns) { - let headers = []; - if (options.skipFirstRow) { - const head = r.shift(); - if (head === void 0) { - throw new TypeError("Cannot parse input: headers must be defined"); - } - headers = head; - } - if (options.columns) { - headers = options.columns; - } - const zeroBasedFirstLineIndex = options.skipFirstRow ? 1 : 0; - return r.map((row, i) => { - return convertRowToObject(row, headers, zeroBasedFirstLineIndex + i); - }); - } - return r; -} - -// deno:https://jsr.io/@std/streams/1.0.10/_common.ts -var DEFAULT_BUFFER_SIZE = 32 * 1024; -function createLPS(pat) { - const length = pat.length; - const lps = new Uint8Array(length); - lps[0] = 0; - let prefixEnd = 0; - let i = 1; - while (i < length) { - if (pat[i] === pat[prefixEnd]) { - prefixEnd++; - lps[i] = prefixEnd; - i++; - } else if (prefixEnd === 0) { - lps[i] = 0; - i++; - } else { - prefixEnd = lps[prefixEnd - 1]; - } - } - return lps; -} - -// deno:https://jsr.io/@std/streams/1.0.10/text_delimiter_stream.ts -var TextDelimiterStream = class extends TransformStream { - #buf = ""; - #delimiter; - #inspectIndex = 0; - #matchIndex = 0; - #delimLPS; - #disp; - /** - * Constructs a new instance. - * - * @param delimiter A delimiter to split the stream by. - * @param options Options for the stream. - */ - constructor(delimiter, options) { - super({ - transform: (chunk, controller) => { - this.#handle(chunk, controller); - }, - flush: (controller) => { - controller.enqueue(this.#buf); - } - }); - this.#delimiter = delimiter; - this.#delimLPS = createLPS(new TextEncoder().encode(delimiter)); - this.#disp = options?.disposition ?? "discard"; - } - #handle(chunk, controller) { - this.#buf += chunk; - let localIndex = 0; - while (this.#inspectIndex < this.#buf.length) { - if (chunk[localIndex] === this.#delimiter[this.#matchIndex]) { - this.#inspectIndex++; - localIndex++; - this.#matchIndex++; - if (this.#matchIndex === this.#delimiter.length) { - const start = this.#inspectIndex - this.#delimiter.length; - const end = this.#disp === "suffix" ? this.#inspectIndex : start; - const copy = this.#buf.slice(0, end); - controller.enqueue(copy); - const shift = this.#disp === "prefix" ? start : this.#inspectIndex; - this.#buf = this.#buf.slice(shift); - this.#inspectIndex = this.#disp === "prefix" ? this.#delimiter.length : 0; - this.#matchIndex = 0; - } - } else { - if (this.#matchIndex === 0) { - this.#inspectIndex++; - localIndex++; - } else { - this.#matchIndex = this.#delimLPS[this.#matchIndex - 1]; - } - } - } - } -}; - -// deno:https://jsr.io/@std/csv/1.0.6/stringify.ts -var QUOTE = '"'; -var LF = "\n"; -var CRLF = "\r\n"; -var BYTE_ORDER_MARK2 = "\uFEFF"; -function getEscapedString(value, sep) { - if (value === void 0 || value === null) return ""; - let str = ""; - if (typeof value === "object") str = JSON.stringify(value); - else str = String(value); - if (str.includes(sep) || str.includes(LF) || str.includes(QUOTE)) { - return `${QUOTE}${str.replaceAll(QUOTE, `${QUOTE}${QUOTE}`)}${QUOTE}`; - } - return str; -} -function normalizeColumn(column) { - let header; - let prop; - if (typeof column === "object") { - if (Array.isArray(column)) { - header = String(column[column.length - 1]); - prop = column; - } else { - prop = Array.isArray(column.prop) ? column.prop : [ - column.prop - ]; - header = typeof column.header === "string" ? column.header : String(prop[prop.length - 1]); - } - } else { - header = String(column); - prop = [ - column - ]; - } - return { - header, - prop - }; -} -function getValuesFromItem(item, normalizedColumns) { - const values = []; - if (normalizedColumns.length) { - for (const column of normalizedColumns) { - let value = item; - for (const prop of column.prop) { - if (typeof value !== "object" || value === null) { - continue; - } - if (Array.isArray(value)) { - if (typeof prop === "number") value = value[prop]; - else { - throw new TypeError('Property accessor is not of type "number"'); - } - } else value = value[prop]; - } - values.push(value); - } - } else { - if (Array.isArray(item)) { - values.push(...item); - } else if (typeof item === "object") { - throw new TypeError("No property accessor function was provided for object"); - } else { - values.push(item); - } - } - return values; -} -function stringify(data, options) { - const { headers = true, separator: sep = ",", columns = [], bom = false } = options ?? {}; - if (sep.includes(QUOTE) || sep.includes(CRLF)) { - const message = [ - "Separator cannot include the following strings:", - ' - U+0022: Quotation mark (")', - " - U+000D U+000A: Carriage Return + Line Feed (\\r\\n)" - ].join("\n"); - throw new TypeError(message); - } - const normalizedColumns = columns.map(normalizeColumn); - let output = ""; - if (bom) { - output += BYTE_ORDER_MARK2; - } - if (headers && normalizedColumns.length > 0) { - output += normalizedColumns.map((column) => getEscapedString(column.header, sep)).join(sep); - output += CRLF; - } - for (const item of data) { - const values = getValuesFromItem(item, normalizedColumns); - output += values.map((value) => getEscapedString(value, sep)).join(sep); - output += CRLF; - } - return output; -} - -// deno:https://jsr.io/@std/csv/1.0.6/stringify_stream.ts -var CsvStringifyStream = class extends TransformStream { - /** - * Construct a new instance. - * - * @param options Options for the stream. - */ - constructor(options) { - const { separator, columns = [] } = options ?? {}; - super({ - start(controller) { - if (columns && columns.length > 0) { - try { - controller.enqueue(stringify([ - columns - ], separator !== void 0 ? { - separator, - headers: false - } : { - headers: false - })); - } catch (error) { - controller.error(error); - } - } - }, - transform(chunk, controller) { - try { - controller.enqueue(stringify([ - chunk - ], separator !== void 0 ? { - separator, - headers: false, - columns - } : { - headers: false, - columns - })); - } catch (error) { - controller.error(error); - } - } - }); - } -}; - -// src/textarea-csv.js -var TextareaCSV = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: "open" }); - this.isComponentInitialized = false; - this.custcomCleanupFilter = void 0; - } - static get observedAttributes() { - return ["column-headings", "id", "class", "caption", "text", "placeholder", "css-href", "debug"]; - } - attributeChangedCallback(name) { - if (name === "column-headings" && this.isComponentInitialized) { - this.initializeTable(); - } - } - connectedCallback() { - this.initializeComponent(); - this.isComponentInitialized = true; - this.initializeTable(); - this.setupEventListeners(); - this.setupMutationObserver(); - } - setupMutationObserver() { - const targetNode = this.shadowRoot.querySelector("tbody"); - const config = { - attributes: true, - childList: true, - subtree: true, - attributeFilter: ["value"] - // This ensures we only observe changes to the 'value' attribute - }; - const callback = (mutationsList, _observer) => { - for (const mutation of mutationsList) { - if (mutation.type === "attributes" && mutation.attributeName === "value") { - this.toTextarea(); - } else if (mutation.type === "childList") { - this.toTextarea(); - } - } - }; - this.observer = new MutationObserver(callback); - if (targetNode) { - this.observer.observe(targetNode, config); - } - } - disconnectedCallback() { - if (this.observer) { - this.observer.disconnect(); - } - } - initializeComponent() { - const template = document.createElement("template"); - template.innerHTML = ` - - - - -
    - - - ${this.hasAttribute("debug") ? '' : ""} - ${this.hasAttribute("title") || this.hasAttribute("help-description") ? '\u24D8' : ""} - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - if (this.hasAttribute("css-href")) { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = this.getAttribute("css-href"); - this.shadowRoot.appendChild(link); - } - if (this.hasAttribute("title") || this.hasAttribute("help-description")) { - const helpIcon = this.shadowRoot.getElementById("help-icon"); - helpIcon.addEventListener("click", () => { - alert(`${this.getAttribute("title") || ""} -${this.getAttribute("help-description") || ""}`); - }); - } - } - initializeTable() { - const headings = parseCSVRow(this.getAttribute("column-headings")); - const table = this.shadowRoot.querySelector("table"); - const thead = table.querySelector("thead"); - thead.innerHTML = ""; - const tr = document.createElement("tr"); - headings.forEach((heading) => { - const th = document.createElement("th"); - th.textContent = heading; - tr.appendChild(th); - }); - thead.appendChild(tr); - const textarea = this.querySelector("textarea"); - if (textarea && textarea.value.trim()) { - this.fromTextarea(); - } else { - this.appendRow(); - } - const datalists = this.querySelectorAll("datalist"); - datalists.forEach((datalist) => { - this.shadowRoot.appendChild(datalist.cloneNode(true)); - }); - headings.forEach((heading, index) => { - const datalist = this.shadowRoot.querySelector(`datalist#${heading.toLowerCase()}`); - if (datalist) { - this.setAutocomplete(index, Array.from(datalist.options).map((option) => ({ value: option.value }))); - } - }); - } - setupEventListeners() { - this.shadowRoot.querySelector("#append-row").addEventListener("click", () => this.appendRow()); - this.shadowRoot.querySelector("#cleanup").addEventListener("click", () => this.cleanupTable()); - if (this.hasAttribute("debug")) { - this.shadowRoot.querySelector("#debug").addEventListener("click", () => { - console.log(this.toCSV()); - }); - } - this.shadowRoot.querySelector("tbody").addEventListener("input", (event) => { - if (event.target.tagName === "INPUT") { - const rowIndex = event.target.closest("tr").rowIndex - 1; - const colIndex = event.target.closest("td").cellIndex; - const value = event.target.value; - this.dispatchEvent(new CustomEvent("changed", { detail: { rowIndex, colIndex, value } })); - if (this.hasAttribute("debug")) { - console.log(`Cell changed: Row ${rowIndex}, Col ${colIndex}, Value ${value}`); - } - } - }); - this.shadowRoot.querySelector("tbody").addEventListener("focus", (event) => { - if (event.target.tagName === "INPUT") { - const rowIndex = event.target.closest("tr").rowIndex - 1; - const colIndex = event.target.closest("td").cellIndex; - const value = event.target.value; - this.dispatchEvent(new CustomEvent("focused", { detail: { rowIndex, colIndex, value } })); - if (this.hasAttribute("debug")) { - console.log(`Cell focused: Row ${rowIndex}, Col ${colIndex}, Value ${value}`); - } - } - }, true); - this.shadowRoot.querySelector("tbody").addEventListener("keydown", (event) => { - if (event.key === "Backspace" && event.target.tagName === "INPUT") { - const input = event.target; - const start = input.selectionStart; - const end = input.selectionEnd; - if (start === end && start > 0) { - input.value = input.value.slice(0, start - 1) + input.value.slice(end); - input.selectionStart = input.selectionEnd = start - 1; - event.preventDefault(); - } - } - }); - } - rowCount() { - return this.shadowRoot.querySelector("tbody").rows.length; - } - columnCount() { - return this.shadowRoot.querySelector("thead").rows[0].cells.length; - } - isEmptyRow(rowIndex) { - const row = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1})`); - return Array.from(row.cells).every((cell) => cell.querySelector("input").value === ""); - } - appendRow() { - const tbody = this.shadowRoot.querySelector("tbody"); - const row = document.createElement("tr"); - for (let i = 0; i < this.columnCount(); i++) { - const td = document.createElement("td"); - const input = document.createElement("input"); - input.type = "text"; - input.placeholder = this.getAttribute("placeholder") || ""; - const datalistId = `column-${i}`; - const datalist = this.shadowRoot.querySelector(`datalist#${datalistId}`); - if (datalist) { - input.setAttribute("list", datalistId); - } - td.appendChild(input); - row.appendChild(td); - } - tbody.appendChild(row); - row.cells[0].querySelector("input").focus(); - } - cleanupTable() { - const tbody = this.shadowRoot.querySelector("tbody"); - const rows = tbody.rows; - for (let i = rows.length - 1; i >= 0; i--) { - if (this.isEmptyRow(i)) { - tbody.deleteRow(i); - } - if (this.customCleanupFilter !== void 0 && this.customCleanupFilter(rows[i]) === false) { - tbody.deleteRow(i); - } - } - } - toCSV() { - const rows = []; - const tbody = this.shadowRoot.querySelector("tbody").rows; - console.log(this.innertHTML); - for (let i = 0; i < tbody.length; i++) { - const cells = tbody[i].cells; - const row = []; - for (let j = 0; j < cells.length; j++) { - row.push(cells[j].querySelector("input").value); - } - rows.push(row); - } - return stringifyCSV(rows); - } - fromCSV(csvText) { - const rows = parse(csvText); - const tbody = this.shadowRoot.querySelector("tbody"); - tbody.innerHTML = ""; - rows.forEach((row) => { - const tr = document.createElement("tr"); - row.forEach((cell) => { - const td = document.createElement("td"); - const input = document.createElement("input"); - input.type = "text"; - input.value = cell; - td.appendChild(input); - tr.appendChild(td); - }); - tbody.appendChild(tr); - }); - } - toObjects() { - const objects = []; - const tbody = this.shadowRoot.querySelector("tbody").rows; - for (let i = 0; i < tbody.length; i++) { - const cells = tbody[i].cells; - for (let j = 0; j < cells.length; j++) { - objects.push({ rowIndex: i, colIndex: j, value: cells[j].querySelector("input").value }); - } - } - return objects; - } - fromObjects(objects) { - const tbody = this.shadowRoot.querySelector("tbody"); - tbody.innerHTML = ""; - objects.forEach((obj) => { - let row = tbody.rows[obj.rowIndex]; - if (!row) { - row = document.createElement("tr"); - tbody.appendChild(row); - } - let cell = row.cells[obj.colIndex]; - if (!cell) { - cell = document.createElement("td"); - row.appendChild(cell); - } - let input = cell.querySelector("input"); - if (!input) { - input = document.createElement("input"); - input.type = "text"; - cell.appendChild(input); - } - input.value = obj.value; - }); - } - fromTextarea() { - const textarea = this.querySelector("textarea"); - if (textarea) { - this.fromCSV(textarea.value.trim()); - } - } - toTextarea() { - const textarea = this.querySelector("textarea"); - if (textarea) { - textarea.value = this.toCSV(); - console.log("Textarea updated with CSV data:", textarea.value); - } - } - setupFormListener() { - const form = this.closest("form"); - if (form) { - form.addEventListener("submit", (_event) => { - this.toTextarea(); - }); - } - } - getCellValue(rowIndex, colIndexOrName) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const cell = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1}) td:nth-child(${colIndex + 1}) input`); - return cell ? cell.value : ""; - } - setCellValue(rowIndex, colIndexOrName, value) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const cell = this.shadowRoot.querySelector(`tbody tr:nth-child(${rowIndex + 1}) td:nth-child(${colIndex + 1}) input`); - if (cell) { - cell.value = value; - } - } - toJSON() { - return JSON.stringify(this.toObjects()); - } - fromJSON(jsonString) { - this.fromObjects(JSON.parse(jsonString)); - } - setAutocomplete(colIndexOrName, options) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const datalistId = `column-${colIndex}`; - let datalist = this.shadowRoot.querySelector(`datalist#${datalistId}`); - if (!datalist) { - datalist = document.createElement("datalist"); - datalist.id = datalistId; - this.shadowRoot.appendChild(datalist); - } - datalist.innerHTML = ""; - options.forEach((option) => { - const opt = document.createElement("option"); - opt.value = option.value; - datalist.appendChild(opt); - }); - const inputs = this.shadowRoot.querySelectorAll(`tbody td:nth-child(${colIndex + 1}) input`); - inputs.forEach((input) => input.setAttribute("list", datalistId)); - } - getAutocomplete(colIndexOrName) { - const colIndex = typeof colIndexOrName === "number" ? colIndexOrName : this.getColumnIndexByName(colIndexOrName); - const datalist = this.shadowRoot.querySelector(`datalist#column-${colIndex}`); - if (datalist) { - const options = []; - datalist.querySelectorAll("option").forEach((option) => { - options.push({ value: option.value }); - }); - return options; - } - return void 0; - } - getColumnIndexByName(colName) { - const headings = parseCSVRow(this.getAttribute("column-headings")); - return headings.indexOf(colName); - } -}; -function parseCSVRow(csvRowString) { - const data = parse(csvRowString); - return data[0]; -} -function stringifyCSVRow(row) { - return stringify([row]); -} -function stringifyCSV(data) { - return stringify(data); -} -customElements.define("textarea-csv", TextareaCSV); -export { - TextareaCSV, - parseCSVRow, - stringifyCSV, - stringifyCSVRow -}; diff --git a/ul-a-to-z-list.js b/ul-a-to-z-list.js deleted file mode 100644 index 8f1086a..0000000 --- a/ul-a-to-z-list.js +++ /dev/null @@ -1,121 +0,0 @@ -// src/ul-a-to-z-list.js -var ULAtoZList = class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: "open" }); - } - connectedCallback() { - this.render(); - this.setupMutationObserver(); - } - setupMutationObserver() { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - this.render(); - } - }); - }); - observer.observe(this, { - attributes: false, - childList: true, - subtree: true - }); - } - render() { - const template = document.createElement("template"); - template.innerHTML = ` - - -
    - ${this.hasAttribute("long") ? 'Back to Menu' : ""} - `; - this.shadowRoot.appendChild(template.content.cloneNode(true)); - const listContainer = this.shadowRoot.querySelector("#list-container"); - const menu = this.shadowRoot.querySelector("#menu"); - const ulElement = this.querySelector("ul"); - if (!ulElement) return; - const items = Array.from(ulElement.querySelectorAll("li")); - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const sections = {}; - items.forEach((item) => { - const firstLetter = item.textContent.trim()[0].toUpperCase(); - if (!sections[firstLetter]) { - sections[firstLetter] = []; - } - sections[firstLetter].push(item); - }); - alphabet.split("").forEach((letter) => { - if (sections[letter]) { - const menuItem = document.createElement("li"); - const menuLink = document.createElement("a"); - menuLink.href = `#section-${letter}`; - menuLink.textContent = letter; - menuLink.addEventListener("click", (event) => { - event.preventDefault(); - const targetSection = this.shadowRoot.querySelector(`#section-${letter}`); - this.scrollToSection(targetSection); - }); - menuItem.appendChild(menuLink); - menu.appendChild(menuItem); - const section = document.createElement("ul"); - section.classList.add("letter-section"); - section.id = `section-${letter}`; - const sectionHeading = document.createElement("li"); - const sectionHeadingLink = document.createElement("a"); - sectionHeadingLink.href = `#menu`; - sectionHeadingLink.textContent = letter; - sectionHeadingLink.addEventListener("click", (event) => { - event.preventDefault(); - this.scrollToSection(menu); - }); - sectionHeading.appendChild(sectionHeadingLink); - section.appendChild(sectionHeading); - sections[letter].forEach((item) => { - const clonedItem = item.cloneNode(true); - section.appendChild(clonedItem); - }); - listContainer.appendChild(section); - } - }); - const backToMenuLink = this.shadowRoot.querySelector(".back-to-menu"); - if (backToMenuLink) { - backToMenuLink.addEventListener("click", (event) => { - event.preventDefault(); - this.scrollToSection(menu); - }); - } - } - scrollToSection(section) { - const yOffset = -100; - const y = section.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ - top: y, - behavior: "smooth" - }); - } -}; -customElements.define("ul-a-to-z-list", ULAtoZList); -export { - ULAtoZList -}; diff --git a/version.js b/version.js deleted file mode 100644 index 799a8da..0000000 --- a/version.js +++ /dev/null @@ -1,26 +0,0 @@ -// src/version.js -var version = "0.0.12"; -var releaseDate = "2025-07-17"; -var releaseHash = "9729d57"; -var licenseText = ` - -Copyright (c) 2025, Caltech -All rights not granted herein are expressly reserved by Caltech. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -`; -export { - licenseText, - releaseDate, - releaseHash, - version -};