@trustquery/browser 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/trustquery.js +548 -25
- package/dist/trustquery.js.map +1 -1
- package/package.json +2 -2
- package/src/CommandHandlers.js +44 -0
- package/src/DropdownManager.js +124 -24
- package/src/MobileKeyboardHandler.js +240 -0
- package/src/StyleManager.js +6 -2
- package/src/TrustQuery.js +22 -0
- package/src/dropdown-manager-helpers/EdgeDetectionHelper.js +113 -0
package/dist/trustquery.js
CHANGED
|
@@ -764,8 +764,123 @@ class BubbleManager {
|
|
|
764
764
|
}
|
|
765
765
|
}
|
|
766
766
|
|
|
767
|
+
// EdgeDetectionHelper - Calculates dropdown positioning to prevent overflow off viewport edges
|
|
768
|
+
|
|
769
|
+
class EdgeDetectionHelper {
|
|
770
|
+
/**
|
|
771
|
+
* Calculate optimal position for dropdown to prevent viewport overflow
|
|
772
|
+
* @param {Object} options - Positioning options
|
|
773
|
+
* @param {DOMRect} options.matchRect - Bounding rect of the match element
|
|
774
|
+
* @param {DOMRect} options.dropdownRect - Bounding rect of the dropdown
|
|
775
|
+
* @param {number} options.offset - Offset from match element (default: 28)
|
|
776
|
+
* @param {number} options.padding - Padding from viewport edges (default: 10)
|
|
777
|
+
* @returns {Object} - { top, left } position in pixels
|
|
778
|
+
*/
|
|
779
|
+
static calculatePosition({ matchRect, dropdownRect, offset = 28, padding = 10 }) {
|
|
780
|
+
const viewportWidth = window.innerWidth;
|
|
781
|
+
const viewportHeight = window.innerHeight;
|
|
782
|
+
|
|
783
|
+
console.log('[EdgeDetectionHelper] Calculation inputs:');
|
|
784
|
+
console.log(' Match rect:', { left: matchRect.left, top: matchRect.top, width: matchRect.width });
|
|
785
|
+
console.log(' Dropdown rect:', { width: dropdownRect.width, height: dropdownRect.height });
|
|
786
|
+
console.log(' Viewport:', { width: viewportWidth, height: viewportHeight });
|
|
787
|
+
console.log(' Scroll:', { x: window.scrollX, y: window.scrollY });
|
|
788
|
+
|
|
789
|
+
// Calculate initial position (above match by default, since input is at bottom)
|
|
790
|
+
let top = matchRect.top + window.scrollY - dropdownRect.height - offset;
|
|
791
|
+
let left = matchRect.left + window.scrollX;
|
|
792
|
+
|
|
793
|
+
console.log(' Initial position:', { left, top });
|
|
794
|
+
|
|
795
|
+
// Vertical positioning: Check if dropdown goes off top edge
|
|
796
|
+
if (top < window.scrollY) {
|
|
797
|
+
// Position below match instead
|
|
798
|
+
top = matchRect.bottom + window.scrollY + offset;
|
|
799
|
+
console.log(' Adjusted for top overflow, new top:', top);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Horizontal positioning: Check if dropdown goes off right edge
|
|
803
|
+
const rightEdge = left + dropdownRect.width;
|
|
804
|
+
const viewportRightEdge = viewportWidth + window.scrollX;
|
|
805
|
+
|
|
806
|
+
console.log(' Right edge check:', { rightEdge, viewportRightEdge: viewportRightEdge - padding });
|
|
807
|
+
|
|
808
|
+
if (rightEdge > viewportRightEdge - padding) {
|
|
809
|
+
// Calculate how much we overflow past the right edge
|
|
810
|
+
const overflow = rightEdge - (viewportRightEdge - padding);
|
|
811
|
+
console.log(' Right overflow detected:', overflow, 'px');
|
|
812
|
+
|
|
813
|
+
// Shift left by the overflow amount
|
|
814
|
+
left = left - overflow;
|
|
815
|
+
console.log(' Adjusted left position:', left);
|
|
816
|
+
|
|
817
|
+
// Ensure we don't go off the left edge either
|
|
818
|
+
const minLeft = window.scrollX + padding;
|
|
819
|
+
if (left < minLeft) {
|
|
820
|
+
console.log(' Hit left edge, clamping to:', minLeft);
|
|
821
|
+
left = minLeft;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Also check left edge (in case match is near left edge)
|
|
826
|
+
const minLeft = window.scrollX + padding;
|
|
827
|
+
if (left < minLeft) {
|
|
828
|
+
console.log(' Left edge adjustment:', minLeft);
|
|
829
|
+
left = minLeft;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
console.log('[EdgeDetectionHelper] Final position:', { left, top });
|
|
833
|
+
|
|
834
|
+
return { top, left };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Check if dropdown would overflow the right edge
|
|
839
|
+
* @param {DOMRect} matchRect - Match element rect
|
|
840
|
+
* @param {DOMRect} dropdownRect - Dropdown rect
|
|
841
|
+
* @param {number} padding - Padding from edge
|
|
842
|
+
* @returns {boolean} - True if would overflow
|
|
843
|
+
*/
|
|
844
|
+
static wouldOverflowRight(matchRect, dropdownRect, padding = 10) {
|
|
845
|
+
const left = matchRect.left + window.scrollX;
|
|
846
|
+
const rightEdge = left + dropdownRect.width;
|
|
847
|
+
const viewportRightEdge = window.innerWidth + window.scrollX;
|
|
848
|
+
|
|
849
|
+
return rightEdge > viewportRightEdge - padding;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Check if dropdown would overflow the top edge
|
|
854
|
+
* @param {DOMRect} matchRect - Match element rect
|
|
855
|
+
* @param {DOMRect} dropdownRect - Dropdown rect
|
|
856
|
+
* @param {number} offset - Offset from match
|
|
857
|
+
* @returns {boolean} - True if would overflow
|
|
858
|
+
*/
|
|
859
|
+
static wouldOverflowTop(matchRect, dropdownRect, offset = 28) {
|
|
860
|
+
const top = matchRect.top + window.scrollY - dropdownRect.height - offset;
|
|
861
|
+
return top < window.scrollY;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Calculate overflow amount on the right edge
|
|
866
|
+
* @param {DOMRect} matchRect - Match element rect
|
|
867
|
+
* @param {DOMRect} dropdownRect - Dropdown rect
|
|
868
|
+
* @param {number} padding - Padding from edge
|
|
869
|
+
* @returns {number} - Overflow amount in pixels (0 if no overflow)
|
|
870
|
+
*/
|
|
871
|
+
static calculateRightOverflow(matchRect, dropdownRect, padding = 10) {
|
|
872
|
+
const left = matchRect.left + window.scrollX;
|
|
873
|
+
const rightEdge = left + dropdownRect.width;
|
|
874
|
+
const viewportRightEdge = window.innerWidth + window.scrollX;
|
|
875
|
+
|
|
876
|
+
const overflow = rightEdge - (viewportRightEdge - padding);
|
|
877
|
+
return Math.max(0, overflow);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
767
881
|
// DropdownManager - Handles dropdown menus with filtering, keyboard navigation, and selection
|
|
768
882
|
|
|
883
|
+
|
|
769
884
|
class DropdownManager {
|
|
770
885
|
/**
|
|
771
886
|
* Create dropdown manager
|
|
@@ -819,9 +934,10 @@ class DropdownManager {
|
|
|
819
934
|
const dropdown = document.createElement('div');
|
|
820
935
|
dropdown.className = 'tq-dropdown';
|
|
821
936
|
|
|
822
|
-
// Apply inline styles via StyleManager
|
|
937
|
+
// Apply inline styles via StyleManager (pass category for width adjustment)
|
|
938
|
+
const category = matchData.intent?.category || '';
|
|
823
939
|
if (this.options.styleManager) {
|
|
824
|
-
this.options.styleManager.applyDropdownStyles(dropdown);
|
|
940
|
+
this.options.styleManager.applyDropdownStyles(dropdown, category);
|
|
825
941
|
}
|
|
826
942
|
|
|
827
943
|
// Add header container based on message-state
|
|
@@ -1005,6 +1121,10 @@ class DropdownManager {
|
|
|
1005
1121
|
* @param {Object} matchData - Match data
|
|
1006
1122
|
*/
|
|
1007
1123
|
createDropdownItems(dropdown, options, matchData) {
|
|
1124
|
+
// Check if this is a display-menu-with-uri category
|
|
1125
|
+
const category = matchData.intent?.category || '';
|
|
1126
|
+
const hasUriSupport = category === 'display-menu-with-uri';
|
|
1127
|
+
|
|
1008
1128
|
options.forEach((option, index) => {
|
|
1009
1129
|
// Check if this is a user-input option
|
|
1010
1130
|
if (typeof option === 'object' && option['user-input'] === true) {
|
|
@@ -1014,7 +1134,82 @@ class DropdownManager {
|
|
|
1014
1134
|
|
|
1015
1135
|
const item = document.createElement('div');
|
|
1016
1136
|
item.className = 'tq-dropdown-item';
|
|
1017
|
-
|
|
1137
|
+
|
|
1138
|
+
// Create label text
|
|
1139
|
+
const labelText = typeof option === 'string' ? option : option.label || option.value;
|
|
1140
|
+
|
|
1141
|
+
// If display-menu-with-uri and option has uri, create label + link icon
|
|
1142
|
+
if (hasUriSupport && typeof option === 'object' && option.uri) {
|
|
1143
|
+
// Create label span
|
|
1144
|
+
const labelSpan = document.createElement('span');
|
|
1145
|
+
labelSpan.className = 'tq-dropdown-item-label';
|
|
1146
|
+
labelSpan.textContent = labelText;
|
|
1147
|
+
labelSpan.style.flex = '1';
|
|
1148
|
+
labelSpan.style.cursor = 'pointer';
|
|
1149
|
+
|
|
1150
|
+
// Create link with truncated URI text
|
|
1151
|
+
const linkIcon = document.createElement('a');
|
|
1152
|
+
linkIcon.className = 'tq-dropdown-item-link';
|
|
1153
|
+
linkIcon.href = option.uri;
|
|
1154
|
+
linkIcon.target = '_blank';
|
|
1155
|
+
linkIcon.rel = 'noopener noreferrer';
|
|
1156
|
+
|
|
1157
|
+
// Truncate URI to 20 characters
|
|
1158
|
+
let displayUri = option.uri;
|
|
1159
|
+
if (displayUri.length > 20) {
|
|
1160
|
+
displayUri = displayUri.substring(0, 20) + '...';
|
|
1161
|
+
}
|
|
1162
|
+
linkIcon.textContent = displayUri;
|
|
1163
|
+
|
|
1164
|
+
linkIcon.style.marginLeft = '8px';
|
|
1165
|
+
linkIcon.style.fontSize = '12px';
|
|
1166
|
+
linkIcon.style.color = '#3b82f6';
|
|
1167
|
+
linkIcon.style.textDecoration = 'underline';
|
|
1168
|
+
linkIcon.style.opacity = '0.7';
|
|
1169
|
+
linkIcon.style.transition = 'opacity 0.2s';
|
|
1170
|
+
linkIcon.style.whiteSpace = 'nowrap';
|
|
1171
|
+
linkIcon.title = option.uri;
|
|
1172
|
+
linkIcon.setAttribute('aria-label', `Open ${labelText} documentation`);
|
|
1173
|
+
|
|
1174
|
+
// Hover effect for link icon
|
|
1175
|
+
linkIcon.addEventListener('mouseenter', () => {
|
|
1176
|
+
linkIcon.style.opacity = '1';
|
|
1177
|
+
});
|
|
1178
|
+
linkIcon.addEventListener('mouseleave', () => {
|
|
1179
|
+
linkIcon.style.opacity = '0.6';
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// Prevent link click from selecting the option
|
|
1183
|
+
linkIcon.addEventListener('click', (e) => {
|
|
1184
|
+
e.stopPropagation();
|
|
1185
|
+
// Link will open naturally via href
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// Set item to flex layout
|
|
1189
|
+
item.style.display = 'flex';
|
|
1190
|
+
item.style.alignItems = 'center';
|
|
1191
|
+
item.style.justifyContent = 'space-between';
|
|
1192
|
+
|
|
1193
|
+
item.appendChild(labelSpan);
|
|
1194
|
+
item.appendChild(linkIcon);
|
|
1195
|
+
|
|
1196
|
+
// Only label click should select the option
|
|
1197
|
+
labelSpan.addEventListener('click', (e) => {
|
|
1198
|
+
e.stopPropagation();
|
|
1199
|
+
this.handleDropdownSelect(option, matchData);
|
|
1200
|
+
this.hideDropdown();
|
|
1201
|
+
});
|
|
1202
|
+
} else {
|
|
1203
|
+
// Regular option without URI
|
|
1204
|
+
item.textContent = labelText;
|
|
1205
|
+
|
|
1206
|
+
// Click anywhere on item to select
|
|
1207
|
+
item.addEventListener('click', (e) => {
|
|
1208
|
+
e.stopPropagation();
|
|
1209
|
+
this.handleDropdownSelect(option, matchData);
|
|
1210
|
+
this.hideDropdown();
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1018
1213
|
|
|
1019
1214
|
// Highlight first item by default
|
|
1020
1215
|
if (index === 0) {
|
|
@@ -1026,11 +1221,6 @@ class DropdownManager {
|
|
|
1026
1221
|
this.options.styleManager.applyDropdownItemStyles(item);
|
|
1027
1222
|
}
|
|
1028
1223
|
|
|
1029
|
-
item.addEventListener('click', (e) => {
|
|
1030
|
-
e.stopPropagation();
|
|
1031
|
-
this.handleDropdownSelect(option, matchData);
|
|
1032
|
-
this.hideDropdown();
|
|
1033
|
-
});
|
|
1034
1224
|
dropdown.appendChild(item);
|
|
1035
1225
|
});
|
|
1036
1226
|
}
|
|
@@ -1138,26 +1328,49 @@ class DropdownManager {
|
|
|
1138
1328
|
* @param {HTMLElement} matchEl - Match element
|
|
1139
1329
|
*/
|
|
1140
1330
|
positionDropdown(dropdown, matchEl) {
|
|
1141
|
-
const
|
|
1331
|
+
const matchRect = matchEl.getBoundingClientRect();
|
|
1332
|
+
|
|
1333
|
+
// Force a layout calculation by accessing offsetWidth
|
|
1334
|
+
// This ensures we get the correct dropdown dimensions after styles are applied
|
|
1335
|
+
dropdown.offsetWidth;
|
|
1336
|
+
|
|
1142
1337
|
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1143
|
-
const offset = this.options.dropdownOffset;
|
|
1144
1338
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1339
|
+
console.log('[DropdownManager] Positioning Debug:');
|
|
1340
|
+
console.log(' Trigger position (x, y):', matchRect.left, matchRect.top);
|
|
1341
|
+
console.log(' Dropdown width:', dropdownRect.width);
|
|
1342
|
+
console.log(' Viewport width:', window.innerWidth);
|
|
1343
|
+
|
|
1344
|
+
// Use EdgeDetectionHelper to calculate optimal position
|
|
1345
|
+
// Increased padding to account for body margins/transforms and ensure visible clearance
|
|
1346
|
+
const position = EdgeDetectionHelper.calculatePosition({
|
|
1347
|
+
matchRect,
|
|
1348
|
+
dropdownRect,
|
|
1349
|
+
offset: this.options.dropdownOffset,
|
|
1350
|
+
padding: 35
|
|
1351
|
+
});
|
|
1148
1352
|
|
|
1149
|
-
|
|
1150
|
-
if (top < window.scrollY) {
|
|
1151
|
-
top = rect.bottom + window.scrollY + offset;
|
|
1152
|
-
}
|
|
1353
|
+
console.log(' Calculated dropdown position (x, y):', position.left, position.top);
|
|
1153
1354
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
left = window.innerWidth - dropdownRect.width - 10;
|
|
1157
|
-
}
|
|
1355
|
+
dropdown.style.top = `${position.top}px`;
|
|
1356
|
+
dropdown.style.left = `${position.left}px`;
|
|
1158
1357
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1358
|
+
// Verify the position was actually applied
|
|
1359
|
+
const computedStyle = window.getComputedStyle(dropdown);
|
|
1360
|
+
const actualRect = dropdown.getBoundingClientRect();
|
|
1361
|
+
console.log(' Verified applied styles:');
|
|
1362
|
+
console.log(' styleTop:', dropdown.style.top);
|
|
1363
|
+
console.log(' styleLeft:', dropdown.style.left);
|
|
1364
|
+
console.log(' computedPosition:', computedStyle.position);
|
|
1365
|
+
console.log(' actualBoundingRect:', JSON.stringify({
|
|
1366
|
+
left: actualRect.left,
|
|
1367
|
+
top: actualRect.top,
|
|
1368
|
+
right: actualRect.right,
|
|
1369
|
+
bottom: actualRect.bottom,
|
|
1370
|
+
width: actualRect.width,
|
|
1371
|
+
height: actualRect.height
|
|
1372
|
+
}));
|
|
1373
|
+
console.log(' Dropdown right edge:', actualRect.left + actualRect.width, 'vs viewport width:', window.innerWidth);
|
|
1161
1374
|
}
|
|
1162
1375
|
|
|
1163
1376
|
/**
|
|
@@ -1946,8 +2159,12 @@ class StyleManager {
|
|
|
1946
2159
|
/**
|
|
1947
2160
|
* Apply dropdown (menu) styles
|
|
1948
2161
|
* @param {HTMLElement} dropdown - Dropdown element
|
|
2162
|
+
* @param {string} category - Optional category to adjust styles
|
|
1949
2163
|
*/
|
|
1950
|
-
applyDropdownStyles(dropdown) {
|
|
2164
|
+
applyDropdownStyles(dropdown, category = '') {
|
|
2165
|
+
// Use wider maxWidth for display-menu-with-uri to accommodate links
|
|
2166
|
+
const maxWidth = category === 'display-menu-with-uri' ? '450px' : '300px';
|
|
2167
|
+
|
|
1951
2168
|
Object.assign(dropdown.style, {
|
|
1952
2169
|
position: 'absolute',
|
|
1953
2170
|
background: '#ffffff',
|
|
@@ -1956,7 +2173,7 @@ class StyleManager {
|
|
|
1956
2173
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
1957
2174
|
zIndex: '10000',
|
|
1958
2175
|
minWidth: '150px',
|
|
1959
|
-
maxWidth:
|
|
2176
|
+
maxWidth: maxWidth,
|
|
1960
2177
|
overflow: 'hidden',
|
|
1961
2178
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
1962
2179
|
fontSize: '14px',
|
|
@@ -2215,6 +2432,8 @@ class CommandHandlerRegistry {
|
|
|
2215
2432
|
this.register('user-select-oneOf', new UserSelectHandler());
|
|
2216
2433
|
this.register('api-json-table', new ApiJsonTableHandler());
|
|
2217
2434
|
this.register('api-md-table', new ApiMdTableHandler());
|
|
2435
|
+
this.register('display-menu', new DisplayMenuHandler());
|
|
2436
|
+
this.register('display-menu-with-uri', new DisplayMenuWithUriHandler());
|
|
2218
2437
|
}
|
|
2219
2438
|
|
|
2220
2439
|
/**
|
|
@@ -2545,6 +2764,48 @@ class ApiMdTableHandler extends CommandHandler {
|
|
|
2545
2764
|
}
|
|
2546
2765
|
}
|
|
2547
2766
|
|
|
2767
|
+
/**
|
|
2768
|
+
* Handler for display-menu category
|
|
2769
|
+
* Shows dropdown menu with selectable options
|
|
2770
|
+
*/
|
|
2771
|
+
class DisplayMenuHandler extends CommandHandler {
|
|
2772
|
+
getStyles() {
|
|
2773
|
+
return {
|
|
2774
|
+
backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
|
|
2775
|
+
color: '#065f46', // Dark green text
|
|
2776
|
+
textDecoration: 'none',
|
|
2777
|
+
borderBottom: '2px solid #10b981', // Green underline
|
|
2778
|
+
cursor: 'pointer'
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
getBubbleContent(matchData) {
|
|
2783
|
+
// Display menu shows dropdown, not bubble
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/**
|
|
2789
|
+
* Handler for display-menu-with-uri category
|
|
2790
|
+
* Shows dropdown menu with selectable options and clickable URI links
|
|
2791
|
+
*/
|
|
2792
|
+
class DisplayMenuWithUriHandler extends CommandHandler {
|
|
2793
|
+
getStyles() {
|
|
2794
|
+
return {
|
|
2795
|
+
backgroundColor: 'rgba(16, 185, 129, 0.15)', // Green background
|
|
2796
|
+
color: '#065f46', // Dark green text
|
|
2797
|
+
textDecoration: 'none',
|
|
2798
|
+
borderBottom: '2px solid #10b981', // Green underline
|
|
2799
|
+
cursor: 'pointer'
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
getBubbleContent(matchData) {
|
|
2804
|
+
// Display menu with URI shows dropdown, not bubble
|
|
2805
|
+
return null;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2548
2809
|
// AutoGrow - Automatically grows textarea height based on content
|
|
2549
2810
|
|
|
2550
2811
|
class AutoGrow {
|
|
@@ -2741,6 +3002,247 @@ class ValidationStateManager {
|
|
|
2741
3002
|
}
|
|
2742
3003
|
}
|
|
2743
3004
|
|
|
3005
|
+
// MobileKeyboardHandler - Handles mobile virtual keyboard behavior
|
|
3006
|
+
// Detects keyboard appearance and adjusts layout to keep textarea visible
|
|
3007
|
+
|
|
3008
|
+
class MobileKeyboardHandler {
|
|
3009
|
+
/**
|
|
3010
|
+
* Create mobile keyboard handler
|
|
3011
|
+
* @param {Object} options - Configuration options
|
|
3012
|
+
*/
|
|
3013
|
+
constructor(options = {}) {
|
|
3014
|
+
this.options = {
|
|
3015
|
+
textarea: options.textarea || null,
|
|
3016
|
+
wrapper: options.wrapper || null,
|
|
3017
|
+
debug: options.debug || false,
|
|
3018
|
+
...options
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
this.isKeyboardVisible = false;
|
|
3022
|
+
this.lastViewportHeight = window.innerHeight;
|
|
3023
|
+
this.visualViewport = window.visualViewport;
|
|
3024
|
+
|
|
3025
|
+
if (this.options.debug) {
|
|
3026
|
+
console.log('[MobileKeyboardHandler] Initialized');
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* Initialize keyboard detection
|
|
3032
|
+
*/
|
|
3033
|
+
init() {
|
|
3034
|
+
if (!this.options.textarea) {
|
|
3035
|
+
console.warn('[MobileKeyboardHandler] No textarea provided');
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// Use Visual Viewport API if available (preferred method)
|
|
3040
|
+
if (this.visualViewport) {
|
|
3041
|
+
this.visualViewport.addEventListener('resize', this.handleViewportResize);
|
|
3042
|
+
this.visualViewport.addEventListener('scroll', this.handleViewportScroll);
|
|
3043
|
+
|
|
3044
|
+
if (this.options.debug) {
|
|
3045
|
+
console.log('[MobileKeyboardHandler] Using Visual Viewport API');
|
|
3046
|
+
}
|
|
3047
|
+
} else {
|
|
3048
|
+
// Fallback to window resize
|
|
3049
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
3050
|
+
|
|
3051
|
+
if (this.options.debug) {
|
|
3052
|
+
console.log('[MobileKeyboardHandler] Using window resize fallback');
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// Handle focus events
|
|
3057
|
+
this.options.textarea.addEventListener('focus', this.handleFocus);
|
|
3058
|
+
this.options.textarea.addEventListener('blur', this.handleBlur);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
/**
|
|
3062
|
+
* Handle Visual Viewport resize (keyboard appearance/disappearance)
|
|
3063
|
+
*/
|
|
3064
|
+
handleViewportResize = () => {
|
|
3065
|
+
if (!this.visualViewport) return;
|
|
3066
|
+
|
|
3067
|
+
const viewportHeight = this.visualViewport.height;
|
|
3068
|
+
const windowHeight = window.innerHeight;
|
|
3069
|
+
|
|
3070
|
+
if (this.options.debug) {
|
|
3071
|
+
console.log('[MobileKeyboardHandler] Viewport resize:', {
|
|
3072
|
+
viewportHeight,
|
|
3073
|
+
windowHeight,
|
|
3074
|
+
scale: this.visualViewport.scale
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// Keyboard is visible if viewport height is significantly smaller than window height
|
|
3079
|
+
const wasKeyboardVisible = this.isKeyboardVisible;
|
|
3080
|
+
this.isKeyboardVisible = viewportHeight < windowHeight * 0.75;
|
|
3081
|
+
|
|
3082
|
+
if (this.isKeyboardVisible !== wasKeyboardVisible) {
|
|
3083
|
+
if (this.isKeyboardVisible) {
|
|
3084
|
+
this.onKeyboardShow();
|
|
3085
|
+
} else {
|
|
3086
|
+
this.onKeyboardHide();
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// Always adjust layout when viewport changes
|
|
3091
|
+
if (this.isKeyboardVisible) {
|
|
3092
|
+
this.adjustLayout();
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* Handle Visual Viewport scroll
|
|
3098
|
+
*/
|
|
3099
|
+
handleViewportScroll = () => {
|
|
3100
|
+
if (this.isKeyboardVisible) {
|
|
3101
|
+
// Ensure textarea stays in view during scroll
|
|
3102
|
+
this.ensureTextareaVisible();
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Handle window resize (fallback)
|
|
3108
|
+
*/
|
|
3109
|
+
handleWindowResize = () => {
|
|
3110
|
+
const currentHeight = window.innerHeight;
|
|
3111
|
+
const heightDifference = this.lastViewportHeight - currentHeight;
|
|
3112
|
+
|
|
3113
|
+
if (this.options.debug) {
|
|
3114
|
+
console.log('[MobileKeyboardHandler] Window resize:', {
|
|
3115
|
+
lastHeight: this.lastViewportHeight,
|
|
3116
|
+
currentHeight,
|
|
3117
|
+
difference: heightDifference
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// Significant decrease in height suggests keyboard appeared
|
|
3122
|
+
if (heightDifference > 150) {
|
|
3123
|
+
if (!this.isKeyboardVisible) {
|
|
3124
|
+
this.isKeyboardVisible = true;
|
|
3125
|
+
this.onKeyboardShow();
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
// Significant increase suggests keyboard hidden
|
|
3129
|
+
else if (heightDifference < -150) {
|
|
3130
|
+
if (this.isKeyboardVisible) {
|
|
3131
|
+
this.isKeyboardVisible = false;
|
|
3132
|
+
this.onKeyboardHide();
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
this.lastViewportHeight = currentHeight;
|
|
3137
|
+
};
|
|
3138
|
+
|
|
3139
|
+
/**
|
|
3140
|
+
* Handle textarea focus
|
|
3141
|
+
*/
|
|
3142
|
+
handleFocus = () => {
|
|
3143
|
+
if (this.options.debug) {
|
|
3144
|
+
console.log('[MobileKeyboardHandler] Textarea focused');
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// Delay to allow keyboard to appear
|
|
3148
|
+
setTimeout(() => {
|
|
3149
|
+
this.ensureTextareaVisible();
|
|
3150
|
+
}, 300);
|
|
3151
|
+
};
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Handle textarea blur
|
|
3155
|
+
*/
|
|
3156
|
+
handleBlur = () => {
|
|
3157
|
+
if (this.options.debug) {
|
|
3158
|
+
console.log('[MobileKeyboardHandler] Textarea blurred');
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
/**
|
|
3163
|
+
* Called when keyboard appears
|
|
3164
|
+
*/
|
|
3165
|
+
onKeyboardShow() {
|
|
3166
|
+
if (this.options.debug) {
|
|
3167
|
+
console.log('[MobileKeyboardHandler] Keyboard shown');
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
this.adjustLayout();
|
|
3171
|
+
this.ensureTextareaVisible();
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Called when keyboard hides
|
|
3176
|
+
*/
|
|
3177
|
+
onKeyboardHide() {
|
|
3178
|
+
if (this.options.debug) {
|
|
3179
|
+
console.log('[MobileKeyboardHandler] Keyboard hidden');
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Reset wrapper height to auto
|
|
3183
|
+
if (this.options.wrapper) {
|
|
3184
|
+
this.options.wrapper.style.maxHeight = '';
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* Adjust layout to accommodate keyboard
|
|
3190
|
+
*/
|
|
3191
|
+
adjustLayout() {
|
|
3192
|
+
if (!this.visualViewport || !this.options.wrapper) return;
|
|
3193
|
+
|
|
3194
|
+
const viewportHeight = this.visualViewport.height;
|
|
3195
|
+
|
|
3196
|
+
// Set wrapper max-height to visible viewport height minus some padding
|
|
3197
|
+
const maxHeight = viewportHeight - 20; // 20px padding
|
|
3198
|
+
this.options.wrapper.style.maxHeight = `${maxHeight}px`;
|
|
3199
|
+
this.options.wrapper.style.overflow = 'auto';
|
|
3200
|
+
|
|
3201
|
+
if (this.options.debug) {
|
|
3202
|
+
console.log('[MobileKeyboardHandler] Adjusted wrapper height:', maxHeight);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
/**
|
|
3207
|
+
* Ensure textarea is visible above the keyboard
|
|
3208
|
+
*/
|
|
3209
|
+
ensureTextareaVisible() {
|
|
3210
|
+
if (!this.options.textarea) return;
|
|
3211
|
+
|
|
3212
|
+
// Scroll textarea into view
|
|
3213
|
+
this.options.textarea.scrollIntoView({
|
|
3214
|
+
behavior: 'smooth',
|
|
3215
|
+
block: 'center',
|
|
3216
|
+
inline: 'nearest'
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
if (this.options.debug) {
|
|
3220
|
+
console.log('[MobileKeyboardHandler] Scrolled textarea into view');
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
/**
|
|
3225
|
+
* Cleanup event listeners
|
|
3226
|
+
*/
|
|
3227
|
+
destroy() {
|
|
3228
|
+
if (this.visualViewport) {
|
|
3229
|
+
this.visualViewport.removeEventListener('resize', this.handleViewportResize);
|
|
3230
|
+
this.visualViewport.removeEventListener('scroll', this.handleViewportScroll);
|
|
3231
|
+
} else {
|
|
3232
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
if (this.options.textarea) {
|
|
3236
|
+
this.options.textarea.removeEventListener('focus', this.handleFocus);
|
|
3237
|
+
this.options.textarea.removeEventListener('blur', this.handleBlur);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (this.options.debug) {
|
|
3241
|
+
console.log('[MobileKeyboardHandler] Destroyed');
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
|
|
2744
3246
|
// TrustQuery - Lightweight library to make textareas interactive
|
|
2745
3247
|
// Turns matching words into interactive elements with hover bubbles and click actions
|
|
2746
3248
|
|
|
@@ -2948,6 +3450,17 @@ class TrustQuery {
|
|
|
2948
3450
|
console.log('[TrustQuery] AutoGrow feature enabled');
|
|
2949
3451
|
}
|
|
2950
3452
|
|
|
3453
|
+
// Mobile keyboard handler (enabled by default, can be disabled via options)
|
|
3454
|
+
if (this.options.mobileKeyboard !== false) {
|
|
3455
|
+
this.features.mobileKeyboard = new MobileKeyboardHandler({
|
|
3456
|
+
textarea: this.textarea,
|
|
3457
|
+
wrapper: this.wrapper,
|
|
3458
|
+
debug: this.options.debug
|
|
3459
|
+
});
|
|
3460
|
+
this.features.mobileKeyboard.init();
|
|
3461
|
+
console.log('[TrustQuery] Mobile keyboard handler enabled');
|
|
3462
|
+
}
|
|
3463
|
+
|
|
2951
3464
|
// Debug logging feature
|
|
2952
3465
|
if (this.options.debug) {
|
|
2953
3466
|
this.enableDebugLogging();
|
|
@@ -3238,6 +3751,16 @@ class TrustQuery {
|
|
|
3238
3751
|
this.interactionHandler.destroy();
|
|
3239
3752
|
}
|
|
3240
3753
|
|
|
3754
|
+
// Cleanup mobile keyboard handler
|
|
3755
|
+
if (this.features.mobileKeyboard) {
|
|
3756
|
+
this.features.mobileKeyboard.destroy();
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// Cleanup auto-grow
|
|
3760
|
+
if (this.features.autoGrow) {
|
|
3761
|
+
this.features.autoGrow.destroy();
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3241
3764
|
// Unwrap textarea
|
|
3242
3765
|
const parent = this.wrapper.parentNode;
|
|
3243
3766
|
parent.insertBefore(this.textarea, this.wrapper);
|