diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dabeed..65a089d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### **Bug Fixes:** - [#392](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/392) - Fixed sheet rolls +- [#394](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/394) - Unified actor and item description fields on schema with shared ProseMirror preparation ## Version 1.6.6 - 2026-04-15 diff --git a/css/deltagreen.css b/css/deltagreen.css index 9fa2cd3..3d6cf24 100644 --- a/css/deltagreen.css +++ b/css/deltagreen.css @@ -894,8 +894,10 @@ div.horizontal-bar-section-header { } .notes-wrapper { - height: 100%; margin-block-start: 12px; + display: flex; + flex-direction: column; + min-height: 0; } div.motivation-editor-container div.editor { @@ -905,15 +907,135 @@ div.motivation-editor-container div.editor { .deltagreen .editor-host { align-items: stretch; + display: flex; + flex-direction: column; + min-height: 0; +} + +/* ProseMirror: compact when toggled off; expand to fill tab space while editing. */ +.deltagreen .editor-host prose-mirror { + --min-height: 5rem; + flex: 0 0 auto; +} + +.deltagreen:not(.item) .editor-host prose-mirror:not(.active) { + height: auto !important; +} + +.deltagreen .editor-host prose-mirror.active { + --min-height: 12rem; + flex: 1 1 auto; + min-height: 12rem; +} + +.deltagreen .editor-host prose-mirror.active .editor-container { + flex: 1 1 auto; + min-height: 0; +} + +.deltagreen .editor-host prose-mirror .editor-content { + font-family: "SpecialElite", sans-serif !important; +} + +/* Actor sheets: compact description when not editing. */ +.deltagreen:not(.item) .editor-host prose-mirror:not(.active) .editor-content { + position: relative; + inset: unset; + overflow: visible; + min-height: 2.5rem; +} + +/* Let the description block consume remaining tab height while editing. */ +.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) { + display: flex; + flex-direction: column; + min-height: 0; +} + +.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) .notes-wrapper, +.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) .notes-wrapper .editor-host { + flex: 1 1 auto; + min-height: 0; +} + +/* Item sheets: description fills space below header (view and edit). */ +.deltagreen.item form { + display: flex; + flex-direction: column; height: 100%; } -.deltagreen .editor-host .editor { - height: 100% !important; +.deltagreen.item .window-content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; } -.deltagreen .editor .editor-container { - height: 100% !important; +.deltagreen.item [data-application-part="header"], +.deltagreen.item [data-application-part="tabs"] { + flex: 0 0 auto; +} + +/* Bond, gear, weapon, armor, motivation (single description part). */ +.deltagreen.item section.sheet-body[data-application-part="description"] { + flex: 1 1 auto; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +/* Tome/ritual tab parts — flex only when .active; do not override core display:none on inactive tabs. */ +.deltagreen.item .tab[data-tab][data-application-part].active { + flex: 1 1 auto; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.deltagreen.item section.sheet-body > .tab.top-div-border, +.deltagreen.item .tab[data-application-part="handler"].active > .tab.top-div-border { + flex: 1 1 auto; + display: flex; + flex-direction: column; + min-height: 0; +} + +.deltagreen.item .tab.top-div-border > .editor-host, +.deltagreen.item .tab[data-application-part="description"].active > .editor-host { + flex: 1 1 auto; + min-height: 0; +} + +.deltagreen.item .tab.top-div-border .editor-host prose-mirror { + flex: 1 1 auto; + min-height: 5rem; +} + +.deltagreen.item .tab.top-div-border .editor-host prose-mirror:not(.active) .editor-content { + position: absolute; + inset: 0; + overflow: auto; + min-height: unset; +} + +.deltagreen.item .tab.top-div-border .editor-host prose-mirror.active { + min-height: 5rem; +} + +/* Core uses display:block on hover for prose-mirror toggles, which breaks flex icon centering. */ +.deltagreen prose-mirror:hover button.toggle:enabled { + display: flex; + justify-content: center; + align-items: center; + padding: 0; +} + +.deltagreen prose-mirror button.toggle > i { + margin: 0; + line-height: 1; } /* I think all the stuff below is now obsolete @@ -1007,6 +1129,7 @@ input.item-sheet-short-input { justify-content: center; text-align: left; padding-left: 5px; + padding-bottom: 10px; grid-gap: 5px; } @@ -1096,48 +1219,72 @@ input.item-sheet-short-input { padding-right: 5px; } -.weapon-sheet-outer-grid-2col { - display: grid; - grid-template-columns: 60% 40%; +.weapon-sheet-horizontal-grid-2col .resource-label { + display: flex; align-items: center; - justify-content: center; - text-align: center; - padding-left: 5px; - padding-right: 5px; - column-gap: 10px; - row-gap: 5px; + min-height: 1.5rem; + margin: 0; } -.weapon-sheet-horizontal-grid-left-2col { +.weapon-sheet-horizontal-grid-2col input[type="text"], +.weapon-sheet-horizontal-grid-2col select { + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 0; + line-height: 1.5rem; +} + +.weapon-sheet-header { + margin-bottom: 12px; +} + +/* One grid row per stat pair so left/right labels share the same baseline. */ +.weapon-sheet-stats-grid { display: grid; - grid-template-columns: 50% 50%; + grid-template-columns: minmax(5.5rem, auto) minmax(9rem, 1.35fr) minmax(5.5rem, auto) minmax(9rem, 1.35fr); align-items: center; - text-align: left; - padding-bottom: 10px; - column-gap: 10px; + column-gap: 12px; row-gap: 10px; - padding-right: 10px; + padding: 0 2px 10px; + text-align: left; } -.weapon-sheet-horizontal-grid-right-2col { - display: grid; - grid-template-columns: 60% 40%; +.weapon-sheet-stats-grid .resource-label { + display: flex; align-items: center; - text-align: left; - padding-bottom: 10px; - column-gap: 10px; - row-gap: 10px; - padding-right: 10px; + min-height: var(--weapon-sheet-control-height, 1.75rem); + margin: 0; } -.weapon-sheet-secondary-grid-2col { - display: grid; - grid-template-columns: 40% 60%; - align-items: center; - justify-content: left; - text-align: left; - grid-gap: 5px; - padding-right: 5px; +.weapon-sheet-stats-grid input[type="text"], +.weapon-sheet-stats-grid select { + width: 100%; + min-width: 0; + max-width: 100%; + box-sizing: border-box; + margin: 0; + height: var(--weapon-sheet-control-height, 1.75rem); + min-height: var(--weapon-sheet-control-height, 1.75rem); + line-height: 1.5rem; + padding: 0 6px; +} + +.weapon-sheet-stats-grid select.weapon-sheet-skill-select { + min-width: 9rem; +} + +.weapon-sheet-stats-grid .labeled-resource-checkbox { + justify-self: start; + align-self: center; + margin: 0; +} + +.weapon-sheet-stats-pad { + display: block; + min-height: 0; + visibility: hidden; + pointer-events: none; } /* diff --git a/css/editor.css b/css/editor.css index b4a32ee..54c4ef6 100644 --- a/css/editor.css +++ b/css/editor.css @@ -20,7 +20,8 @@ font-family: "SpecialElite", sans-serif !important; } -.editor-host .editor { +.editor-host .editor .editor-content, +.editor-host prose-mirror .editor-content { font-family: "SpecialElite", sans-serif !important; } diff --git a/lang/ca.json b/lang/ca.json index 611d826..d4a0418 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -199,7 +199,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Una arma letal té l'oportunitat de matar instantàniament un objectiu. Tira 1d100 enlloc del dany. Si no és letal, el dany és dalcula tirant 2d10, on el primer d10 és iugal a 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Marca si l'arma té un % de letalitat associat.", "DG.ItemWindow.Weapons.ArmorPiercing": "Perforació d'armadures", - "DG.ItemWindow.Weapons.Ammo": "Munició (Quantitat/Tipus)", + "DG.ItemWindow.Weapons.Ammo": "Munició", "DG.ItemWindow.Weapons.Range": "Abast", "DG.ItemWindow.Weapons.KillRadius": "Radi de mort", diff --git a/lang/cn.json b/lang/cn.json index fc8f18c..b179958 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -215,7 +215,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "致死武器有机会立即杀死目标。掷D100而不是伤害骰。如果不致死,则造成伤害,就像结果是2d10掷骰一样,第一个d10上的'00'等于10", "DG.ItemWindow.Weapons.IsLethalityTooltip": "如果武器有致死等级 % 与之相关,请勾选此项", "DG.ItemWindow.Weapons.ArmorPiercing": "穿甲", - "DG.ItemWindow.Weapons.Ammo": "弹药 (数量/类别)", + "DG.ItemWindow.Weapons.Ammo": "弹药", "DG.ItemWindow.Weapons.Range": "射程", "DG.ItemWindow.Weapons.KillRadius": "击杀半径", diff --git a/lang/de.json b/lang/de.json index b94fef6..23a5d3e 100644 --- a/lang/de.json +++ b/lang/de.json @@ -216,7 +216,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Eine tödliche Waffe hat eine Chance, ein Ziel sofort zu töten. Wirf einen D100 statt eines Schadenswürfels. Wenn sie nicht tödlich ist, wird der Schaden stattdessen wie bei einer 2d10-Probe berechnet, wobei eine '00' auf dem ersten d10 einer 10 entspricht.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Prüfe dies, wenn die Waffe eine Tödlichkeit in % hat.", "DG.ItemWindow.Weapons.ArmorPiercing": "Rüstungsdurchdringung", - "DG.ItemWindow.Weapons.Ammo": "Munition (Menge/Typ)", + "DG.ItemWindow.Weapons.Ammo": "Munition", "DG.ItemWindow.Weapons.Range": "Reichweite", "DG.ItemWindow.Weapons.KillRadius": "Tötungsradius", diff --git a/lang/en.json b/lang/en.json index e4c0805..dedb711 100644 --- a/lang/en.json +++ b/lang/en.json @@ -282,7 +282,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "A lethal weapon has a chance to instantly kill a target. Roll a D100 instead of damage die. If it is not lethal, damage is dealt instead as if the result was a 2d10 roll, with a '00' on the first d10 equaling a 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Check this if the weapon has a lethality % associated with it.", "DG.ItemWindow.Weapons.ArmorPiercing": "Armor Piercing", - "DG.ItemWindow.Weapons.Ammo": "Ammo (Amount/Type)", + "DG.ItemWindow.Weapons.Ammo": "Ammo", "DG.ItemWindow.Weapons.Range": "Range", "DG.ItemWindow.Weapons.KillRadius": "Kill Radius", diff --git a/lang/es.json b/lang/es.json index 0db399b..c35a0ac 100644 --- a/lang/es.json +++ b/lang/es.json @@ -280,7 +280,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Un arma letal tiene la oportunidad de matar instantáneamente a un objetivo. Tira 1D100 en lugar de un dado de daño. Si no es letal, el daño se calcula en su lugar tirando 2d10, donde un '00' en el primer d10 es igual a 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Marca si el arma tiene un % de letalidad asociada.", "DG.ItemWindow.Weapons.ArmorPiercing": "Perforación", - "DG.ItemWindow.Weapons.Ammo": "Munición (cantidad/tipo)", + "DG.ItemWindow.Weapons.Ammo": "Munición", "DG.ItemWindow.Weapons.Range": "Alcance", "DG.ItemWindow.Weapons.KillRadius": "Radio muerte", diff --git a/lang/fr.json b/lang/fr.json index e8a53e4..9e98377 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -280,7 +280,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Une arme létale a une chance de tuer instantanément une cible. Lancer un D100 au lieu d'un jet de dégâts. En cas d'échec, la cible subit des dommages égaux aux 2d10 associés", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Cochez cette case si l'arme est associée à un pourcentage de Létalité", "DG.ItemWindow.Weapons.ArmorPiercing": "Perforant", - "DG.ItemWindow.Weapons.Ammo": "Munitions (Nombre/Type)", + "DG.ItemWindow.Weapons.Ammo": "Munitions", "DG.ItemWindow.Weapons.Range": "Portée", "DG.ItemWindow.Weapons.KillRadius": "Rayon de mortalité", diff --git a/lang/ja.json b/lang/ja.json index c2e7902..c6293fd 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -215,7 +215,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "致命武器は、対象を即死させる可能性がある。ダメージダイスではなく、D100をロールする。致命でない場合、ダメージは2d10をロールした結果となる。最初のd10の「00」は10として扱う。", "DG.ItemWindow.Weapons.IsLethalityTooltip": "致命率のある武器なら、ここにチェックする", "DG.ItemWindow.Weapons.ArmorPiercing": "装甲貫通", - "DG.ItemWindow.Weapons.Ammo": "弾 (数量/種類)", + "DG.ItemWindow.Weapons.Ammo": "弾", "DG.ItemWindow.Weapons.Range": "射程", "DG.ItemWindow.Weapons.KillRadius": "致命半径", diff --git a/lang/pl.json b/lang/pl.json index 053bfaa..b28a2ad 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -278,7 +278,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Broń śmiertelna ma szansę natychmiastowo zabić cel. Rzuć D100 zamiast kości obrażeń. Jeśli nie jest śmiertelna, obrażenia są traktowane jakby wynik był rzutem 2d10, z '00' na pierwszym d10 traktowanym jak 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Zaznacz to, jeśli broń ma procentową wartość śmiertelności.", "DG.ItemWindow.Weapons.ArmorPiercing": "Przebicie Pancerza", - "DG.ItemWindow.Weapons.Ammo": "Amunicja (Ilość/Typ)", + "DG.ItemWindow.Weapons.Ammo": "Amunicja", "DG.ItemWindow.Weapons.Range": "Zasięg", "DG.ItemWindow.Weapons.KillRadius": "Zasięg Śmierci", diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 2843a2c..53fea46 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -280,7 +280,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Uma arma letal tem a chance de matar instantaneamente um alvo. Role um D100 em vez de um dado de dano. Se não for letal, o dano é causado como se o resultado fosse uma rolagem de 2d10, com um '00' no primeiro d10 igual a 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Marque isso se a arma tiver uma % de letalidade associada a ela.", "DG.ItemWindow.Weapons.ArmorPiercing": "Perfuração de Armadura", - "DG.ItemWindow.Weapons.Ammo": "Munição (quantidade/tipo)", + "DG.ItemWindow.Weapons.Ammo": "Munição", "DG.ItemWindow.Weapons.Range": "Alcance", "DG.ItemWindow.Weapons.KillRadius": "Raio de Morte", diff --git a/lang/sv.json b/lang/sv.json index a57d77d..668c7e3 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -215,7 +215,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Ett dödligt vapen har en chans att omedelbart döda ett mål. Slå en D100 istället för skadetärningen. Om det inte är dödligt tolkas resultatet som om det var 2 enskilda d10or skada, där en '00' på den första d10an motsvarar 10 skada.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Kryssa i detta om vapnet är dödligt.", "DG.ItemWindow.Weapons.ArmorPiercing": "Genomträngande", - "DG.ItemWindow.Weapons.Ammo": "Ammo (Mängd/Typ)", + "DG.ItemWindow.Weapons.Ammo": "Ammo", "DG.ItemWindow.Weapons.Range": "Räckvidd", "DG.ItemWindow.Weapons.KillRadius": "Dödsradie", diff --git a/lang/uk.json b/lang/uk.json index 4ce6bf9..aa4196b 100644 --- a/lang/uk.json +++ b/lang/uk.json @@ -280,7 +280,7 @@ "DG.ItemWindow.Weapons.LethalityTooltip": "Летальна зброя має шанс миттєво вбити ціль. Замість кубів шкоди кидай D100. Якщо результат НЕ летальний, тоді шкода рахується так, ніби випало 2d10, де «00» на першому d10 дорівнює 10.", "DG.ItemWindow.Weapons.IsLethalityTooltip": "Познач, якщо зброя має відсоток летальності.", "DG.ItemWindow.Weapons.ArmorPiercing": "Бронепробиття", - "DG.ItemWindow.Weapons.Ammo": "Набої (кількість/тип)", + "DG.ItemWindow.Weapons.Ammo": "Набої", "DG.ItemWindow.Weapons.Range": "Дальність", "DG.ItemWindow.Weapons.KillRadius": "Радіус ураження", diff --git a/module/config/rich-text-fields.js b/module/config/rich-text-fields.js new file mode 100644 index 0000000..91d5d36 --- /dev/null +++ b/module/config/rich-text-fields.js @@ -0,0 +1,33 @@ +/** + * Rich-text field specs for actor sheets (by actor type). + * @type {Record} + */ +export const ACTOR_RICH_TEXT_BY_TYPE = { + agent: [{ path: "physical.description", key: "description" }], + npc: [{ path: "notes", key: "description" }], + unnatural: [{ path: "notes", key: "description" }], + vehicle: [{ path: "description", key: "description" }], +}; + +/** + * Rich-text field specs for all item types. + * @type {{ path: string, key: string, types?: string[] }[]} + */ +export const ITEM_RICH_TEXT_FIELDS = [ + { path: "description", key: "description" }, + { + path: "handlerNotes", + key: "handlerNotes", + types: ["tome", "ritual"], + }, +]; + +/** + * @param {string} itemType + * @returns {{ path: string, key: string }[]} + */ +export function getItemRichTextFields(itemType) { + return ITEM_RICH_TEXT_FIELDS.filter( + (spec) => !spec.types || spec.types.includes(itemType), + ); +} diff --git a/module/data/actor/agent.js b/module/data/actor/agent.js index a3b1fc8..227f6f4 100644 --- a/module/data/actor/agent.js +++ b/module/data/actor/agent.js @@ -1,5 +1,6 @@ import HumanSkillsActorData from "./base/human-skills.js"; import CharacterData from "./base/character.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { SchemaField, NumberField, StringField, BooleanField, HTMLField } = foundry.data.fields; @@ -29,11 +30,7 @@ export default class AgentData extends CharacterData { }), }), physical: new SchemaField({ - description: new HTMLField({ - initial: "", - blank: true, - textSearch: true, - }), + description: DGHTMLField(), wounds: new StringField({ initial: "" }), firstAidAttempted: new BooleanField({ initial: false }), exhausted: new BooleanField({ initial: false }), diff --git a/module/data/actor/npc.js b/module/data/actor/npc.js index 412cfc1..403af85 100644 --- a/module/data/actor/npc.js +++ b/module/data/actor/npc.js @@ -1,5 +1,6 @@ import HumanSkillsActorData from "./base/human-skills.js"; import CharacterData from "./base/character.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { SchemaField, NumberField, StringField, BooleanField, HTMLField } = foundry.data.fields; @@ -19,7 +20,7 @@ export default class NPCData extends CharacterData { biography: new SchemaField({ profession: new StringField({ initial: "" }), }), - notes: new HTMLField({ initial: "", blank: true, textSearch: true }), + notes: DGHTMLField(), shortDescription: new StringField({ initial: "" }), showUntrainedSkills: new BooleanField({ initial: true }), }; diff --git a/module/data/actor/unnatural.js b/module/data/actor/unnatural.js index 7e293b6..001b28e 100644 --- a/module/data/actor/unnatural.js +++ b/module/data/actor/unnatural.js @@ -1,5 +1,6 @@ import UnnaturalSkillsActorData from "./base/unnatural-skills.js"; import CharacterData from "./base/character.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { SchemaField, NumberField, StringField, BooleanField, HTMLField } = foundry.data.fields; @@ -17,12 +18,7 @@ export default class UnnaturalData extends CharacterData { successLoss: new StringField({ initial: "1" }), }), schemaVersion: new NumberField({ initial: 1.0 }), - notes: new HTMLField({ - initial: "", - blank: true, - textSearch: true, - allowColor: true, - }), + notes: DGHTMLField(), shortDescription: new StringField({ initial: "" }), showUntrainedSkills: new BooleanField({ initial: true }), }; diff --git a/module/data/actor/vehicle.js b/module/data/actor/vehicle.js index e6e75e3..78b083e 100644 --- a/module/data/actor/vehicle.js +++ b/module/data/actor/vehicle.js @@ -1,4 +1,5 @@ import SheetSettingsActorData from "./base/sheet-settings.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { SchemaField, NumberField, StringField, ArrayField, HTMLField } = foundry.data.fields; @@ -8,11 +9,7 @@ export default class VehicleData extends foundry.abstract.TypeDataModel { return { ...SheetSettingsActorData.defineSchema(), name: new StringField({ initial: "" }), - description: new HTMLField({ - initial: "", - blank: true, - textSearch: true, - }), + description: DGHTMLField(), shortDescription: new StringField({ initial: "" }), health: new SchemaField({ value: new NumberField({ initial: 10 }), diff --git a/module/data/fields/html-content-field.js b/module/data/fields/html-content-field.js new file mode 100644 index 0000000..491c005 --- /dev/null +++ b/module/data/fields/html-content-field.js @@ -0,0 +1,15 @@ +const { HTMLField } = foundry.data.fields; + +/** + * Standard Delta Green HTML / ProseMirror field defaults. + * @param {Partial} [options] + * @returns {foundry.data.fields.HTMLField} + */ +export default function DGHTMLField(options = {}) { + return new HTMLField({ + required: false, + blank: true, + initial: "", + ...options, + }); +} diff --git a/module/data/item/base-fields.js b/module/data/item/base-fields.js index 896b518..c1423e9 100644 --- a/module/data/item/base-fields.js +++ b/module/data/item/base-fields.js @@ -1,4 +1,4 @@ -const { HTMLField } = foundry.data.fields; +import DGHTMLField from "../fields/html-content-field.js"; /** * Shared system fields for all Delta Green item types (from legacy template base). @@ -6,6 +6,6 @@ const { HTMLField } = foundry.data.fields; */ export default function defineBaseItemSystemFields() { return { - description: new HTMLField({ initial: "", blank: true, textSearch: true }), + description: DGHTMLField(), }; } diff --git a/module/data/item/ritual.js b/module/data/item/ritual.js index 7386cf8..71d58e1 100644 --- a/module/data/item/ritual.js +++ b/module/data/item/ritual.js @@ -1,4 +1,5 @@ import defineBaseItemSystemFields from "./base-fields.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { NumberField, StringField, BooleanField, SchemaField, HTMLField } = foundry.data.fields; @@ -22,11 +23,7 @@ export default class RitualItemData extends foundry.abstract.TypeDataModel { activationCosts: new StringField({ initial: "" }), activationTime: new StringField({ initial: "" }), complexity: new StringField({ initial: "Simple" }), - handlerNotes: new HTMLField({ - initial: "", - blank: true, - textSearch: true, - }), + handlerNotes: DGHTMLField(), revealed: new BooleanField({ initial: false }), }; } diff --git a/module/data/item/tome.js b/module/data/item/tome.js index 2673153..d2eab1f 100644 --- a/module/data/item/tome.js +++ b/module/data/item/tome.js @@ -1,4 +1,5 @@ import defineBaseItemSystemFields from "./base-fields.js"; +import DGHTMLField from "../fields/html-content-field.js"; const { NumberField, StringField, BooleanField, SchemaField, HTMLField } = foundry.data.fields; @@ -16,11 +17,7 @@ export default class TomeItemData extends foundry.abstract.TypeDataModel { failedLoss: new StringField({ initial: "1D6" }), successLoss: new StringField({ initial: "1D4" }), }), - handlerNotes: new HTMLField({ - initial: "", - blank: true, - textSearch: true, - }), + handlerNotes: DGHTMLField(), revealed: new BooleanField({ initial: false }), }; } diff --git a/module/sheets/base-actor-sheet.js b/module/sheets/base-actor-sheet.js index 457cb6d..5d988c4 100644 --- a/module/sheets/base-actor-sheet.js +++ b/module/sheets/base-actor-sheet.js @@ -1,4 +1,5 @@ import DG from "../config.js"; +import { ACTOR_RICH_TEXT_BY_TYPE } from "../config/rich-text-fields.js"; import { DGPercentileRoll, createDGRollFromDataset, @@ -98,8 +99,10 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { // Prepare subname info placeholder. context.subnameInfoPlaceholder = this._prepareSubnameInfoPlaceholder(); - // Prepare descriptions for each sheet. - context.enrichedDescription = await this._prepareDescriptions(); + await this._prepareRichTextContext( + context, + ACTOR_RICH_TEXT_BY_TYPE[this.actor.type] ?? [], + ); // Early return if this is a vehicle. if (this.actor.type === "vehicle") return context; @@ -258,12 +261,6 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { } } - /** @override */ - activateEditor(target, editorOptions, initialContent) { - editorOptions.content_css = "./systems/deltagreen/css/editor.css"; - return super.activateEditor(target, editorOptions, initialContent); - } - /* --------- Context Preparation Functions --------- */ /** @@ -291,51 +288,6 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) { return game.i18n.localize(subnameInfoPlaceholder); } - /** - * Prepares and enriches the description for an actor based on its type. - * - * @returns {Promise} The outer HTML of the enriched description. - */ - async _prepareDescriptions() { - let descriptionPath; - - switch (this.actor.type) { - case "agent": - descriptionPath = "system.physical.description"; - break; - case "npc": - case "unnatural": - descriptionPath = "system.notes"; - break; - case "vehicle": - descriptionPath = "system.description"; - break; - default: - break; - } - - const descriptionValue = foundry.utils.getProperty( - this.actor, - descriptionPath, - ); - - const enrichedDescription = - await foundry.applications.ux.TextEditor.implementation.enrichHTML( - descriptionValue, - { - rollData: this.document.getRollData(), - relativeTo: this.document, - }, - ); - const { HTMLProseMirrorElement } = foundry.applications.elements; - return HTMLProseMirrorElement.create({ - name: descriptionPath, - value: descriptionValue, - enriched: enrichedDescription, - toggled: true, - }).outerHTML; - } - /** * Sorts the skills on the actor sheet based on the appropriate localized entry. * If the localized entry is not found, the sort label is the skill key. diff --git a/module/sheets/base-item-sheet.js b/module/sheets/base-item-sheet.js index 25f6c6c..a9db633 100644 --- a/module/sheets/base-item-sheet.js +++ b/module/sheets/base-item-sheet.js @@ -1,10 +1,14 @@ import { BASE_TEMPLATE_PATH } from "../config.js"; +import { getItemRichTextFields } from "../config/rich-text-fields.js"; import DGSheetMixin from "./base-sheet.js"; import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js"; const { ItemSheetV2 } = foundry.applications.sheets; const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`; +/** Tab group id used by item sheet templates (`data-group="primary"`). */ +const ITEM_TAB_GROUP = "primary"; + /** Tab and part layout per item type. */ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({ weapon: { @@ -44,22 +48,8 @@ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({ }, }, motivation: { - tabs: { - initial: "attributes", - tabs: [ - { - id: "attributes", - label: "DG.ItemWindow.Motivations.Attributes", - }, - { - id: "description", - label: "DG.ItemWindow.Motivations.Description", - }, - ], - }, parts: { - tabs: { template: "templates/generic/tab-navigation.hbs" }, - attributes: { template: `${ITEM_PARTS_PATH}/motivation-attributes.html` }, + header: { template: `${ITEM_PARTS_PATH}/motivation-header.html` }, description: { template: `${ITEM_PARTS_PATH}/motivation-description.html`, scrollable: [""], @@ -161,31 +151,43 @@ export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) { /** @inheritdoc */ _getTabsConfig(group) { const layout = ITEM_SHEET_LAYOUT[this.item.type]; - if (!layout?.tabs) return super._getTabsConfig(group); + if (!layout?.tabs || group !== ITEM_TAB_GROUP) { + return super._getTabsConfig(group); + } const tabs = layout.tabs.tabs.filter((tab) => !tab.gmOnly || game.user.isGM); return { ...layout.tabs, tabs }; } + /** @inheritdoc */ + async _preparePartContext(partId, context, options) { + const partContext = await super._preparePartContext(partId, context, options); + if (partId === "tabs") partContext.tabClasses = "sheet-tabs"; + return partContext; + } + /** @inheritdoc */ async _prepareContext(options) { const context = await super._prepareContext(options); - context.owner = this.document.isOwner; - context.enrichedDescription = - await foundry.applications.ux.TextEditor.implementation.enrichHTML( - this.item.system.description, - { async: true }, - ); - - if (this.item.type === "tome" || this.item.type === "ritual") { - context.enrichedHandlerNotes = - await foundry.applications.ux.TextEditor.implementation.enrichHTML( - this.item.system.handlerNotes, - { async: true }, - ); + const layout = ITEM_SHEET_LAYOUT[this.item.type]; + if (layout?.tabs) { + const { initial } = layout.tabs; + const tabIds = layout.tabs.tabs + .filter((tab) => !tab.gmOnly || game.user.isGM) + .map((tab) => tab.id); + if (!tabIds.includes(this.tabGroups[ITEM_TAB_GROUP])) { + this.tabGroups[ITEM_TAB_GROUP] = initial; + } + context.tabs = this._prepareTabs(ITEM_TAB_GROUP); } + context.owner = this.document.isOwner; + await this._prepareRichTextContext( + context, + getItemRichTextFields(this.item.type), + ); + return context; } diff --git a/module/sheets/base-sheet.js b/module/sheets/base-sheet.js index f4de812..172804a 100644 --- a/module/sheets/base-sheet.js +++ b/module/sheets/base-sheet.js @@ -1,4 +1,5 @@ import DG, { BASE_TEMPLATE_PATH } from "../config.js"; +import { prepareRichTextContext } from "./rich-text.js"; const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin; @@ -63,6 +64,23 @@ const DGSheetMixin = (Base) => { resetPosition() { this.setPosition(this.options.position); } + + /** + * Prepare `context.richText` from schema-driven ProseMirror field specs. + * @param {ApplicationRenderContext} context + * @param {{ path: string, key: string }[]} fieldSpecs + * @returns {Promise} + */ + async _prepareRichTextContext(context, fieldSpecs) { + if (!fieldSpecs?.length) { + context.richText = {}; + return; + } + context.richText = await prepareRichTextContext( + this.document, + fieldSpecs, + ); + } }; }; diff --git a/module/sheets/rich-text.js b/module/sheets/rich-text.js new file mode 100644 index 0000000..e90234c --- /dev/null +++ b/module/sheets/rich-text.js @@ -0,0 +1,50 @@ +const { HTMLField } = foundry.data.fields; + +/** + * Build toggled ProseMirror markup for a system schema HTML field (App V2). + * @param {Actor|Item} document + * @param {string} fieldPath Path within `document.system` (e.g. "physical.description") + * @param {object} [options] Additional options passed to DataField#toInput + * @returns {Promise} + */ +export async function prepareProseMirrorInput(document, fieldPath, options = {}) { + const field = document.system.schema.getField(fieldPath); + if (!(field instanceof HTMLField)) { + throw new Error( + `Rich text field "system.${fieldPath}" must be an HTMLField on ${document.documentName} ${document.name}`, + ); + } + + const value = foundry.utils.getProperty(document.system, fieldPath) ?? ""; + const enriched = + await foundry.applications.ux.TextEditor.implementation.enrichHTML(value, { + async: true, + rollData: document.getRollData?.() ?? {}, + relativeTo: document, + }); + + const input = field.toInput({ + name: `system.${fieldPath}`, + value, + enriched, + toggled: true, + documentUUID: document.uuid, + ...options, + }); + + return input.outerHTML; +} + +/** + * Populate `context.richText` from a list of field specs. + * @param {Actor|Item} document + * @param {{ path: string, key: string }[]} fieldSpecs + * @returns {Promise>} + */ +export async function prepareRichTextContext(document, fieldSpecs) { + const richText = {}; + for (const { path, key } of fieldSpecs) { + richText[key] = await prepareProseMirrorInput(document, path); + } + return richText; +} diff --git a/module/sheets/vehicle-sheet.js b/module/sheets/vehicle-sheet.js index 89e1b4b..d1d0d62 100644 --- a/module/sheets/vehicle-sheet.js +++ b/module/sheets/vehicle-sheet.js @@ -8,8 +8,8 @@ export default class DGVehicleSheet extends DGActorSheet { initial: "gear", labelPrefix: "DG.Navigation.Vehicle", tabs: [ - { id: "notes" }, { id: "gear" }, + { id: "notes" }, { id: "about", icon: "fas fa-question-circle", label: "" }, ], }, diff --git a/module/templates.js b/module/templates.js index dbc6613..a04a632 100644 --- a/module/templates.js +++ b/module/templates.js @@ -6,5 +6,6 @@ export default async function preloadHandlebarsTemplates() { return foundry.applications.handlebars.loadTemplates([ "systems/deltagreen/templates/dialog/modify-percentile-roll.html", + "systems/deltagreen/templates/partials/prose-mirror-host.html", ]); } diff --git a/system.json b/system.json index fec8891..784ff8c 100644 --- a/system.json +++ b/system.json @@ -233,10 +233,12 @@ "htmlFields": ["description"] }, "tome": { - "htmlFields": ["description", "handlerNotes"] + "htmlFields": ["description", "handlerNotes"], + "gmOnlyFields": ["handlerNotes"] }, "ritual": { - "htmlFields": ["description", "handlerNotes"] + "htmlFields": ["description", "handlerNotes"], + "gmOnlyFields": ["handlerNotes"] } } } diff --git a/templates/actor/partials/notes-partial.html b/templates/actor/partials/notes-partial.html index 922f6d7..86a5815 100644 --- a/templates/actor/partials/notes-partial.html +++ b/templates/actor/partials/notes-partial.html @@ -2,5 +2,5 @@
{{localize "DG.Sheet.BlockHeaders.PhysicalDescriptionNotes"}}
-
{{{enrichedDescription}}}
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}} diff --git a/templates/item/parts/armor-description.html b/templates/item/parts/armor-description.html index 6950bf9..a12cce8 100644 --- a/templates/item/parts/armor-description.html +++ b/templates/item/parts/armor-description.html @@ -1,7 +1,5 @@
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
diff --git a/templates/item/parts/bond-description.html b/templates/item/parts/bond-description.html index 6950bf9..a12cce8 100644 --- a/templates/item/parts/bond-description.html +++ b/templates/item/parts/bond-description.html @@ -1,7 +1,5 @@
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
diff --git a/templates/item/parts/gear-description.html b/templates/item/parts/gear-description.html index 6950bf9..a12cce8 100644 --- a/templates/item/parts/gear-description.html +++ b/templates/item/parts/gear-description.html @@ -1,7 +1,5 @@
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
diff --git a/templates/item/parts/motivation-description.html b/templates/item/parts/motivation-description.html index e54c16e..a12cce8 100644 --- a/templates/item/parts/motivation-description.html +++ b/templates/item/parts/motivation-description.html @@ -1,8 +1,5 @@ -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} +
+
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
-
diff --git a/templates/item/parts/motivation-attributes.html b/templates/item/parts/motivation-header.html similarity index 91% rename from templates/item/parts/motivation-attributes.html rename to templates/item/parts/motivation-header.html index 2b3b3af..615fd93 100644 --- a/templates/item/parts/motivation-attributes.html +++ b/templates/item/parts/motivation-header.html @@ -1,15 +1,5 @@ -
-
- +
- {{localize 'DG.ItemWindow.Motivations.IsDisorder'}}: - {{localize 'DG.ItemWindow.Motivations.Motivation'}}: + {{localize 'DG.ItemWindow.Motivations.IsDisorder'}}: + {{#if item.system.crossedOut }} - {{localize 'DG.ItemWindow.Motivations.Disorder'}}: {{/if}} -
-
+ diff --git a/templates/item/parts/ritual-description.html b/templates/item/parts/ritual-description.html index 99c658f..5f81c26 100644 --- a/templates/item/parts/ritual-description.html +++ b/templates/item/parts/ritual-description.html @@ -1,9 +1,5 @@ -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
-
diff --git a/templates/item/parts/ritual-handler.html b/templates/item/parts/ritual-handler.html index 1a04cd2..e8dfd13 100644 --- a/templates/item/parts/ritual-handler.html +++ b/templates/item/parts/ritual-handler.html @@ -25,8 +25,6 @@ {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}:
-
- {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.handlerNotes}}
diff --git a/templates/item/parts/tome-description.html b/templates/item/parts/tome-description.html index 99c658f..5f81c26 100644 --- a/templates/item/parts/tome-description.html +++ b/templates/item/parts/tome-description.html @@ -1,9 +1,5 @@ -
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
-
diff --git a/templates/item/parts/tome-handler.html b/templates/item/parts/tome-handler.html index 1b02cfd..832b919 100644 --- a/templates/item/parts/tome-handler.html +++ b/templates/item/parts/tome-handler.html @@ -20,8 +20,6 @@ {{localize 'DG.ItemWindow.Tome.HandlerNotes'}}:
-
- {{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.handlerNotes}}
diff --git a/templates/item/parts/weapon-description.html b/templates/item/parts/weapon-description.html index cf1b564..930653f 100644 --- a/templates/item/parts/weapon-description.html +++ b/templates/item/parts/weapon-description.html @@ -1,7 +1,5 @@
-
- {{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}} -
+ {{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
diff --git a/templates/item/parts/weapon-header.html b/templates/item/parts/weapon-header.html index 2c66421..70352f5 100644 --- a/templates/item/parts/weapon-header.html +++ b/templates/item/parts/weapon-header.html @@ -1,4 +1,4 @@ -
+
@@ -24,78 +24,64 @@
-
+
-
+ {{localize 'DG.ItemWindow.Weapons.Skill'}}: + - {{localize 'DG.ItemWindow.Weapons.Skill'}}: - -
+ {{localize 'DG.ItemWindow.Weapons.Range'}}: + -
- {{#if_eq item.system.skill 'custom'}} - {{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}: - - {{/if_eq}} -
+ {{#if_eq item.system.skill 'custom'}} + {{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}: + + + + {{/if_eq}} -
+ {{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}: + - {{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}: - + {{localize 'DG.ItemWindow.Weapons.KillRadius'}}: + - {{localize 'DG.ItemWindow.Weapons.SkillModifier'}}: - + {{localize 'DG.ItemWindow.Weapons.SkillModifier'}}: + - {{localize 'DG.ItemWindow.Weapons.IsLethality'}}: - {{localize 'DG.ItemWindow.Weapons.Damage'}}: + + {{/unless}} + + {{#if item.system.isLethal}} + {{localize 'DG.ItemWindow.Weapons.Lethality'}} %: + + {{/if}} -
- -
- - {{localize 'DG.ItemWindow.Weapons.Range'}}: - - - {{localize 'DG.ItemWindow.Weapons.KillRadius'}}: - - - {{#unless item.system.isLethal}} - {{localize 'DG.ItemWindow.Weapons.Damage'}}: - - {{/unless}} - - {{#if item.system.isLethal}} - {{localize 'DG.ItemWindow.Weapons.Lethality'}} %: - - {{/if}} - -
- -
- -
{{localize 'DG.ItemWindow.Weapons.Ammo'}}: + + {{localize 'DG.ItemWindow.Weapons.IsLethality'}}: + +
-
diff --git a/templates/partials/prose-mirror-host.html b/templates/partials/prose-mirror-host.html new file mode 100644 index 0000000..25c30bd --- /dev/null +++ b/templates/partials/prose-mirror-host.html @@ -0,0 +1 @@ +
{{{html}}}