+Dynamic textarea sizing

This commit is contained in:
Simon Martens
2026-01-09 11:20:16 +01:00
parent a08a7e5710
commit 69d8ec71b3
7 changed files with 484 additions and 298 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -117,9 +117,7 @@ type AlmanachResult struct {
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<label for="preferred_title" class="inputlabel">Kurztitel</label> <label for="preferred_title" class="inputlabel">Kurztitel</label>
</div> </div>
<textarea name="preferred_title" id="preferred_title" class="inputinput no-enter" placeholder="" required autocomplete="off" rows="1"> <textarea name="preferred_title" id="preferred_title" class="inputinput no-enter" placeholder="" required autocomplete="off" rows="1">{{- $model.result.Entry.PreferredTitle -}}</textarea>
{{- $model.result.Entry.PreferredTitle -}}
</textarea>
</div> </div>
<div class="mt-3"> <div class="mt-3">
@@ -142,9 +140,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="title" id="title" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1"> <textarea name="title" id="title" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.TitleStmt -}}</textarea>
{{- $model.result.Entry.TitleStmt -}}
</textarea>
</div> </div>
<div class="mt-2 inputwrapper {{ if eq $model.result.Entry.ParallelTitle "" }}hidden{{ end }}" data-dm-target="titles"> <div class="mt-2 inputwrapper {{ if eq $model.result.Entry.ParallelTitle "" }}hidden{{ end }}" data-dm-target="titles">
@@ -158,9 +154,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="paralleltitle" id="paralleltitle" class="inputinput" placeholder="" autocomplete="off"> <textarea name="paralleltitle" id="paralleltitle" class="inputinput" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.ParallelTitle -}}</textarea>
{{- $model.result.Entry.ParallelTitle -}}
</textarea>
</div> </div>
<div class="mt-3 inputwrapper {{ if eq $model.result.Entry.SubtitleStmt "" }}hidden{{ end }}" data-dm-target="titles"> <div class="mt-3 inputwrapper {{ if eq $model.result.Entry.SubtitleStmt "" }}hidden{{ end }}" data-dm-target="titles">
@@ -173,9 +167,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="subtitle" id="subtitle" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1"> <textarea name="subtitle" id="subtitle" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.SubtitleStmt -}}</textarea>
{{- $model.result.Entry.SubtitleStmt -}}
</textarea>
</div> </div>
<div class="mt-3 inputwrapper {{ if eq $model.result.Entry.VariantTitle "" }}hidden{{ end }}" data-dm-target="titles"> <div class="mt-3 inputwrapper {{ if eq $model.result.Entry.VariantTitle "" }}hidden{{ end }}" data-dm-target="titles">
@@ -188,9 +180,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="varianttitle" id="varianttitle" class="inputinput" placeholder="" autocomplete="off"> <textarea name="varianttitle" id="varianttitle" class="inputinput" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.VariantTitle -}}</textarea>
{{- $model.result.Entry.VariantTitle -}}
</textarea>
</div> </div>
<div class="mt-3 inputwrapper {{ if eq $model.result.Entry.IncipitStmt "" }}hidden{{ end }}" data-dm-target="titles"> <div class="mt-3 inputwrapper {{ if eq $model.result.Entry.IncipitStmt "" }}hidden{{ end }}" data-dm-target="titles">
@@ -203,9 +193,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="incipit" id="incipit" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1"> <textarea name="incipit" id="incipit" class="inputinput no-enter" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.IncipitStmt -}}</textarea>
{{- $model.result.Entry.IncipitStmt -}}
</textarea>
</div> </div>
<div class="inputwrapper {{ if eq $model.result.Entry.ResponsibilityStmt "" }}hidden{{ end }}" data-dm-target="publication"> <div class="inputwrapper {{ if eq $model.result.Entry.ResponsibilityStmt "" }}hidden{{ end }}" data-dm-target="publication">
@@ -272,7 +260,7 @@ type AlmanachResult struct {
<!-- Annotationen --> <!-- Annotationen -->
<div class="inputwrapper"> <div class="inputwrapper">
<label for="annotation" class="inputlabel">Annotationen</label> <label for="annotation" class="inputlabel">Annotationen</label>
<textarea name="annotation" id="annotation" class="inputinput" placeholder="" autocomplete="off">{{- $model.result.Entry.Annotation -}}</textarea> <textarea name="annotation" id="annotation" class="inputinput" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.Annotation -}}</textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -613,7 +601,7 @@ type AlmanachResult struct {
</div> </div>
</div> </div>
<textarea name="edit_comment" id="edit_comment" class="inputinput" placeholder="" autocomplete="off">{{- $model.result.Entry.Comment -}}</textarea> <textarea name="edit_comment" id="edit_comment" class="inputinput" placeholder="" autocomplete="off" rows="1">{{- $model.result.Entry.Comment -}}</textarea>
</div> </div>
</div-manager> </div-manager>
</div> </div>

View File

@@ -13,9 +13,12 @@ export class AlmanachEditPage extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this._initForm(); // Small delay to ensure main.js has loaded
this._initPlaces(); setTimeout(() => {
this._initSaveHandling(); this._initForm();
this._initPlaces();
this._initSaveHandling();
}, 0);
} }
disconnectedCallback() { disconnectedCallback() {
@@ -23,9 +26,13 @@ export class AlmanachEditPage extends HTMLElement {
} }
_initForm() { _initForm() {
console.log("AlmanachEditPage: _initForm called");
const form = this.querySelector("#changealmanachform"); const form = this.querySelector("#changealmanachform");
console.log("Form found:", !!form, "FormLoad exists:", typeof window.FormLoad === "function");
if (form && typeof window.FormLoad === "function") { if (form && typeof window.FormLoad === "function") {
window.FormLoad(form); window.FormLoad(form);
} else {
console.error("Cannot initialize form - form or FormLoad missing");
} }
} }
@@ -436,6 +443,15 @@ export class AlmanachEditPage extends HTMLElement {
this._initForm(); this._initForm();
this._initPlaces(); this._initPlaces();
this._initSaveHandling(); this._initSaveHandling();
// Resize all textareas after reload
if (typeof window.TextareaAutoResize === "function") {
setTimeout(() => {
this.querySelectorAll("textarea").forEach((textarea) => {
window.TextareaAutoResize(textarea);
});
}, 100);
}
} }
} }

View File

@@ -245,6 +245,19 @@ export class DivManager extends HTMLElement {
this.renderMenu(); this.renderMenu();
this.renderButton(); this.renderButton();
this.updateTargetVisibility(); this.updateTargetVisibility();
// Resize any textareas in the newly shown element
if (typeof window.TextareaAutoResize === "function") {
const textareas = child.node.querySelectorAll("textarea");
if (textareas.length > 0) {
// Small delay to ensure element is visible before measuring
setTimeout(() => {
textareas.forEach((textarea) => {
window.TextareaAutoResize(textarea);
});
}, 10);
}
}
} }
renderMenu() { renderMenu() {

View File

@@ -21,6 +21,17 @@
@apply block w-full focus:border-none focus:outline-none resize-y px-3 py-1; @apply block w-full focus:border-none focus:outline-none resize-y px-3 py-1;
} }
.dbform .inputwrapper textarea {
/* Modern browsers: use CSS auto-sizing */
field-sizing: content;
/* Styling for all browsers */
display: block;
resize: none;
max-height: 20rem;
box-sizing: border-box;
/* overflow will be managed by JS for browsers without field-sizing support */
}
.inputlabeltext { .inputlabeltext {
@apply text-gray-700 font-bold; @apply text-gray-700 font-bold;
} }

View File

@@ -177,15 +177,63 @@ function HookupRBChange(target, action) {
}); });
} }
// @param {HTMLTextAreaElement} textarea - The textarea element. // Check if browser supports field-sizing: content (Chrome 123+, Edge 123+)
// Firefox, Safari don't support it yet as of 2024
let browserSupportsFieldSizing = null;
function supportsFieldSizing() {
if (browserSupportsFieldSizing !== null) {
return browserSupportsFieldSizing;
}
// Check if CSS.supports is available and test for field-sizing
if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
browserSupportsFieldSizing = CSS.supports("field-sizing", "content");
} else {
browserSupportsFieldSizing = false;
}
console.log("Browser supports field-sizing:", browserSupportsFieldSizing);
return browserSupportsFieldSizing;
}
// Simple textarea auto-resize function
function TextareaAutoResize(textarea) { function TextareaAutoResize(textarea) {
console.log("TextareaAutoResize called for:", textarea.name || textarea.id);
if (!(textarea instanceof HTMLTextAreaElement)) { if (!(textarea instanceof HTMLTextAreaElement)) {
console.warn("TextareaAutoResize: Provided element is not a textarea."); console.log("Not a textarea element");
return; return;
} }
textarea.style.height = "auto"; // Skip if not visible
textarea.style.height = `${textarea.scrollHeight}px`; if (textarea.offsetParent === null) {
console.log("Textarea not visible");
return;
}
// Remove rows attribute
textarea.removeAttribute("rows");
// Set overflow auto to allow scrolling if needed
textarea.style.overflow = "auto";
// Special case: annotation textarea has 2 rows minimum
const isAnnotation = textarea.name === "annotation";
const minHeight = isAnnotation ? 76 : 38; // 2 rows vs 1 row
// For empty textareas, set minimum height
if (textarea.value.trim() === "") {
textarea.style.height = minHeight + "px";
console.log("Empty textarea, setting height to:", minHeight + "px");
return;
}
// Reset to 1px to get accurate scrollHeight
textarea.style.height = "1px";
// Set to content height (scrollHeight is the actual content height)
const contentHeight = textarea.scrollHeight;
const newHeight = Math.max(contentHeight, minHeight) + "px";
console.log("Setting height to:", newHeight);
textarea.style.height = newHeight;
} }
function NoEnters(event) { function NoEnters(event) {
@@ -200,7 +248,12 @@ function HookupTextareaAutoResize(textarea) {
return; return;
} }
// Reset height on input // If browser supports field-sizing, CSS handles it
if (supportsFieldSizing()) {
return;
}
// Fallback: attach event listener for manual resizing
textarea.addEventListener("input", () => { textarea.addEventListener("input", () => {
TextareaAutoResize(textarea); TextareaAutoResize(textarea);
}); });
@@ -239,18 +292,24 @@ function DisconnectNoEnters(textarea) {
} }
function MutateObserve(mutations, observer) { function MutateObserve(mutations, observer) {
const needsJSResize = !supportsFieldSizing();
for (const mutation of mutations) { for (const mutation of mutations) {
if (mutation.type === "childList") { if (mutation.type === "childList") {
for (const node of mutation.addedNodes) { for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) { if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) {
HookupTextareaAutoResize(node); if (needsJSResize) {
TextareaAutoResize(node); HookupTextareaAutoResize(node);
TextareaAutoResize(node);
}
} }
} }
for (const node of mutation.removedNodes) { for (const node of mutation.removedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) { if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) {
DisconnectNoEnters(node); DisconnectNoEnters(node);
DisconnectTextareaAutoResize(node); if (needsJSResize) {
DisconnectTextareaAutoResize(node);
}
} }
} }
} }
@@ -259,17 +318,33 @@ function MutateObserve(mutations, observer) {
// INFO: Various options and plugs for laoding and parsing forms. // INFO: Various options and plugs for laoding and parsing forms.
function FormLoad(form) { function FormLoad(form) {
console.log("=== FormLoad CALLED ===");
if (!(form instanceof HTMLFormElement)) { if (!(form instanceof HTMLFormElement)) {
console.warn("FormLoad: Provided element is not a form."); console.warn("FormLoad: Provided element is not a form.");
return; return;
} }
const textareas = document.querySelectorAll("textarea"); const textareas = document.querySelectorAll("textarea");
console.log("Found", textareas.length, "textareas");
// Attach resize handler to all textareas
for (const textarea of textareas) { for (const textarea of textareas) {
HookupTextareaAutoResize(textarea); console.log("Attaching input listener to:", textarea.name || textarea.id);
TextareaAutoResize(textarea); textarea.addEventListener('input', function() {
console.log("Input event on textarea:", this.name || this.id);
TextareaAutoResize(this);
});
} }
// Initial resize after short delay (increased to ensure content is loaded)
setTimeout(() => {
console.log("Running initial textarea resize on", textareas.length, "textareas");
for (const textarea of textareas) {
TextareaAutoResize(textarea);
}
}, 200);
const noEnterTextareas = document.querySelectorAll("textarea.no-enter"); const noEnterTextareas = document.querySelectorAll("textarea.no-enter");
for (const textarea of noEnterTextareas) { for (const textarea of noEnterTextareas) {
HookupNoEnters(textarea); HookupNoEnters(textarea);
@@ -280,6 +355,34 @@ function FormLoad(form) {
childList: true, childList: true,
subtree: true, subtree: true,
}); });
// Watch for class changes (hidden/visible) and resize textareas when they become visible
const visibilityObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const target = mutation.target;
// Check if this element or its children contain textareas
if (target instanceof HTMLElement) {
const textareasInTarget = target.matches("textarea")
? [target]
: Array.from(target.querySelectorAll("textarea"));
for (const textarea of textareasInTarget) {
// Only resize if now visible
if (textarea.offsetParent !== null) {
TextareaAutoResize(textarea);
}
}
}
}
}
});
visibilityObserver.observe(form, {
attributes: true,
attributeFilter: ["class"],
subtree: true,
});
} }
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
@@ -301,5 +404,6 @@ window.SelectableInput = SelectableInput;
window.PathPlusQuery = PathPlusQuery; window.PathPlusQuery = PathPlusQuery;
window.HookupRBChange = HookupRBChange; window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad; window.FormLoad = FormLoad;
window.TextareaAutoResize = TextareaAutoResize;
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor }; export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor };