311 lines
9.7 KiB
JavaScript
311 lines
9.7 KiB
JavaScript
/**
|
|
* The UI element which displays the Scene documents which are currently enabled for quick navigation.
|
|
*/
|
|
class SceneNavigation extends Application {
|
|
constructor(options) {
|
|
super(options);
|
|
game.scenes.apps.push(this);
|
|
|
|
/**
|
|
* Navigation collapsed state
|
|
* @type {boolean}
|
|
*/
|
|
this._collapsed = false;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static get defaultOptions() {
|
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
id: "navigation",
|
|
template: "templates/hud/navigation.html",
|
|
popOut: false,
|
|
dragDrop: [{dragSelector: ".scene"}]
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Return an Array of Scenes which are displayed in the Navigation bar
|
|
* @returns {Scene[]}
|
|
*/
|
|
get scenes() {
|
|
const scenes = game.scenes.filter(s => {
|
|
return (s.navigation && s.visible) || s.active || s.isView;
|
|
});
|
|
scenes.sort((a, b) => a.navOrder - b.navOrder);
|
|
return scenes;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/* Application Rendering
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
render(force, context = {}) {
|
|
let {renderContext, renderData} = context;
|
|
if ( renderContext ) {
|
|
const events = ["createScene", "updateScene", "deleteScene"];
|
|
if ( !events.includes(renderContext) ) return this;
|
|
const updateKeys = ["name", "ownership", "active", "navigation", "navName", "navOrder"];
|
|
if ( (renderContext === "updateScene") && !renderData.some(d => updateKeys.some(k => k in d)) ) return this;
|
|
}
|
|
return super.render(force, context);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
async _render(force, options) {
|
|
await super._render(force, options);
|
|
const loading = document.getElementById("loading");
|
|
const nav = this.element[0];
|
|
loading.style.top = `${nav.offsetTop + nav.offsetHeight}px`;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
getData(options={}) {
|
|
const scenes = this.scenes.map(scene => {
|
|
return {
|
|
id: scene.id,
|
|
active: scene.active,
|
|
name: TextEditor.truncateText(scene.navName || scene.name, {maxLength: 32}),
|
|
tooltip: scene.navName && game.user.isGM ? scene.name : null,
|
|
users: game.users.reduce((arr, u) => {
|
|
if ( u.active && ( u.viewedScene === scene.id) ) arr.push({letter: u.name[0], color: u.color.css});
|
|
return arr;
|
|
}, []),
|
|
visible: game.user.isGM || scene.isOwner || scene.active,
|
|
css: [
|
|
scene.isView ? "view" : null,
|
|
scene.active ? "active" : null,
|
|
scene.ownership.default === 0 ? "gm" : null
|
|
].filterJoin(" ")
|
|
};
|
|
});
|
|
return {collapsed: this._collapsed, scenes: scenes};
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A hook event that fires when the SceneNavigation menu is expanded or collapsed.
|
|
* @function collapseSceneNavigation
|
|
* @memberof hookEvents
|
|
* @param {SceneNavigation} sceneNavigation The SceneNavigation application
|
|
* @param {boolean} collapsed Whether the SceneNavigation is now collapsed or not
|
|
*/
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Expand the SceneNavigation menu, sliding it down if it is currently collapsed
|
|
*/
|
|
expand() {
|
|
if ( !this._collapsed ) return true;
|
|
const nav = this.element;
|
|
const icon = nav.find("#nav-toggle i.fas");
|
|
const ul = nav.children("#scene-list");
|
|
return new Promise(resolve => {
|
|
ul.slideDown(200, () => {
|
|
nav.removeClass("collapsed");
|
|
icon.removeClass("fa-caret-down").addClass("fa-caret-up");
|
|
this._collapsed = false;
|
|
Hooks.callAll("collapseSceneNavigation", this, this._collapsed);
|
|
return resolve(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Collapse the SceneNavigation menu, sliding it up if it is currently expanded
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async collapse() {
|
|
if ( this._collapsed ) return true;
|
|
const nav = this.element;
|
|
const icon = nav.find("#nav-toggle i.fas");
|
|
const ul = nav.children("#scene-list");
|
|
return new Promise(resolve => {
|
|
ul.slideUp(200, () => {
|
|
nav.addClass("collapsed");
|
|
icon.removeClass("fa-caret-up").addClass("fa-caret-down");
|
|
this._collapsed = true;
|
|
Hooks.callAll("collapseSceneNavigation", this, this._collapsed);
|
|
return resolve(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Event Listeners and Handlers
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
activateListeners(html) {
|
|
super.activateListeners(html);
|
|
|
|
// Click event listener
|
|
const scenes = html.find(".scene");
|
|
scenes.click(this._onClickScene.bind(this));
|
|
html.find("#nav-toggle").click(this._onToggleNav.bind(this));
|
|
|
|
// Activate Context Menu
|
|
const contextOptions = this._getContextMenuOptions();
|
|
Hooks.call("getSceneNavigationContext", html, contextOptions);
|
|
if ( contextOptions ) new ContextMenu(html, ".scene", contextOptions);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Get the set of ContextMenu options which should be applied for Scenes in the menu
|
|
* @returns {object[]} The Array of context options passed to the ContextMenu instance
|
|
* @private
|
|
*/
|
|
_getContextMenuOptions() {
|
|
return [
|
|
{
|
|
name: "SCENES.Activate",
|
|
icon: '<i class="fas fa-bullseye"></i>',
|
|
condition: li => game.user.isGM && !game.scenes.get(li.data("sceneId")).active,
|
|
callback: li => {
|
|
let scene = game.scenes.get(li.data("sceneId"));
|
|
scene.activate();
|
|
}
|
|
},
|
|
{
|
|
name: "SCENES.Configure",
|
|
icon: '<i class="fas fa-cogs"></i>',
|
|
condition: game.user.isGM,
|
|
callback: li => {
|
|
let scene = game.scenes.get(li.data("sceneId"));
|
|
scene.sheet.render(true);
|
|
}
|
|
},
|
|
{
|
|
name: "SCENES.Notes",
|
|
icon: '<i class="fas fa-scroll"></i>',
|
|
condition: li => {
|
|
if ( !game.user.isGM ) return false;
|
|
const scene = game.scenes.get(li.data("sceneId"));
|
|
return !!scene.journal;
|
|
},
|
|
callback: li => {
|
|
const scene = game.scenes.get(li.data("sceneId"));
|
|
const entry = scene.journal;
|
|
if ( entry ) {
|
|
const sheet = entry.sheet;
|
|
const options = {};
|
|
if ( scene.journalEntryPage ) options.pageId = scene.journalEntryPage;
|
|
sheet.render(true, options);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: "SCENES.Preload",
|
|
icon: '<i class="fas fa-download"></i>',
|
|
condition: game.user.isGM,
|
|
callback: li => {
|
|
let sceneId = li.attr("data-scene-id");
|
|
game.scenes.preload(sceneId, true);
|
|
}
|
|
},
|
|
{
|
|
name: "SCENES.ToggleNav",
|
|
icon: '<i class="fas fa-compass"></i>',
|
|
condition: li => {
|
|
const scene = game.scenes.get(li.data("sceneId"));
|
|
return game.user.isGM && (!scene.active);
|
|
},
|
|
callback: li => {
|
|
const scene = game.scenes.get(li.data("sceneId"));
|
|
scene.update({navigation: !scene.navigation});
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle left-click events on the scenes in the navigation menu
|
|
* @param {PointerEvent} event
|
|
* @private
|
|
*/
|
|
_onClickScene(event) {
|
|
event.preventDefault();
|
|
let sceneId = event.currentTarget.dataset.sceneId;
|
|
game.scenes.get(sceneId).view();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
_onDragStart(event) {
|
|
const sceneId = event.currentTarget.dataset.sceneId;
|
|
const scene = game.scenes.get(sceneId);
|
|
event.dataTransfer.setData("text/plain", JSON.stringify(scene.toDragData()));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
async _onDrop(event) {
|
|
const data = TextEditor.getDragEventData(event);
|
|
if ( data.type !== "Scene" ) return;
|
|
|
|
// Identify the document, the drop target, and the set of siblings
|
|
const scene = await Scene.implementation.fromDropData(data);
|
|
const dropTarget = event.target.closest(".scene") || null;
|
|
const sibling = dropTarget ? game.scenes.get(dropTarget.dataset.sceneId) : null;
|
|
if ( sibling && (sibling.id === scene.id) ) return;
|
|
const siblings = this.scenes.filter(s => s.id !== scene.id);
|
|
|
|
// Update the navigation sorting for each Scene
|
|
return scene.sortRelative({
|
|
target: sibling,
|
|
siblings: siblings,
|
|
sortKey: "navOrder"
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle navigation menu toggle click events
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
_onToggleNav(event) {
|
|
event.preventDefault();
|
|
if ( this._collapsed ) return this.expand();
|
|
else return this.collapse();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Display progress of some major operation like loading Scene textures.
|
|
* @param {object} options Options for how the progress bar is displayed
|
|
* @param {string} options.label A text label to display
|
|
* @param {number} options.pct A percentage of progress between 0 and 100
|
|
*/
|
|
static displayProgressBar({label, pct} = {}) {
|
|
const loader = document.getElementById("loading");
|
|
pct = Math.clamp(pct, 0, 100);
|
|
loader.querySelector("#context").textContent = label;
|
|
loader.querySelector("#loading-bar").style.width = `${pct}%`;
|
|
loader.querySelector("#progress").textContent = `${pct}%`;
|
|
loader.style.display = "block";
|
|
if ( (pct === 100) && !loader.hidden ) $(loader).fadeOut(2000);
|
|
}
|
|
}
|