feat!: complete ApplicationV2 migration with item sheet port

Convert DS4ItemSheet and all item sheet templates from ApplicationV1 to
ItemSheetV2. Add V2 action handlers, tab navigation, and form
processing. Update effect management to use DialogV2. Preserve all
functionality including tab state and override handling.
This commit is contained in:
Alexander Minges 2025-07-12 21:18:18 +02:00
parent f0c5fa07dd
commit 4821eba0a9
Signed by: Athemis
GPG key ID: 31FBDEF92DDB162B
13 changed files with 284 additions and 147 deletions

View file

@ -67,12 +67,12 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 {
} }
/** @override */ /** @override */
async _renderHTML(context, options) { async _renderHTML(context) {
return await foundry.applications.handlebars.renderTemplate(this.template, context); return await foundry.applications.handlebars.renderTemplate(this.template, context);
} }
/** @override */ /** @override */
_replaceHTML(result, content, options) { _replaceHTML(result, content) {
content.innerHTML = result; content.innerHTML = result;
} }
@ -210,7 +210,7 @@ export class DS4ActorSheet extends foundry.applications.api.DocumentSheetV2 {
try { try {
const roll = new Roll(value); const roll = new Roll(value);
return roll.evaluateSync().total; return roll.evaluateSync().total;
} catch (error) { } catch {
return value; return value;
} }
} }

View file

@ -4,24 +4,42 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { DS4ActiveEffect } from "../documents/active-effect";
import { enforce, getGame } from "../utils/utils";
import { disableOverriddenFields } from "./sheet-helpers";
/** /**
* The Sheet class for DS4 Items * The Sheet class for DS4 Items
*/ */
export class DS4ItemSheet extends foundry.appv1.sheets.ItemSheet { export class DS4ItemSheet extends foundry.applications.sheets.ItemSheetV2 {
/** @override */ static DEFAULT_OPTIONS = {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-item-sheet"], classes: ["sheet", "ds4-item-sheet"],
height: 400, tag: "form",
scrollY: [".ds4-sheet-body"], form: {
tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "description" }], submitOnChange: true,
closeOnSubmit: false,
},
position: {
width: 540, width: 540,
}); height: 400,
},
window: {
resizable: true,
},
actions: {
controlEffect: DS4ItemSheet.prototype._onControlEffect,
createEffect: DS4ItemSheet.prototype._onCreateEffect,
editEffect: DS4ItemSheet.prototype._onEditEffect,
deleteEffect: DS4ItemSheet.prototype._onDeleteEffect,
changeTab: DS4ItemSheet.prototype._onChangeTab,
},
};
static TABS = {};
constructor(options = {}) {
super(options);
this.activeTab = "description"; // Default active tab
}
get title() {
return `${this.item.name} [${game.i18n.localize("DS4.ItemSheet")}]`;
} }
/** @override */ /** @override */
@ -31,123 +49,242 @@ export class DS4ItemSheet extends foundry.appv1.sheets.ItemSheet {
} }
/** @override */ /** @override */
async getData(options = {}) { async _renderHTML(context) {
const superContext = await super.getData(options); return await foundry.applications.handlebars.renderTemplate(this.template, context);
superContext.data.system.description = await foundry.applications.ux.TextEditor.implementation.enrichHTML( }
superContext.data.system.description,
/** @override */
_replaceHTML(result, content) {
content.innerHTML = result;
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
// Validate document exists
if (!this.item) {
throw new Error("Item not available for sheet rendering");
}
// Add document data
context.data = this.item;
context.system = this.item.system;
context.source = this.item.toObject();
context.cssClass = this.item.constructor.name.toLowerCase();
context.editable = this.isEditable;
context.owner = this.item.isOwner;
context.isOwned = this.item.isOwned;
context.actor = this.actor;
// Add configuration
context.config = CONFIG.DS4;
// Add enriched effects
context.enrichedEffects = [];
if (this.item.effects && this.item.effects.size > 0) {
for (const effect of this.item.effects) {
const enrichedEffect = effect.toObject();
enrichedEffect.sourceName = effect.sourceName;
context.enrichedEffects.push(enrichedEffect);
}
}
// Enrich description HTML content
if (context.data.system.description) {
context.data.system.description = await TextEditor.enrichHTML(
context.data.system.description,
{ {
async: true, async: true,
}, relativeTo: this.item,
}
); );
const context = { }
...superContext,
config: DS4,
isOwned: this.item.isOwned,
actor: this.item.actor,
};
return context; return context;
} }
/** @override */ /**
_getSubmitData(updateData = {}) { * Process form data for submission
const data = super._getSubmitData(updateData); * @param {Event} event - The form submission event
* @param {HTMLFormElement} form - The form element
* @param {FormDataExtended} formData - The form data
* @returns {object} The processed form data
*/
_processFormData(event, form, formData) {
const submitData = foundry.utils.expandObject(formData.object);
// Prevent submitting overridden values // Prevent submitting overridden values
const overrides = foundry.utils.flattenObject(this.item.overrides); const overrides = foundry.utils.flattenObject(this.item.overrides);
for (const k of Object.keys(overrides)) { for (const k of Object.keys(overrides)) {
delete data[k]; foundry.utils.setProperty(submitData, k, undefined);
delete submitData[k];
}
return submitData;
}
/**
* Handle effect control actions
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onControlEffect(event, target) {
const action = target.dataset.action;
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) return;
switch (action) {
case "edit":
await this._onEditEffect(event, target);
break;
case "delete":
await this._onDeleteEffect(event, target);
break;
}
}
/**
* Handle creating a new effect
*/
async _onCreateEffect() {
const effectData = {
name: game.i18n.localize("DS4.EffectNew"),
icon: "icons/svg/aura.svg",
};
await this.item.createEmbeddedDocuments("ActiveEffect", [effectData]);
}
/**
* Handle editing an effect
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onEditEffect(event, target) {
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) {
throw new Error(game.i18n.format("DS4.ErrorItemDoesNotHaveEffect", {
id: effectId,
item: this.item.name
}));
}
await effect.sheet.render(true);
}
/**
* Handle deleting an effect
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onDeleteEffect(event, target) {
const effectId = target.closest("[data-effect-id]")?.dataset.effectId;
if (!effectId) return;
const effect = this.item.effects.get(effectId);
if (!effect) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: game.i18n.localize("DS4.UserInteractionDeleteEffectTitle") },
content: game.i18n.format("DS4.UserInteractionDeleteEffectContent", { effect: effect.name }),
defaultYes: false,
});
if (confirmed) {
await effect.delete();
}
}
/**
* Handle tab changes manually for custom tab behavior
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
async _onChangeTab(event, target) {
event.preventDefault();
const tab = target.dataset.tab;
if (!tab) return;
// Store the active tab
this.activeTab = tab;
// Find tab navigation elements
const nav = target.closest(".ds4-sheet-tab-nav");
const sheet = this.element.querySelector(".ds4-sheet-body");
if (!nav || !sheet) return;
// Update navigation active state
nav.querySelectorAll(".ds4-sheet-tab-nav__item").forEach(item => {
item.classList.remove("active");
});
target.classList.add("active");
// Update tab content visibility
sheet.querySelectorAll(".ds4-sheet-tab").forEach(tabContent => {
tabContent.classList.remove("active");
});
const activeTab = sheet.querySelector(`.ds4-sheet-tab[data-tab="${tab}"]`);
if (activeTab) {
activeTab.classList.add("active");
} }
return data;
} }
/** @override */ /** @override */
setPosition(options = {}) { async _onRender(context, options) {
const position = super.setPosition(options); await super._onRender(context, options);
if (position) {
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
}
return position; // Initialize first tab as active
this._initializeTabs();
} }
/** /**
* @override * Initialize tab state - show first tab, hide others
* @param {JQuery} html
*/ */
activateListeners(html) { _initializeTabs() {
super.activateListeners(html); const nav = this.element.querySelector(".ds4-sheet-tab-nav");
const sheet = this.element.querySelector(".ds4-sheet-body");
if (!this.options.editable) return; if (!nav || !sheet) {
return;
html.find(".control-effect").on("click", this.onControlEffect.bind(this));
disableOverriddenFields(this.form, this.item.overrides, (key) => `[name="${key}"]`);
} }
/** // Get all tab navigation items and tab content
* Handles a click on an element of this sheet to control an embedded effect of the item corresponding to this const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item");
* sheet. const tabContents = sheet.querySelectorAll(".ds4-sheet-tab");
*
* @param {JQuery.ClickEvent} event The originating click event // Remove active class from all items first
* @protected navItems.forEach(item => item.classList.remove("active"));
*/ tabContents.forEach(content => content.classList.remove("active"));
onControlEffect(event) {
event.preventDefault(); // Find the currently active tab or default to first
const a = event.currentTarget; let targetTab = this.activeTab;
switch (a.dataset["action"]) { let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`);
case "create":
return this.onCreateEffect(); // If stored tab doesn't exist, fall back to first tab
case "edit": if (!targetNavItem) {
return this.onEditEffect(event); targetNavItem = navItems[0];
case "delete": targetTab = targetNavItem?.dataset.tab;
return this.onDeleteEffect(event);
}
} }
/** // Set target tab navigation as active
* Creates a new embedded effect. if (targetNavItem && targetTab) {
* @protected targetNavItem.classList.add("active");
*/
onCreateEffect() {
DS4ActiveEffect.createDefault(this.item);
}
/** // Set corresponding tab content as active
* Opens the sheet of the embedded effect corresponding to the clicked element. const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`);
* if (activeTabContent) {
* @param {JQuery.ClickEvent} event The originating click event activeTabContent.classList.add("active");
* @porotected }
*/ }
onEditEffect(event) {
const id = $(event.currentTarget)
.parents(embeddedDocumentListEntryProperties.ActiveEffect.selector)
.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
const effect = this.item.effects.get(id);
enforce(effect, getGame().i18n.format("DS4.ErrorItemDoesNotHaveEffect", { id, item: this.item.name }));
effect.sheet?.render(true);
}
/**
* Deletes the embedded item corresponding to the clicked element.
*
* @param {JQuery.ClickEvent} event The originating click event
* @protected
*/
onDeleteEffect(event) {
const li = $(event.currentTarget).parents(embeddedDocumentListEntryProperties.ActiveEffect.selector);
const id = li.data(embeddedDocumentListEntryProperties.ActiveEffect.idDataAttribute);
this.item.deleteEmbeddedDocuments("ActiveEffect", [id]);
li.slideUp(200, () => this.render(false));
} }
} }
/**
* This object contains information about specific properties embedded document list entries for each different type.
*/
const embeddedDocumentListEntryProperties = Object.freeze({
ActiveEffect: {
selector: ".effect",
idDataAttribute: "effectId",
},
});

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,8 +10,8 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}

View file

@ -10,9 +10,9 @@ SPDX-License-Identifier: MIT
{{!-- Sheet Tab Navigation --}} {{!-- Sheet Tab Navigation --}}
<nav class="ds4-sheet-tab-nav tabs" data-group="primary"> <nav class="ds4-sheet-tab-nav tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="properties">{{localize 'DS4.HeadingProperties'}}</a>
<a class="ds4-sheet-tab-nav__item" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a> <a class="ds4-sheet-tab-nav__item" data-action="changeTab" data-action="changeTab" data-tab="effects">{{localize 'DS4.HeadingEffects'}}</a>
</nav> </nav>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}