more fixes for pad editor, mainly for mobile
This commit is contained in:
parent
5d55c6de4a
commit
bee420feb3
|
@ -21,8 +21,25 @@
|
||||||
let currentWord = $state('');
|
let currentWord = $state('');
|
||||||
let selectedIndex = $state(-1);
|
let selectedIndex = $state(-1);
|
||||||
let cursorPosition = $state({ x: 0, y: 0 });
|
let cursorPosition = $state({ x: 0, y: 0 });
|
||||||
|
let isMobile = $state(false);
|
||||||
const debounce = createDebounce();
|
const debounce = createDebounce();
|
||||||
|
|
||||||
|
// Check for mobile on component mount and resize
|
||||||
|
function checkMobile() {
|
||||||
|
isMobile = window.innerWidth < 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Add resize listener for responsive behavior
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
checkMobile();
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkMobile);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (editor?.getHTML()) {
|
if (editor?.getHTML()) {
|
||||||
localStorage.setItem('tipex', editor.getHTML());
|
localStorage.setItem('tipex', editor.getHTML());
|
||||||
|
@ -49,7 +66,7 @@
|
||||||
currentWord = '';
|
currentWord = '';
|
||||||
}
|
}
|
||||||
updateCursorPosition();
|
updateCursorPosition();
|
||||||
}, 300);
|
}, 1000);
|
||||||
|
|
||||||
const handleSelectionChange = debounce(async () => {
|
const handleSelectionChange = debounce(async () => {
|
||||||
const selection = editor?.view.state.selection;
|
const selection = editor?.view.state.selection;
|
||||||
|
@ -63,6 +80,15 @@
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
// Special handler for touch events
|
||||||
|
function handleTouchEnd(event: TouchEvent) {
|
||||||
|
// Prevent default only if we have suggestions
|
||||||
|
if (suggestions.length > 0 || rhymes.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
handleSelectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
function clearSuggestions() {
|
function clearSuggestions() {
|
||||||
suggestions = [];
|
suggestions = [];
|
||||||
rhymes = [];
|
rhymes = [];
|
||||||
|
@ -129,7 +155,7 @@
|
||||||
<div
|
<div
|
||||||
class="container mx-auto my-8 dark relative px-4"
|
class="container mx-auto my-8 dark relative px-4"
|
||||||
onmouseup={handleSelectionChange}
|
onmouseup={handleSelectionChange}
|
||||||
ontouchend={handleSelectionChange}
|
ontouchend={handleTouchEnd}
|
||||||
onmousedown={clearSuggestions}
|
onmousedown={clearSuggestions}
|
||||||
ontouchstart={clearSuggestions}
|
ontouchstart={clearSuggestions}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
|
@ -144,7 +170,7 @@
|
||||||
|
|
||||||
{#if suggestions.length > 0 || rhymes.length > 0}
|
{#if suggestions.length > 0 || rhymes.length > 0}
|
||||||
<div
|
<div
|
||||||
class="suggestions-container"
|
class="suggestions-container {isMobile ? 'mobile-suggestions' : ''}"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="Suggestions and rhymes"
|
aria-label="Suggestions and rhymes"
|
||||||
style:left="{cursorPosition.x}px"
|
style:left="{cursorPosition.x}px"
|
||||||
|
@ -184,5 +210,30 @@
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
touch-action: manipulation;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile-specific styles */
|
||||||
|
.mobile-suggestions {
|
||||||
|
max-width: 90vw;
|
||||||
|
width: 90vw;
|
||||||
|
max-height: 200px;
|
||||||
|
left: 50% !important; /* Override inline styles */
|
||||||
|
bottom: 20px !important;
|
||||||
|
top: auto !important;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.suggestions-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: auto !important;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,6 +10,13 @@
|
||||||
selectedIndex: number;
|
selectedIndex: number;
|
||||||
onSelect: (word: string) => void;
|
onSelect: (word: string) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
|
||||||
|
function handleTouchSelect(event: TouchEvent, word: string) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSelect(word);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-md p-2 mb-2">
|
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-md p-2 mb-2">
|
||||||
|
@ -17,13 +24,28 @@
|
||||||
{#each suggestions.slice(0, 10) as { word }, i}
|
{#each suggestions.slice(0, 10) as { word }, i}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="block w-full text-left px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded
|
class="block w-full text-left px-2 {isMobile
|
||||||
|
? 'py-3'
|
||||||
|
: 'py-1'} hover:bg-gray-100 dark:hover:bg-gray-700 rounded
|
||||||
{selectedIndex === i ? 'bg-gray-100 dark:bg-gray-700' : ''}"
|
{selectedIndex === i ? 'bg-gray-100 dark:bg-gray-700' : ''}"
|
||||||
onmousedown={() => onSelect(word)}
|
onmousedown={() => onSelect(word)}
|
||||||
|
ontouchstart={(e) => handleTouchSelect(e, word)}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={selectedIndex === i}
|
aria-selected={selectedIndex === i}
|
||||||
|
style:touch-action="manipulation"
|
||||||
>
|
>
|
||||||
{word}
|
{word}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Mobile-specific styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
button {
|
||||||
|
padding: 12px 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 16px; /* Minimum readable size on mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -8,6 +8,19 @@ export function calculateCursorPosition(editor: TipexEditor | undefined) {
|
||||||
const rect = range.getBoundingClientRect();
|
const rect = range.getBoundingClientRect();
|
||||||
const editorRect = editor?.view.dom.getBoundingClientRect() || { left: 0, top: 0 };
|
const editorRect = editor?.view.dom.getBoundingClientRect() || { left: 0, top: 0 };
|
||||||
|
|
||||||
|
// Check if on mobile
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// For mobile, position suggestions at the bottom center of the screen
|
||||||
|
// to avoid issues with virtual keyboard and to make them more accessible
|
||||||
|
return {
|
||||||
|
x: window.innerWidth / 2,
|
||||||
|
y: Math.min(rect.bottom - editorRect.top + window.scrollY, window.innerHeight - 250)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop positioning
|
||||||
const x = Math.min(
|
const x = Math.min(
|
||||||
Math.max(rect.left - editorRect.left + window.scrollX, 100),
|
Math.max(rect.left - editorRect.left + window.scrollX, 100),
|
||||||
window.innerWidth - 300
|
window.innerWidth - 300
|
||||||
|
|
|
@ -9,16 +9,44 @@ function extractLastWord(text: string): string {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the current word at cursor position from the document
|
* Extracts the current word at cursor position from the document
|
||||||
|
* Improved to handle different selection scenarios on mobile
|
||||||
*/
|
*/
|
||||||
export function extractCurrentWord(doc: any, pos: number): string {
|
export function extractCurrentWord(doc: any, pos: number): string {
|
||||||
const textBefore = doc.textBetween(Math.max(0, pos - 100), pos);
|
// For mobile, we might need to look further back to find words
|
||||||
return extractLastWord(textBefore);
|
// since touch selection can be less precise
|
||||||
|
const lookbackDistance = window.innerWidth < 768 ? 200 : 100;
|
||||||
|
const textBefore = doc.textBetween(Math.max(0, pos - lookbackDistance), pos);
|
||||||
|
|
||||||
|
// First try with standard pattern
|
||||||
|
const word = extractLastWord(textBefore);
|
||||||
|
|
||||||
|
// If standard pattern fails, try a more lenient pattern for mobile
|
||||||
|
if (!word && window.innerWidth < 768) {
|
||||||
|
const lenientMatches = textBefore.match(/[a-zA-Z]+(?:\s+[a-zA-Z]+)*\s*$/);
|
||||||
|
return lenientMatches ? lenientMatches[0].trim().split(/\s+/).pop() || '' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the last word from a selected text range
|
* Extracts the last word from a selected text range
|
||||||
|
* Improved to handle imprecise selections on mobile
|
||||||
*/
|
*/
|
||||||
export function extractSelectedWord(doc: any, from: number, to: number): string {
|
export function extractSelectedWord(doc: any, from: number, to: number): string {
|
||||||
|
// On mobile, selections might include extra spaces
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
const selectedText = doc.textBetween(from, to, ' ');
|
const selectedText = doc.textBetween(from, to, ' ');
|
||||||
|
|
||||||
|
if (isMobile && selectedText.trim()) {
|
||||||
|
// For mobile, take the largest word in the selection
|
||||||
|
const words = selectedText.trim().split(/\s+/);
|
||||||
|
if (words.length > 0) {
|
||||||
|
// Find the longest word in selection (mobile users often select multiple words)
|
||||||
|
return words.reduce((longest: string, current: string) =>
|
||||||
|
current.length > longest.length ? current : longest, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return extractLastWord(selectedText);
|
return extractLastWord(selectedText);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue