/** * 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} */ 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: '', 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: '', condition: game.user.isGM, callback: li => { let scene = game.scenes.get(li.data("sceneId")); scene.sheet.render(true); } }, { name: "SCENES.Notes", icon: '', 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: '', condition: game.user.isGM, callback: li => { let sceneId = li.attr("data-scene-id"); game.scenes.preload(sceneId, true); } }, { name: "SCENES.ToggleNav", icon: '', 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); } }