195 lines
9.2 KiB
Plaintext
195 lines
9.2 KiB
Plaintext
@model CharacterEditViewModel
|
|
@{
|
|
ViewData["Title"] = Model.CharacterID == 0 ? "New Character" : "Edit Character";
|
|
}
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Character</p>
|
|
<h1>@ViewData["Title"] <help-icon key="characters.edit" /></h1>
|
|
</div>
|
|
</div>
|
|
|
|
@if (TempData["ImageError"] is string imageError)
|
|
{
|
|
<div class="alert alert-warning">@imageError</div>
|
|
}
|
|
|
|
<form asp-action="Save" method="post" class="edit-panel" enctype="multipart/form-data">
|
|
<input asp-for="CharacterID" type="hidden" />
|
|
<input asp-for="ProjectID" type="hidden" />
|
|
<input asp-for="ImagePath" type="hidden" />
|
|
<input asp-for="ThumbnailPath" type="hidden" />
|
|
<input asp-for="AvatarImagePath" type="hidden" />
|
|
<input asp-for="AvatarThumbnailPath" type="hidden" />
|
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-8">
|
|
<label asp-for="CharacterName" class="form-label"></label>
|
|
<input asp-for="CharacterName" class="form-control" />
|
|
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Aliases</label>
|
|
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
|
@for (var i = 0; i < Model.Aliases.Count; i++)
|
|
{
|
|
<span class="badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2">
|
|
<span>@Model.Aliases[i]</span>
|
|
<button class="btn-close btn-close-sm" type="button" aria-label="Remove @Model.Aliases[i]" data-remove-alias></button>
|
|
<input type="hidden" name="Aliases" value="@Model.Aliases[i]" />
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="input-group">
|
|
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
|
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label asp-for="SexValueID" class="form-label"></label>
|
|
<select asp-for="SexValueID" asp-items="Model.SexOptions" class="form-select" data-sex-select data-custom-sex-value="@CharacterEditViewModel.CustomSexValue"></select>
|
|
<input asp-for="Sex" type="hidden" />
|
|
<div class="form-text">Choose a saved value, or select Add new... to create one.</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label asp-for="CustomSex" class="form-label"></label>
|
|
<input asp-for="CustomSex" class="form-control" placeholder="Custom value" data-custom-sex-input />
|
|
<span asp-validation-for="CustomSex" class="text-danger"></span>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label asp-for="BirthDate" class="form-label"></label>
|
|
<input asp-for="BirthDate" class="form-control" />
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label asp-for="AgeAtSeriesStart" class="form-label"></label>
|
|
<input asp-for="AgeAtSeriesStart" class="form-control" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label asp-for="Height" class="form-label"></label>
|
|
<input asp-for="Height" class="form-control" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label asp-for="EyeColour" class="form-label"></label>
|
|
<input asp-for="EyeColour" class="form-control" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label asp-for="CharacterImportance" class="form-label">Story importance <help-icon key="characters.fields.storyImportance" mode="inline" /></label>
|
|
<select asp-for="CharacterImportance" asp-items="Model.ImportanceOptions" class="form-select"></select>
|
|
<div class="form-text">Controls timeline grouping for major, secondary/supporting, and minor characters.</div>
|
|
</div>
|
|
<div class="col-md-6 d-flex align-items-end">
|
|
<div class="form-check mb-2">
|
|
<input asp-for="ShowInQuickAddBar" class="form-check-input" />
|
|
<label asp-for="ShowInQuickAddBar" class="form-check-label"></label>
|
|
<div class="form-text">Makes this item available as a draggable shortcut on the Timeline page.</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label asp-for="DefaultDescription" class="form-label"></label>
|
|
<textarea asp-for="DefaultDescription" class="form-control" rows="5"></textarea>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="visual-image-editor">
|
|
<div>
|
|
<p class="eyebrow">Character Image</p>
|
|
<partial name="_Avatar" model="@(new AvatarViewModel { DisplayName = Model.CharacterName, ImagePath = Model.AvatarImagePath ?? Model.ImagePath, ThumbnailPath = Model.AvatarThumbnailPath ?? Model.ThumbnailPath, Size = "lg", Lazy = false })" />
|
|
</div>
|
|
<div class="visual-image-editor__controls">
|
|
<label asp-for="ImageUpload" class="form-label"></label>
|
|
<input asp-for="ImageUpload" class="form-control" type="file" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp" />
|
|
<div class="form-text">JPG, PNG or WebP. The image is added to the character gallery and a thumbnail is generated automatically.</div>
|
|
@if (Model.AvatarSourceCharacterImageID.HasValue)
|
|
{
|
|
<a class="btn btn-outline-primary btn-sm mt-2" asp-action="CropAvatar" asp-route-imageId="@Model.AvatarSourceCharacterImageID.Value">Re-crop Avatar</a>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.ImagePath) || !string.IsNullOrWhiteSpace(Model.ThumbnailPath))
|
|
{
|
|
<label class="form-check mt-2">
|
|
<input asp-for="RemoveImage" class="form-check-input" />
|
|
<span class="form-check-label">Remove image</span>
|
|
</label>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-row mt-3">
|
|
<button class="btn btn-primary" type="submit">Save character</button>
|
|
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Cancel</a>
|
|
</div>
|
|
</form>
|
|
|
|
@section Scripts {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
<script>
|
|
(() => {
|
|
const select = document.querySelector("[data-sex-select]");
|
|
const custom = document.querySelector("[data-custom-sex-input]");
|
|
if (!select || !custom) return;
|
|
|
|
const customValue = select.dataset.customSexValue;
|
|
const syncCustomSex = () => {
|
|
const isCustom = select.value === customValue;
|
|
custom.disabled = !isCustom;
|
|
custom.required = isCustom;
|
|
if (!isCustom) {
|
|
custom.value = "";
|
|
}
|
|
};
|
|
|
|
select.addEventListener("change", syncCustomSex);
|
|
syncCustomSex();
|
|
})();
|
|
|
|
(() => {
|
|
const list = document.querySelector("[data-alias-list]");
|
|
const input = document.querySelector("[data-alias-input]");
|
|
const add = document.querySelector("[data-add-alias]");
|
|
if (!list || !input || !add) return;
|
|
|
|
const aliases = () => [...list.querySelectorAll("input[name='Aliases']")].map(x => x.value.toLowerCase());
|
|
const appendAlias = (value) => {
|
|
const alias = value.trim();
|
|
if (!alias || aliases().includes(alias.toLowerCase())) return;
|
|
|
|
const chip = document.createElement("span");
|
|
chip.className = "badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2";
|
|
|
|
const text = document.createElement("span");
|
|
text.textContent = alias;
|
|
|
|
const remove = document.createElement("button");
|
|
remove.className = "btn-close btn-close-sm";
|
|
remove.type = "button";
|
|
remove.setAttribute("aria-label", `Remove ${alias}`);
|
|
remove.dataset.removeAlias = "";
|
|
|
|
const hidden = document.createElement("input");
|
|
hidden.type = "hidden";
|
|
hidden.name = "Aliases";
|
|
hidden.value = alias;
|
|
|
|
chip.append(text, remove, hidden);
|
|
list.append(chip);
|
|
input.value = "";
|
|
};
|
|
|
|
add.addEventListener("click", () => appendAlias(input.value));
|
|
input.addEventListener("keydown", event => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
appendAlias(input.value);
|
|
}
|
|
});
|
|
list.addEventListener("click", event => {
|
|
if (event.target instanceof HTMLElement && event.target.matches("[data-remove-alias]")) {
|
|
event.target.closest(".badge")?.remove();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
}
|