Add the ability to display / edit the description from the item sheet

This commit is contained in:
Oliver-Akins 2025-02-10 23:04:28 -07:00
parent 46a235b603
commit 69db3ca719
13 changed files with 168 additions and 17 deletions

View file

@ -56,6 +56,7 @@
}, },
"damage": "Damage", "damage": "Damage",
"delete": "Delete", "delete": "Delete",
"description": "Description",
"difficulties": { "difficulties": {
"easy": "Easy", "easy": "Easy",
"normal": "Normal", "normal": "Normal",
@ -128,7 +129,8 @@
"location-placeholder": "New Location...", "location-placeholder": "New Location...",
"numberOfDice": "# of Dice", "numberOfDice": "# of Dice",
"rollTarget": "Target", "rollTarget": "Target",
"difficulty": "(DC: {dc})" "difficulty": "(DC: {dc})",
"RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost."
}, },
"notifs": { "notifs": {
"error": { "error": {

View file

@ -1,5 +1,7 @@
import { deleteItemFromElement, editItemFromElement } from "./utils.mjs"; import { deleteItemFromElement, editItemFromElement } from "./utils.mjs";
import { DicePool } from "./DicePool.mjs"; import { DicePool } from "./DicePool.mjs";
import { RichEditor } from "./RichEditor.mjs";
import { toBoolean } from "../consts.mjs";
/** /**
* A mixin that takes the class from HandlebarsApplicationMixin and * A mixin that takes the class from HandlebarsApplicationMixin and
@ -16,6 +18,7 @@ export function GenericAppMixin(HandlebarsApp) {
roll: this._rollDice, roll: this._rollDice,
editItem: (_event, target) => editItemFromElement(target), editItem: (_event, target) => editItemFromElement(target),
deleteItem: (_event, target) => deleteItemFromElement(target), deleteItem: (_event, target) => deleteItemFromElement(target),
openRichEditor: this.#openRichEditor,
}, },
}; };
@ -57,14 +60,39 @@ export function GenericAppMixin(HandlebarsApp) {
// #region Actions // #region Actions
/** @this {GenericRipCryptApp} */ /** @this {GenericRipCryptApp} */
static async _rollDice(_$e, el) { static async _rollDice(_event, target) {
const data = el.dataset; const data = target.dataset;
const diceCount = parseInt(data.diceCount); const diceCount = parseInt(data.diceCount);
const flavor = data.flavor; const flavor = data.flavor;
const dp = new DicePool({ diceCount, flavor }); const dp = new DicePool({ diceCount, flavor });
dp.render({ force: true }); dp.render({ force: true });
}; };
/** @this {GenericRipCryptApp} */
static async #openRichEditor(_event, target) {
const data = target.dataset;
const {
uuid,
path,
collaborative,
compact,
} = data;
if (!uuid || !path) {
console.error(`Rich Editor requires a document uuid and path to edit`);
return;
};
const document = await fromUuid(uuid);
const app = new RichEditor({
document,
path,
collaborative: toBoolean(collaborative),
compact: toBoolean(compact ),
});
app.render({ force: true });
};
// #endregion // #endregion
}; };
return GenericRipCryptApp; return GenericRipCryptApp;

View file

@ -20,8 +20,6 @@ export class AllItemSheetV1 extends GenericAppMixin(HandlebarsApplicationMixin(I
window: { window: {
resizable: false, resizable: false,
}, },
actions: {
},
form: { form: {
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false, closeOnSubmit: false,
@ -40,7 +38,7 @@ export class AllItemSheetV1 extends GenericAppMixin(HandlebarsApplicationMixin(I
ctx = await super._preparePartContext(partId, ctx, opts); ctx = await super._preparePartContext(partId, ctx, opts);
ctx.item = this.document; ctx.item = this.document;
ctx.formFields = this.document.system.getFormFields(ctx); ctx.formFields = await this.document.system.getFormFields(ctx);
Logger.debug(`Context:`, ctx); Logger.debug(`Context:`, ctx);
return ctx; return ctx;

View file

@ -79,11 +79,6 @@ export class RichEditor extends HandlebarsApplicationMixin(DocumentSheetV2) {
path: this.path, path: this.path,
}; };
console.log({
doc: this.document,
path: this.path,
value: this.document.system.description,
});
const value = getProperty(this.document, this.path); const value = getProperty(this.document, this.path);
ctx.enriched = await TextEditor.enrichHTML(value); ctx.enriched = await TextEditor.enrichHTML(value);
ctx.raw = value; ctx.raw = value;

View file

@ -1,3 +1,5 @@
const { getType } = foundry.utils;
// MARK: filePath // MARK: filePath
export function filePath(path) { export function filePath(path) {
if (path.startsWith(`/`)) { if (path.startsWith(`/`)) {
@ -6,6 +8,24 @@ export function filePath(path) {
return `systems/ripcrypt/${path}`; return `systems/ripcrypt/${path}`;
}; };
// MARK: toBoolean
/**
* Converts a value into a boolean based on the type of the value provided
*
* @param {any} val The value to convert
*/
export function toBoolean(val) {
switch (getType(val)) {
case `string`: {
return val === `true`;
};
case `number`: {
return val === 1;
};
};
return Boolean(val);
};
// MARK: documentSorter // MARK: documentSorter
/** /**
* @typedef {Object} Sortable * @typedef {Object} Sortable

View file

@ -35,7 +35,7 @@ export class CraftData extends SkillData {
// #endregion // #endregion
// #region Sheet Data // #region Sheet Data
getFormFields(_ctx) { async getFormFields(_ctx) {
const fields = [ const fields = [
{ {
id: `fate-path`, id: `fate-path`,
@ -48,6 +48,15 @@ export class CraftData extends SkillData {
value: aspect, value: aspect,
})), })),
}, },
{
id: `description`,
type: `prosemirror`,
label: `RipCrypt.common.description`,
path: `system.description`,
uuid: this.parent.uuid,
value: await TextEditor.enrichHTML(this.description),
collaborative: false,
},
{ {
type: `group`, type: `group`,
title: `RipCrypt.common.advances`, title: `RipCrypt.common.advances`,

View file

@ -48,7 +48,7 @@ export class SkillData extends foundry.abstract.TypeDataModel {
// #endregion // #endregion
// #region Sheet Data // #region Sheet Data
getFormFields(_ctx) { async getFormFields(_ctx) {
const fields = [ const fields = [
{ {
id: `fate-path`, id: `fate-path`,
@ -62,12 +62,13 @@ export class SkillData extends foundry.abstract.TypeDataModel {
})), })),
}, },
{ {
// TODO: Figure out how tf to make this work nicely on a generic level
id: `description`, id: `description`,
type: `prosemirror`, type: `prosemirror`,
label: `RipCrypt.common.description`, label: `RipCrypt.common.description`,
path: `system.description`, path: `system.description`,
collaborative: true, uuid: this.parent.uuid,
value: await TextEditor.enrichHTML(this.description),
collaborative: false,
}, },
{ {
type: `group`, type: `group`,

View file

@ -3,6 +3,7 @@ import { booleanInput } from "./booleanInput.mjs";
import { dropdownInput } from "./dropdownInput.mjs"; import { dropdownInput } from "./dropdownInput.mjs";
import { groupInput } from "./groupInput.mjs"; import { groupInput } from "./groupInput.mjs";
import { numberInput } from "./numberInput.mjs"; import { numberInput } from "./numberInput.mjs";
import { prosemirrorInput } from "./prosemirrorInput.mjs";
import { stringSet } from "./stringSet.mjs"; import { stringSet } from "./stringSet.mjs";
import { textInput } from "./textInput.mjs"; import { textInput } from "./textInput.mjs";
@ -10,6 +11,7 @@ const { getType } = foundry.utils;
const inputTypes = { const inputTypes = {
"string-set": stringSet, "string-set": stringSet,
prosemirror: prosemirrorInput,
integer: numberInput, integer: numberInput,
bar: barInput, bar: barInput,
dropdown: dropdownInput, dropdown: dropdownInput,
@ -29,7 +31,10 @@ export function formFields(inputs, opts) {
input.limited ??= true; input.limited ??= true;
}; };
if (typesToSanitize.has(getType(input.value))) { if (
input.type !== `prosemirror`
&& typesToSanitize.has(getType(input.value))
) {
input.value = Handlebars.escapeExpression(input.value); input.value = Handlebars.escapeExpression(input.value);
}; };
fields.push(inputTypes[input.type](input, opts.data.root)); fields.push(inputTypes[input.type](input, opts.data.root));

View file

@ -0,0 +1,43 @@
import { localizer } from "../../utils/Localizer.mjs";
export function prosemirrorInput(input, data) {
const label = localizer(input.label);
if (!data.meta.editable) {
return `<div data-input-type="prose-mirror">
<div class="label-row">
<div class="label">
${label}
</div>
</div>
<div class="value">
${input.value}
</div>
</div>`;
};
return `<div data-input-type="prose-mirror">
<div class="label-row">
<div class="label">
${label}
</div>
<button
type="button"
data-action="openRichEditor"
data-uuid="${input.uuid}"
data-path="${input.path}"
data-compact="${input.compact}"
data-collaborative="${input.collaborative}"
>
${localizer(`RipCrypt.common.edit`)}
</button>
</div>
<!--
This cannot be spread across multiple lines because of the :empty selector
considering whitespace as "not being empty". Though browsers will eventually
treat :empty as "empty, or only whitespace".
-->
<div class="value">${input.value}</div>
</div>`;
};

View file

@ -13,6 +13,8 @@
--input-text: white; --input-text: white;
--input-background: var(--accent-2); --input-background: var(--accent-2);
--button-text: white;
--button-background: var(--accent-2);
--pill-width: 100%; --pill-width: 100%;
--pill-border-radius: 4px; --pill-border-radius: 4px;
@ -21,6 +23,7 @@
grid-template-columns: auto 200px; grid-template-columns: auto 200px;
column-gap: var(--col-gap); column-gap: var(--col-gap);
row-gap: var(--row-gap); row-gap: var(--row-gap);
max-width: 300px;
padding: 8px; padding: 8px;
background: var(--base-background); background: var(--base-background);
@ -29,6 +32,7 @@
[data-input-type] { [data-input-type] {
display: contents; display: contents;
} }
> [data-input-type="group"] { > [data-input-type="group"] {
display: unset; display: unset;
grid-column: 1 / -1; grid-column: 1 / -1;
@ -41,6 +45,36 @@
} }
} }
> [data-input-type="prose-mirror"] {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
gap: var(--row-gap);
> .label-row {
display: flex;
flex-direction: row;
width: 100%;
justify-content: space-between;
}
.value {
background: var(--input-background);
color: var(--input-text);
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
&:empty {
display: none;
}
}
}
hr { hr {
background: var(--accent-1); background: var(--accent-1);
grid-column: 1 / -1; grid-column: 1 / -1;
@ -62,7 +96,7 @@
font-weight: bold; font-weight: bold;
} }
input, select, .value, [data-tag-count] { button, input, select, .value, [data-tag-count] {
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 4px;
} }

View file

@ -1,5 +1,10 @@
<div> <div>
{{#if editable}} {{#if editable}}
{{#if (not collaborative)}}
<p class="warning">
{{ rc-i18n "RipCrypt.Apps.RichEditor-no-collaborative" }}
</p>
{{/if}}
<prose-mirror <prose-mirror
name="{{ path }}" name="{{ path }}"
value="{{ raw }}" value="{{ raw }}"

View file

@ -5,6 +5,7 @@
@import url("./elements/button.css"); @import url("./elements/button.css");
@import url("./elements/input.css"); @import url("./elements/input.css");
@import url("./elements/lists.css"); @import url("./elements/lists.css");
@import url("./elements/p.css");
@import url("./elements/pill-bar.css"); @import url("./elements/pill-bar.css");
@import url("./elements/prose-mirror.css") layer(exceptions); @import url("./elements/prose-mirror.css") layer(exceptions);
@import url("./elements/select.css"); @import url("./elements/select.css");

View file

@ -0,0 +1,10 @@
.ripcrypt > .window-content p {
&.warning {
padding: 0.75rem;
margin: 0.25rem;
border-radius: 8px;
border-color: yellow;
border-style: solid;
border-width: 2px;
}
}