|
4 | 4 | class="floating-panel" |
5 | 5 | role="toolbar" |
6 | 6 | :aria-label="textFormattingToolbar$()" |
| 7 | + :style="{ bottom: keyboardOffset + 'px' }" |
7 | 8 | > |
8 | 9 | <div |
9 | 10 | class="fixed-actions" |
|
106 | 107 |
|
107 | 108 | <script> |
108 | 109 |
|
109 | | - import { defineComponent, ref } from 'vue'; |
| 110 | + import { defineComponent, ref, onMounted, onUnmounted, inject } from 'vue'; |
110 | 111 | import { useToolbarActions } from '../../composables/useToolbarActions'; |
111 | 112 | import { useFormatControls } from '../../composables/useFormatControls'; |
112 | 113 | import { getTipTapEditorStrings } from '../../TipTapEditorStrings'; |
|
118 | 119 | components: { ToolbarButton, ToolbarDivider }, |
119 | 120 | setup(props, { emit }) { |
120 | 121 | const isExpanded = ref(true); |
| 122 | + const keyboardOffset = ref(0); |
| 123 | + const editor = inject('editor'); |
121 | 124 |
|
122 | 125 | const { |
123 | 126 | collapseFormattingBar$, |
|
137 | 140 | isExpanded.value = !isExpanded.value; |
138 | 141 | }; |
139 | 142 |
|
| 143 | + // Keyboard detection and positioning |
| 144 | + onMounted(() => { |
| 145 | + if (editor.value) { |
| 146 | + // Use a timeout to allow the keyboard to start appearing |
| 147 | + setTimeout(() => { |
| 148 | + const { from } = editor.value.state.selection; |
| 149 | + editor.value.view.dom.querySelector(`[pos="${from}"]`)?.scrollIntoView({ |
| 150 | + behavior: 'smooth', |
| 151 | + block: 'nearest', |
| 152 | + }); |
| 153 | + }, 150); |
| 154 | + } |
| 155 | +
|
| 156 | + const vk = navigator.virtualKeyboard; |
| 157 | +
|
| 158 | + const updatePositionWithVisualViewport = () => { |
| 159 | + if (window.visualViewport) { |
| 160 | + const visibleHeight = window.visualViewport.height; |
| 161 | + const totalHeight = window.innerHeight; |
| 162 | +
|
| 163 | + if (totalHeight - visibleHeight > 50) { |
| 164 | + keyboardOffset.value = totalHeight - visibleHeight; |
| 165 | + } else { |
| 166 | + keyboardOffset.value = 0; |
| 167 | + } |
| 168 | + } |
| 169 | + }; |
| 170 | +
|
| 171 | + if (vk) { |
| 172 | + // Use the new VirtualKeyboard API if available |
| 173 | + vk.overlaysContent = true; |
| 174 | + const onGeometryChange = () => { |
| 175 | + const { height } = vk.boundingRect; |
| 176 | + keyboardOffset.value = height > 0 ? height : 0; |
| 177 | + }; |
| 178 | + vk.addEventListener('geometrychange', onGeometryChange); |
| 179 | + onUnmounted(() => { |
| 180 | + vk.removeEventListener('geometrychange', onGeometryChange); |
| 181 | + vk.overlaysContent = false; |
| 182 | + }); |
| 183 | + } else if (window.visualViewport) { |
| 184 | + // Fallback to visualViewport for iOS Safari |
| 185 | + window.visualViewport.addEventListener('resize', updatePositionWithVisualViewport); |
| 186 | + updatePositionWithVisualViewport(); // Initial check |
| 187 | +
|
| 188 | + onUnmounted(() => { |
| 189 | + if (window.visualViewport) { |
| 190 | + window.visualViewport.removeEventListener('resize', updatePositionWithVisualViewport); |
| 191 | + } |
| 192 | + }); |
| 193 | + } |
| 194 | + }); |
| 195 | +
|
140 | 196 | return { |
141 | 197 | isExpanded, |
| 198 | + keyboardOffset, |
142 | 199 | textActions, |
143 | 200 | listActions, |
144 | 201 | scriptActions, |
|
166 | 223 | .floating-panel { |
167 | 224 | position: fixed; |
168 | 225 | right: 0; |
169 | | -
|
170 | | - /* Use visual viewport bottom instead of layout viewport */ |
171 | | - bottom: calc(100vh - 100dvh); |
| 226 | + bottom: 0; /* Will be overridden by JavaScript */ |
172 | 227 | left: 0; |
173 | 228 | z-index: 100; |
174 | 229 | display: flex; |
|
178 | 233 | animation: slide-up 0.2s ease-out; |
179 | 234 | } |
180 | 235 |
|
181 | | - /* Fallback for older browsers */ |
182 | | - @supports not (height: 100dvh) { |
183 | | - .floating-panel { |
184 | | - bottom: 0; |
185 | | - } |
186 | | - } |
187 | | -
|
188 | 236 | @keyframes slide-up { |
189 | 237 | from { |
190 | 238 | opacity: 0; |
|
0 commit comments