mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
+Dynamic textarea sizing
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user