/** * A DocumentSheet application responsible for displaying and editing a single Cards stack. */ class CardsConfig extends DocumentSheet { /** * The CardsConfig sheet is constructed by providing a Cards document and sheet-level options. * @param {Cards} object The {@link Cards} object being configured. * @param {DocumentSheetOptions} [options] Application configuration options. */ constructor(object, options) { super(object, options); this.options.classes.push(object.type); } /** * The allowed sorting methods which can be used for this sheet * @enum {string} */ static SORT_TYPES = { STANDARD: "standard", SHUFFLED: "shuffled" }; /* -------------------------------------------- */ /** @inheritdoc */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { classes: ["sheet", "cards-config"], template: "templates/cards/cards-deck.html", width: 620, height: "auto", closeOnSubmit: false, viewPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, dragDrop: [{dragSelector: "ol.cards li.card", dropSelector: "ol.cards"}], tabs: [{navSelector: ".tabs", contentSelector: "form", initial: "cards"}], scrollY: ["ol.cards"], sort: this.SORT_TYPES.SHUFFLED }); } /* -------------------------------------------- */ /** @inheritdoc */ getData(options={}) { // Sort cards const sortFn = { standard: this.object.sortStandard, shuffled: this.object.sortShuffled }[options?.sort || "standard"]; const cards = this.object.cards.contents.sort((a, b) => sortFn.call(this.object, a, b)); // Return rendering context return foundry.utils.mergeObject(super.getData(options), { cards: cards, types: CONFIG.Cards.typeLabels, inCompendium: !!this.object.pack }); } /* -------------------------------------------- */ /* Event Listeners and Handlers */ /* -------------------------------------------- */ /** @inheritdoc */ activateListeners(html) { super.activateListeners(html); // Card Actions html.find(".card-control").click(this._onCardControl.bind(this)); // Intersection Observer const cards = html.find("ol.cards"); const entries = cards.find("li.card"); const observer = new IntersectionObserver(this._onLazyLoadImage.bind(this), {root: cards[0]}); entries.each((i, li) => observer.observe(li)); } /* -------------------------------------------- */ /** * Handle card control actions which modify single cards on the sheet. * @param {PointerEvent} event The originating click event * @returns {Promise} A Promise which resolves once the handler has completed * @protected */ async _onCardControl(event) { const button = event.currentTarget; const li = button.closest(".card"); const card = li ? this.object.cards.get(li.dataset.cardId) : null; const cls = getDocumentClass("Card"); // Save any pending change to the form await this._onSubmit(event, {preventClose: true, preventRender: true}); // Handle the control action switch ( button.dataset.action ) { case "create": return cls.createDialog({ faces: [{}], face: 0 }, {parent: this.object, pack: this.object.pack}); case "edit": return card.sheet.render(true); case "delete": return card.deleteDialog(); case "deal": return this.object.dealDialog(); case "draw": return this.object.drawDialog(); case "pass": return this.object.passDialog(); case "play": return this.object.playDialog(card); case "reset": return this.object.resetDialog(); case "shuffle": this.options.sort = this.constructor.SORT_TYPES.SHUFFLED; return this.object.shuffle(); case "toggleSort": this.options.sort = {standard: "shuffled", shuffled: "standard"}[this.options.sort]; return this.render(); case "nextFace": return card.update({face: card.face === null ? 0 : card.face+1}); case "prevFace": return card.update({face: card.face === 0 ? null : card.face-1}); } } /* -------------------------------------------- */ /** * Handle lazy-loading card face images. * See {@link SidebarTab#_onLazyLoadImage} * @param {IntersectionObserverEntry[]} entries The entries which are now in the observer frame * @param {IntersectionObserver} observer The intersection observer instance * @protected */ _onLazyLoadImage(entries, observer) { return ui.cards._onLazyLoadImage.call(this, entries, observer); } /* -------------------------------------------- */ /** @inheritdoc */ _canDragStart(selector) { return this.isEditable; } /* -------------------------------------------- */ /** @inheritdoc */ _onDragStart(event) { const li = event.currentTarget; const card = this.object.cards.get(li.dataset.cardId); if ( !card ) return; // Set data transfer event.dataTransfer.setData("text/plain", JSON.stringify(card.toDragData())); } /* -------------------------------------------- */ /** @inheritdoc */ _canDragDrop(selector) { return this.isEditable; } /* -------------------------------------------- */ /** @inheritdoc */ async _onDrop(event) { const data = TextEditor.getDragEventData(event); if ( data.type !== "Card" ) return; const card = await Card.implementation.fromDropData(data); if ( card.parent.id === this.object.id ) return this._onSortCard(event, card); try { return await card.pass(this.object); } catch(err) { Hooks.onError("CardsConfig#_onDrop", err, {log: "error", notify: "error"}); } } /* -------------------------------------------- */ /** * Handle sorting a Card relative to other siblings within this document * @param {Event} event The drag drop event * @param {Card} card The card being dragged * @private */ _onSortCard(event, card) { // Identify a specific card as the drop target let target = null; const li = event.target.closest("[data-card-id]"); if ( li ) target = this.object.cards.get(li.dataset.cardId) ?? null; // Don't sort on yourself. if ( card === target ) return; // Identify the set of siblings const siblings = this.object.cards.filter(c => c.id !== card.id); // Perform an integer-based sort const updateData = SortingHelpers.performIntegerSort(card, {target, siblings}).map(u => { return {_id: u.target.id, sort: u.update.sort}; }); return this.object.updateEmbeddedDocuments("Card", updateData); } } /** * A subclass of CardsConfig which provides a sheet representation for Cards documents with the "hand" type. */ class CardsHand extends CardsConfig { static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { template: "templates/cards/cards-hand.html" }); } } /** * A subclass of CardsConfig which provides a sheet representation for Cards documents with the "pile" type. */ class CardsPile extends CardsConfig { static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { template: "templates/cards/cards-pile.html" }); } }