modified web/src/routes/articles/[id]/+page.svelte +24 -1
| @@ -103,6 +103,25 @@ |
| 103 | 103 | if (text) openForSelection(); |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | + // Mobile browsers (e.g. Chrome on Android) finalize text selections via |
| 107 | + // the selection handles and do not reliably emit mouseup afterwards. Poll |
| 108 | + // selectionchange so the popover appears once a selection inside the |
| 109 | + // article body becomes available. |
| 110 | + let selectionTimer: ReturnType<typeof setTimeout> | undefined; |
| 111 | + function onSelectionChange() { |
| 112 | + if (popoverEl?.contains(document.activeElement)) return; |
| 113 | + clearTimeout(selectionTimer); |
| 114 | + selectionTimer = setTimeout(() => { |
| 115 | + const sel = window.getSelection(); |
| 116 | + if (!sel || sel.rangeCount === 0) return; |
| 117 | + const text = sel.toString().trim(); |
| 118 | + if (!text) return; |
| 119 | + const range = sel.getRangeAt(0); |
| 120 | + if (!bodyEl?.contains(range.commonAncestorContainer)) return; |
| 121 | + openForSelection(); |
| 122 | + }, 200); |
| 123 | + } |
| 124 | + |
| 106 | 125 | function closePopover() { |
| 107 | 126 | popoverOpen = false; |
| 108 | 127 | quoteValue = ""; |
| @@ -195,7 +214,11 @@ |
| 195 | 214 | } |
| 196 | 215 | </script> |
| 197 | 216 | |
| 198 | | -<svelte:window onmouseup={onMouseUp} onkeydown={onKeydown} /> |
| 217 | +<svelte:window |
| 218 | + onmouseup={onMouseUp} |
| 219 | + onkeydown={onKeydown} |
| 220 | +/> |
| 221 | +<svelte:document onselectionchange={onSelectionChange} /> |
| 199 | 222 | |
| 200 | 223 | <div class="mx-auto max-w-3xl"> |
| 201 | 224 | <!-- Top nav --> |
| @@ -103,6 +103,25 @@ |
| 103 | if (text) openForSelection(); | 103 | if (text) openForSelection(); |
| 104 | } | 104 | } |
| 105 | | 105 | |
| | 106 | + // Mobile browsers (e.g. Chrome on Android) finalize text selections via |
| | 107 | + // the selection handles and do not reliably emit mouseup afterwards. Poll |
| | 108 | + // selectionchange so the popover appears once a selection inside the |
| | 109 | + // article body becomes available. |
| | 110 | + let selectionTimer: ReturnType<typeof setTimeout> | undefined; |
| | 111 | + function onSelectionChange() { |
| | 112 | + if (popoverEl?.contains(document.activeElement)) return; |
| | 113 | + clearTimeout(selectionTimer); |
| | 114 | + selectionTimer = setTimeout(() => { |
| | 115 | + const sel = window.getSelection(); |
| | 116 | + if (!sel || sel.rangeCount === 0) return; |
| | 117 | + const text = sel.toString().trim(); |
| | 118 | + if (!text) return; |
| | 119 | + const range = sel.getRangeAt(0); |
| | 120 | + if (!bodyEl?.contains(range.commonAncestorContainer)) return; |
| | 121 | + openForSelection(); |
| | 122 | + }, 200); |
| | 123 | + } |
| | 124 | + |
| 106 | function closePopover() { | 125 | function closePopover() { |
| 107 | popoverOpen = false; | 126 | popoverOpen = false; |
| 108 | quoteValue = ""; | 127 | quoteValue = ""; |
| @@ -195,7 +214,11 @@ |
| 195 | } | 214 | } |
| 196 | </script> | 215 | </script> |
| 197 | | 216 | |
| 198 | -<svelte:window onmouseup={onMouseUp} onkeydown={onKeydown} /> | 217 | +<svelte:window |
| | 218 | + onmouseup={onMouseUp} |
| | 219 | + onkeydown={onKeydown} |
| | 220 | +/> |
| | 221 | +<svelte:document onselectionchange={onSelectionChange} /> |
| 199 | | 222 | |
| 200 | <div class="mx-auto max-w-3xl"> | 223 | <div class="mx-auto max-w-3xl"> |
| 201 | <!-- Top nav --> | 224 | <!-- Top nav --> |