import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; import { closeWithAnimation, resetScrollOnBlur } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; import { classnames, graphemeCount, readFileAsDataUrl } from "/js/utils.js"; import { ImageCompressor } from "/js/imageCompressor.js"; import "/js/components/app-icon.js"; import "/js/components/image-cropper.js"; import "/js/components/context-menu.js"; import "/js/components/context-menu-item.js"; import "/js/components/context-menu-item-group.js"; import { confirmModal } from "/js/modals/confirm.modal.js"; const MAX_NAME_LENGTH = 64; const MAX_DESCRIPTION_LENGTH = 300; const CURATE_LIST_PURPOSE = "app.bsky.graph.defs#curatelist"; const MOD_LIST_PURPOSE = "app.bsky.graph.defs#modlist"; class CreateListDialog extends Component { connectedCallback() { if (this.initialized) { return; } this.setAttribute("data-dialog-wrapper", ""); this.scrollLock = null; this._name = ""; this._description = ""; this._newAvatarDataUrl = null; this._purpose = CURATE_LIST_PURPOSE; this._saving = false; this._error = null; this._croppingImageSrc = null; this._isOpen = false; this.innerHTML = ""; this.render(); this.initialized = true; } get _isDirty() { return ( this._name.length > 0 || this._description.length > 0 || this._newAvatarDataUrl !== null ); } get _isNameTooLong() { return graphemeCount(this._name) > MAX_NAME_LENGTH; } get _isNameEmpty() { return this._name.trim().length === 0; } get _isDescriptionTooLong() { return graphemeCount(this._description) > MAX_DESCRIPTION_LENGTH; } get _canSave() { return ( !this._saving && !this._isNameEmpty && !this._isNameTooLong && !this._isDescriptionTooLong ); } render() { const isCropping = !!this._croppingImageSrc; const nameCount = graphemeCount(this._name); const descriptionCount = graphemeCount(this._description); const avatarSrc = this._newAvatarDataUrl; render( html` { if (!isCropping && event.target === event.currentTarget) { if (await this.confirmClose()) { this.close(); } } }} @cancel=${async (event) => { event.preventDefault(); if (isCropping) { this._croppingImageSrc = null; this.render(); } else if (await this.confirmClose()) { this.close(); } }} @close=${() => { this.scrollLock?.release(); this.scrollLock = null; this.dispatchEvent(new CustomEvent("create-list-closed")); }} > ${isCropping ? html`

Edit image

` : html`

New list

List Avatar
this._openAvatarMenu()} >
${avatarSrc ? html`Avatar preview` : html``}
this._pickImage()} > Upload from Files ${avatarSrc ? html` { this._newAvatarDataUrl = null; this.render(); }} > Remove Avatar ` : ""}
List Type
{ this._purpose = event.target.value; this.render(); }} >
{ this._name = event.target.value; this.render(); }} data-testid="create-list-name" />
${nameCount}/${MAX_NAME_LENGTH}
${descriptionCount}/${MAX_DESCRIPTION_LENGTH}
${this._error ? html`
${this._error}
` : ""}
`} this._handleFileSelect(event)} @cancel=${(event) => { event.stopPropagation(); }} />
`, this, ); if (this._isOpen) { const dialog = this.querySelector(".create-list-dialog"); if (dialog && !dialog.open) { dialog.showModal(); } } } _openAvatarMenu() { const menu = this.querySelector(".create-list-avatar-menu"); const cameraButton = this.querySelector( ".form-dialog-camera-button-avatar", ); if (menu && cameraButton) { const rect = cameraButton.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.bottom; menu.open(x, y); } } _pickImage() { const input = this.querySelector(".create-list-file-input"); if (input) { input.click(); } } async _handleFileSelect(event) { const file = event.target.files?.[0]; if (!file || !file.type.startsWith("image/")) { event.target.value = ""; return; } const dataUrl = await readFileAsDataUrl(file); event.target.value = ""; this._croppingImageSrc = dataUrl; this.render(); } async _applyCrop() { const cropper = this.querySelector("image-cropper"); if (!cropper) return; const croppedDataUrl = cropper.cropImage(); if (!croppedDataUrl) return; this._newAvatarDataUrl = croppedDataUrl; this._croppingImageSrc = null; this.render(); } async _save() { this._saving = true; this._error = null; this.render(); try { let avatarBlob = null; if (this._newAvatarDataUrl) { const compressed = await new ImageCompressor().compressImage( this._newAvatarDataUrl, ); avatarBlob = compressed.blob; } const successCallback = () => { this.close(); }; const errorCallback = (error) => { console.error("Failed to create list:", error); this._error = "Failed to create list. Please try again."; this._saving = false; this.render(); }; this.dispatchEvent( new CustomEvent("list-create", { detail: { listData: { purpose: this._purpose, name: this._name, description: this._description, avatarBlob, }, successCallback, errorCallback, }, }), ); } catch (error) { console.error("Error creating list:", error); this._error = "Failed to create list. Please try again."; this._saving = false; this.render(); } } open() { this._isOpen = true; this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector(".create-list-dialog"); if (dialog?.open) return; if (dialog) { dialog.showModal(); this.querySelector("#create-list-name")?.focus({ preventScroll: true }); enableDragToDismiss(dialog, { confirmDismiss: () => this.confirmClose(), onDismiss: () => this.close(), scrollContainer: this.querySelector(".form-dialog-content"), dragHandle: this.querySelector(".form-dialog-header"), ignoreTouchTarget: (el) => !!el.closest("button") || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || !!el.closest("image-cropper"), disableWhenKeyboardOpen: true, }); resetScrollOnBlur(dialog, this.querySelector(".form-dialog-content")); } } async confirmClose() { if (!this._isDirty || !!this._croppingImageSrc || this._saving) return true; return confirmModal("Are you sure you want to discard this list?", { title: "Discard list?", confirmButtonStyle: "danger", confirmButtonText: "Discard", }); } close() { this._isOpen = false; return closeWithAnimation(this.querySelector(".create-list-dialog")); } disconnectedCallback() { this.scrollLock?.release(); this.scrollLock = null; } } CreateListDialog.register();