|
|
|
|
@ -146,17 +146,65 @@
@@ -146,17 +146,65 @@
|
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<div class="textarea-wrapper"> |
|
|
|
|
<textarea |
|
|
|
|
bind:this={textareaRef} |
|
|
|
|
bind:value |
|
|
|
|
{placeholder} |
|
|
|
|
class="w-full p-3 border border-fog-border dark:border-fog-dark-border rounded bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text {showToolbar ? 'has-buttons' : ''}" |
|
|
|
|
{rows} |
|
|
|
|
{disabled} |
|
|
|
|
<div class="textarea-container"> |
|
|
|
|
<textarea |
|
|
|
|
bind:this={textareaRef} |
|
|
|
|
bind:value |
|
|
|
|
{placeholder} |
|
|
|
|
class="w-full p-3 border border-fog-border dark:border-fog-dark-border rounded bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text" |
|
|
|
|
{rows} |
|
|
|
|
{disabled} |
|
|
|
|
oninput={(e) => { |
|
|
|
|
if (onValueChange) { |
|
|
|
|
onValueChange(e.currentTarget.value); |
|
|
|
|
} |
|
|
|
|
// On mobile, ensure cursor stays in view within the textarea |
|
|
|
|
if (textareaRef && window.innerWidth <= 768) { |
|
|
|
|
// Use requestAnimationFrame to ensure DOM is updated |
|
|
|
|
requestAnimationFrame(() => { |
|
|
|
|
if (textareaRef) { |
|
|
|
|
// Scroll the textarea to show the cursor |
|
|
|
|
const cursorPos = textareaRef.selectionStart; |
|
|
|
|
const textBeforeCursor = textareaRef.value.substring(0, cursorPos); |
|
|
|
|
|
|
|
|
|
// Create a temporary element to measure cursor position |
|
|
|
|
const measureDiv = document.createElement('div'); |
|
|
|
|
const computedStyle = window.getComputedStyle(textareaRef); |
|
|
|
|
measureDiv.style.cssText = computedStyle.cssText; |
|
|
|
|
measureDiv.style.visibility = 'hidden'; |
|
|
|
|
measureDiv.style.position = 'absolute'; |
|
|
|
|
measureDiv.style.whiteSpace = 'pre-wrap'; |
|
|
|
|
measureDiv.style.wordWrap = 'break-word'; |
|
|
|
|
measureDiv.style.width = computedStyle.width; |
|
|
|
|
measureDiv.textContent = textBeforeCursor; |
|
|
|
|
document.body.appendChild(measureDiv); |
|
|
|
|
|
|
|
|
|
const cursorTop = measureDiv.offsetHeight; |
|
|
|
|
document.body.removeChild(measureDiv); |
|
|
|
|
|
|
|
|
|
// Calculate scroll position to keep cursor visible |
|
|
|
|
const textareaHeight = textareaRef.clientHeight; |
|
|
|
|
const lineHeight = parseFloat(computedStyle.lineHeight) || 20; |
|
|
|
|
const bottomPadding = 2 * lineHeight; // Keep 2 lines of padding at bottom |
|
|
|
|
const topPadding = 2 * lineHeight; // Keep 2 lines of padding at top |
|
|
|
|
|
|
|
|
|
// Get the total scroll height to know how much content there is |
|
|
|
|
const scrollHeight = textareaRef.scrollHeight; |
|
|
|
|
const maxScrollTop = scrollHeight - textareaHeight; |
|
|
|
|
|
|
|
|
|
// Scroll to show cursor with padding |
|
|
|
|
if (cursorTop < textareaRef.scrollTop + topPadding) { |
|
|
|
|
// Cursor is above visible area |
|
|
|
|
textareaRef.scrollTop = Math.max(0, cursorTop - topPadding); |
|
|
|
|
} else if (cursorTop > textareaRef.scrollTop + textareaHeight - bottomPadding) { |
|
|
|
|
// Cursor is below visible area - scroll down to show it with bottom padding |
|
|
|
|
// Ensure we don't scroll past the maximum |
|
|
|
|
const targetScroll = cursorTop - textareaHeight + bottomPadding; |
|
|
|
|
textareaRef.scrollTop = Math.min(maxScrollTop, Math.max(0, targetScroll)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
onkeydown={(e) => { |
|
|
|
|
// CTRL+ENTER or CMD+ENTER to publish |
|
|
|
|
@ -164,12 +212,73 @@
@@ -164,12 +212,73 @@
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
onPublish(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// On mobile, also handle scrolling on keydown for better responsiveness |
|
|
|
|
if (textareaRef && window.innerWidth <= 768 && !e.ctrlKey && !e.metaKey) { |
|
|
|
|
// Use a small delay to let the input update first |
|
|
|
|
setTimeout(() => { |
|
|
|
|
if (textareaRef) { |
|
|
|
|
const cursorPos = textareaRef.selectionStart; |
|
|
|
|
const textBeforeCursor = textareaRef.value.substring(0, cursorPos); |
|
|
|
|
|
|
|
|
|
// Create a temporary element to measure cursor position |
|
|
|
|
const measureDiv = document.createElement('div'); |
|
|
|
|
const computedStyle = window.getComputedStyle(textareaRef); |
|
|
|
|
measureDiv.style.cssText = computedStyle.cssText; |
|
|
|
|
measureDiv.style.visibility = 'hidden'; |
|
|
|
|
measureDiv.style.position = 'absolute'; |
|
|
|
|
measureDiv.style.whiteSpace = 'pre-wrap'; |
|
|
|
|
measureDiv.style.wordWrap = 'break-word'; |
|
|
|
|
measureDiv.style.width = computedStyle.width; |
|
|
|
|
measureDiv.textContent = textBeforeCursor; |
|
|
|
|
document.body.appendChild(measureDiv); |
|
|
|
|
|
|
|
|
|
const cursorTop = measureDiv.offsetHeight; |
|
|
|
|
document.body.removeChild(measureDiv); |
|
|
|
|
|
|
|
|
|
// Calculate scroll position to keep cursor visible |
|
|
|
|
const textareaHeight = textareaRef.clientHeight; |
|
|
|
|
const lineHeight = parseFloat(computedStyle.lineHeight) || 20; |
|
|
|
|
const bottomPadding = 2 * lineHeight; // Keep 2 lines of padding at bottom |
|
|
|
|
const topPadding = 2 * lineHeight; // Keep 2 lines of padding at top |
|
|
|
|
|
|
|
|
|
const scrollHeight = textareaRef.scrollHeight; |
|
|
|
|
const maxScrollTop = scrollHeight - textareaHeight; |
|
|
|
|
|
|
|
|
|
// Scroll to show cursor with padding |
|
|
|
|
if (cursorTop < textareaRef.scrollTop + topPadding) { |
|
|
|
|
textareaRef.scrollTop = Math.max(0, cursorTop - topPadding); |
|
|
|
|
} else if (cursorTop > textareaRef.scrollTop + textareaHeight - bottomPadding) { |
|
|
|
|
const targetScroll = cursorTop - textareaHeight + bottomPadding; |
|
|
|
|
textareaRef.scrollTop = Math.min(maxScrollTop, Math.max(0, targetScroll)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, 10); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
></textarea> |
|
|
|
|
|
|
|
|
|
{#if textareaRef} |
|
|
|
|
<MentionsAutocomplete textarea={textareaRef} /> |
|
|
|
|
{/if} |
|
|
|
|
onfocus={(e) => { |
|
|
|
|
// On mobile, ensure textarea is scrolled into view when focused |
|
|
|
|
if (window.innerWidth <= 768 && textareaRef) { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
if (textareaRef) { |
|
|
|
|
// Scroll the page to show the textarea, but only if it's not fully visible |
|
|
|
|
const rect = textareaRef.getBoundingClientRect(); |
|
|
|
|
const viewportHeight = window.innerHeight; |
|
|
|
|
const isFullyVisible = rect.top >= 0 && rect.bottom <= viewportHeight; |
|
|
|
|
|
|
|
|
|
if (!isFullyVisible) { |
|
|
|
|
textareaRef.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, 100); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
></textarea> |
|
|
|
|
|
|
|
|
|
{#if textareaRef} |
|
|
|
|
<MentionsAutocomplete textarea={textareaRef} /> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{#if showToolbar} |
|
|
|
|
<div class="textarea-buttons"> |
|
|
|
|
@ -226,29 +335,34 @@
@@ -226,29 +335,34 @@
|
|
|
|
|
position: relative; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.textarea-container { |
|
|
|
|
position: relative; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
textarea { |
|
|
|
|
resize: vertical; |
|
|
|
|
min-height: 100px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Add padding to bottom when buttons are visible to prevent text overlap */ |
|
|
|
|
textarea.has-buttons { |
|
|
|
|
padding-bottom: 3rem; /* Increased padding to accommodate buttons */ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
textarea:focus { |
|
|
|
|
outline: none; |
|
|
|
|
border-color: var(--fog-accent, #64748b); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.textarea-buttons { |
|
|
|
|
position: absolute; |
|
|
|
|
bottom: 0.5rem; |
|
|
|
|
left: 0.75rem; |
|
|
|
|
display: flex; |
|
|
|
|
gap: 0.25rem; |
|
|
|
|
z-index: 10; |
|
|
|
|
padding-top: 0.5rem; /* Add padding above buttons to separate from text */ |
|
|
|
|
gap: 0.5rem; |
|
|
|
|
margin-top: 0.75rem; |
|
|
|
|
padding: 0.5rem; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
border: 1px solid var(--fog-border, #cbd5e1); |
|
|
|
|
border-radius: 0.25rem; |
|
|
|
|
justify-content: flex-start; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .textarea-buttons { |
|
|
|
|
background: var(--fog-dark-post, #334155); |
|
|
|
|
border-color: var(--fog-dark-border, #475569); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.upload-button { |
|
|
|
|
|