Initial
This commit is contained in:
1
resources/app/client-esm/applications/ui/_module.mjs
Normal file
1
resources/app/client-esm/applications/ui/_module.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export {default as RegionLegend} from "./region-legend.mjs";
|
||||
389
resources/app/client-esm/applications/ui/region-legend.mjs
Normal file
389
resources/app/client-esm/applications/ui/region-legend.mjs
Normal file
@@ -0,0 +1,389 @@
|
||||
import ApplicationV2 from "../api/application.mjs";
|
||||
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
|
||||
|
||||
/**
|
||||
* Scene Region Legend.
|
||||
* @extends ApplicationV2
|
||||
* @mixes HandlebarsApplication
|
||||
* @alias RegionLegend
|
||||
*/
|
||||
export default class RegionLegend extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "region-legend",
|
||||
tag: "aside",
|
||||
position: {
|
||||
width: 320,
|
||||
height: "auto"
|
||||
},
|
||||
window: {
|
||||
title: "REGION.LEGEND.title",
|
||||
icon: "fa-regular fa-game-board",
|
||||
minimizable: false
|
||||
},
|
||||
actions: {
|
||||
config: RegionLegend.#onConfig,
|
||||
control: RegionLegend.#onControl,
|
||||
create: RegionLegend.#onCreate,
|
||||
delete: RegionLegend.#onDelete,
|
||||
lock: RegionLegend.#onLock
|
||||
},
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
list: {
|
||||
id: "list",
|
||||
template: "templates/scene/region-legend.hbs",
|
||||
scrollable: ["ol.region-list"]
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The currently filtered Regions.
|
||||
* @type {{bottom: number, top: number}}
|
||||
*/
|
||||
#visibleRegions = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The currently viewed elevation range.
|
||||
* @type {{bottom: number, top: number}}
|
||||
*/
|
||||
elevation = {bottom: -Infinity, top: Infinity};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @type {SearchFilter} */
|
||||
#searchFilter = new SearchFilter({
|
||||
inputSelector: 'input[name="search"]',
|
||||
contentSelector: ".region-list",
|
||||
callback: this.#onSearchFilter.bind(this)
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Record a reference to the currently highlighted Region.
|
||||
* @type {Region|null}
|
||||
*/
|
||||
#hoveredRegion = null;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_configureRenderOptions(options) {
|
||||
super._configureRenderOptions(options);
|
||||
if ( options.isFirstRender ) {
|
||||
options.position.left ??= ui.nav?.element[0].getBoundingClientRect().left;
|
||||
options.position.top ??= ui.controls?.element[0].getBoundingClientRect().top;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_canRender(options) {
|
||||
const rc = options.renderContext;
|
||||
if ( rc && !["createregions", "updateregions", "deleteregions"].includes(rc) ) return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _renderFrame(options) {
|
||||
const frame = await super._renderFrame(options);
|
||||
this.window.close.remove(); // Prevent closing
|
||||
return frame;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async close(options={}) {
|
||||
if ( !options.closeKey ) return super.close(options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onFirstRender(context, options) {
|
||||
super._onFirstRender(context, options);
|
||||
canvas.scene.apps[this.id] = this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
this.#searchFilter.bind(this.element);
|
||||
for ( const li of this.element.querySelectorAll(".region") ) {
|
||||
li.addEventListener("mouseover", this.#onRegionHoverIn.bind(this));
|
||||
li.addEventListener("mouseout", this.#onRegionHoverOut.bind(this));
|
||||
}
|
||||
this.element.querySelector(`input[name="elevationBottom"]`)
|
||||
.addEventListener("change", this.#onElevationBottomChange.bind(this));
|
||||
this.element.querySelector(`input[name="elevationTop"]`)
|
||||
.addEventListener("change", this.#onElevationTopChange.bind(this));
|
||||
this.#updateVisibleRegions();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onClose(options) {
|
||||
super._onClose(options);
|
||||
this.#visibleRegions.clear();
|
||||
this.elevation.bottom = -Infinity;
|
||||
this.elevation.top = Infinity;
|
||||
delete canvas.scene.apps[this.id];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(_options) {
|
||||
const regions = canvas.scene.regions.map(r => this.#prepareRegion(r));
|
||||
regions.sort((a, b) => a.name.localeCompare(b.name, game.i18n.lang));
|
||||
return {
|
||||
regions,
|
||||
elevation: {
|
||||
bottom: Number.isFinite(this.elevation.bottom) ? this.elevation.bottom : "",
|
||||
top: Number.isFinite(this.elevation.top) ? this.elevation.top : "",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*-------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare each Region for rendering in the legend.
|
||||
* @param {Region} region
|
||||
* @returns {object}
|
||||
*/
|
||||
#prepareRegion(region) {
|
||||
const hasElv = (region.elevation.bottom !== null) || (region.elevation.top !== null);
|
||||
return {
|
||||
id: region.id,
|
||||
name: region.name,
|
||||
color: region.color.css,
|
||||
elevation: region.elevation,
|
||||
elevationLabel: hasElv ? `[${region.elevation.bottom ?? "∞"}, ${region.elevation.top ?? "∞"}]` : "",
|
||||
empty: !region.shapes.length,
|
||||
locked: region.locked,
|
||||
controlled: region.object?.controlled,
|
||||
hover: region.object?.hover,
|
||||
buttons: [
|
||||
{
|
||||
action: "config",
|
||||
icon: "fa-cogs",
|
||||
tooltip: game.i18n.localize("REGION.LEGEND.config"),
|
||||
disabled: ""
|
||||
},
|
||||
{
|
||||
action: "lock",
|
||||
icon: region.locked ? "fa-lock" : "fa-unlock",
|
||||
tooltip: game.i18n.localize(region.locked ? "REGION.LEGEND.unlock" : "REGION.LEGEND.lock"),
|
||||
disabled: ""
|
||||
},
|
||||
{
|
||||
action: "delete",
|
||||
icon: "fa-trash",
|
||||
tooltip: game.i18n.localize("REGION.LEGEND.delete"),
|
||||
disabled: region.locked ? "disabled" : ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the region list and hide regions that are not visible.
|
||||
*/
|
||||
#updateVisibleRegions() {
|
||||
this.#visibleRegions.clear();
|
||||
for ( const li of this.element.querySelectorAll(".region-list > .region") ) {
|
||||
const id = li.dataset.regionId;
|
||||
const region = canvas.scene.regions.get(id);
|
||||
const hidden = !((this.#searchFilter.rgx?.test(SearchFilter.cleanQuery(region.name)) !== false)
|
||||
&& (Math.max(region.object.bottom, this.elevation.bottom) <= Math.min(region.object.top, this.elevation.top)));
|
||||
if ( !hidden ) this.#visibleRegions.add(region);
|
||||
li.classList.toggle("hidden", hidden);
|
||||
}
|
||||
this.setPosition({height: "auto"});
|
||||
for ( const region of canvas.regions.placeables ) region.renderFlags.set({refreshState: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Filter regions.
|
||||
* @param {KeyboardEvent} event The key-up event from keyboard input
|
||||
* @param {string} query The raw string input to the search field
|
||||
* @param {RegExp} rgx The regular expression to test against
|
||||
* @param {HTMLElement} html The HTML element which should be filtered
|
||||
*/
|
||||
#onSearchFilter(event, query, rgx, html) {
|
||||
if ( !this.rendered ) return;
|
||||
this.#updateVisibleRegions();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle change events of the elevation range (bottom) input.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onElevationBottomChange(event) {
|
||||
this.elevation.bottom = Number(event.currentTarget.value || -Infinity);
|
||||
this.#updateVisibleRegions();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle change events of the elevation range (top) input.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onElevationTopChange(event) {
|
||||
this.elevation.top = Number(event.currentTarget.value || Infinity);
|
||||
this.#updateVisibleRegions();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this Region visible in this RegionLegend?
|
||||
* @param {Region} region The region
|
||||
* @returns {boolean}
|
||||
* @internal
|
||||
*/
|
||||
_isRegionVisible(region) {
|
||||
if ( !this.rendered ) return true;
|
||||
return this.#visibleRegions.has(region.document);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse-in events on a region in the legend.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
#onRegionHoverIn(event) {
|
||||
event.preventDefault();
|
||||
if ( !canvas.ready ) return;
|
||||
const li = event.currentTarget.closest(".region");
|
||||
const region = canvas.regions.get(li.dataset.regionId);
|
||||
region._onHoverIn(event, {hoverOutOthers: true, updateLegend: false});
|
||||
this.#hoveredRegion = region;
|
||||
li.classList.add("hovered");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse-out events for a region in the legend.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
#onRegionHoverOut(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".region");
|
||||
this.#hoveredRegion?._onHoverOut(event, {updateLegend: false});
|
||||
this.#hoveredRegion = null;
|
||||
li.classList.remove("hovered");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Highlight a hovered region in the legend.
|
||||
* @param {Region} region The Region
|
||||
* @param {boolean} hover Whether they are being hovered in or out.
|
||||
* @internal
|
||||
*/
|
||||
_hoverRegion(region, hover) {
|
||||
if ( !this.rendered ) return;
|
||||
const li = this.element.querySelector(`.region[data-region-id="${region.id}"]`);
|
||||
if ( !li ) return;
|
||||
if ( hover ) li.classList.add("hovered");
|
||||
else li.classList.remove("hovered");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicks to configure a Region.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onConfig(event) {
|
||||
const regionId = event.target.closest(".region").dataset.regionId;
|
||||
const region = canvas.scene.regions.get(regionId);
|
||||
region.sheet.render({force: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicks to assume control over a Region.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static #onControl(event) {
|
||||
const regionId = event.target.closest(".region").dataset.regionId;
|
||||
const region = canvas.scene.regions.get(regionId);
|
||||
|
||||
// Double-click = toggle sheet
|
||||
if ( event.detail === 2 ) {
|
||||
region.object.control({releaseOthers: true});
|
||||
region.sheet.render({force: true});
|
||||
}
|
||||
|
||||
// Single-click = toggle control
|
||||
else if ( event.detail === 1 ) {
|
||||
if ( region.object.controlled ) region.object.release();
|
||||
else region.object.control({releaseOthers: true});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle button clicks to create a new Region.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static async #onCreate(event) {
|
||||
await canvas.scene.createEmbeddedDocuments("Region", [{
|
||||
name: RegionDocument.implementation.defaultName({parent: canvas.scene})
|
||||
}]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicks to delete a Region.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static async #onDelete(event) {
|
||||
const regionId = event.target.closest(".region").dataset.regionId;
|
||||
const region = canvas.scene.regions.get(regionId);
|
||||
await region.deleteDialog();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicks to toggle the locked state of a Region.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static async #onLock(event) {
|
||||
const regionId = event.target.closest(".region").dataset.regionId;
|
||||
const region = canvas.scene.regions.get(regionId);
|
||||
await region.update({locked: !region.locked});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user