@trustquery/browser 0.2.9 → 0.3.0
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 +270 -2
- package/dist/trustquery.js.map +1 -1
- package/package.json +2 -2
- package/src/DropdownManager.js +8 -2
- package/src/MobileKeyboardHandler.js +240 -0
- package/src/TrustQuery.js +22 -0
package/dist/trustquery.js
CHANGED
|
@@ -981,8 +981,9 @@ class DropdownManager {
|
|
|
981
981
|
// Setup keyboard navigation
|
|
982
982
|
this.setupKeyboardHandlers();
|
|
983
983
|
|
|
984
|
-
// Close on click outside
|
|
984
|
+
// Close on click outside (using mousedown to match icon behavior)
|
|
985
985
|
setTimeout(() => {
|
|
986
|
+
document.addEventListener('mousedown', this.closeDropdownHandler);
|
|
986
987
|
document.addEventListener('click', this.closeDropdownHandler);
|
|
987
988
|
}, 0);
|
|
988
989
|
|
|
@@ -1307,6 +1308,7 @@ class DropdownManager {
|
|
|
1307
1308
|
this.dropdownOptions = null;
|
|
1308
1309
|
this.dropdownMatchData = null;
|
|
1309
1310
|
this.selectedDropdownIndex = 0;
|
|
1311
|
+
document.removeEventListener('mousedown', this.closeDropdownHandler);
|
|
1310
1312
|
document.removeEventListener('click', this.closeDropdownHandler);
|
|
1311
1313
|
document.removeEventListener('keydown', this.keyboardHandler);
|
|
1312
1314
|
}
|
|
@@ -1316,8 +1318,12 @@ class DropdownManager {
|
|
|
1316
1318
|
* Close dropdown handler (bound to document)
|
|
1317
1319
|
*/
|
|
1318
1320
|
closeDropdownHandler = (e) => {
|
|
1319
|
-
// Only close if clicking outside the dropdown
|
|
1321
|
+
// Only close if clicking outside the dropdown AND not on the trigger element
|
|
1320
1322
|
if (this.activeDropdown && !this.activeDropdown.contains(e.target)) {
|
|
1323
|
+
// Check if clicking on the trigger element itself - don't close in that case
|
|
1324
|
+
if (this.activeDropdownMatch && (this.activeDropdownMatch === e.target || this.activeDropdownMatch.contains(e.target))) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1321
1327
|
this.hideDropdown();
|
|
1322
1328
|
}
|
|
1323
1329
|
}
|
|
@@ -3002,6 +3008,247 @@ class ValidationStateManager {
|
|
|
3002
3008
|
}
|
|
3003
3009
|
}
|
|
3004
3010
|
|
|
3011
|
+
// MobileKeyboardHandler - Handles mobile virtual keyboard behavior
|
|
3012
|
+
// Detects keyboard appearance and adjusts layout to keep textarea visible
|
|
3013
|
+
|
|
3014
|
+
class MobileKeyboardHandler {
|
|
3015
|
+
/**
|
|
3016
|
+
* Create mobile keyboard handler
|
|
3017
|
+
* @param {Object} options - Configuration options
|
|
3018
|
+
*/
|
|
3019
|
+
constructor(options = {}) {
|
|
3020
|
+
this.options = {
|
|
3021
|
+
textarea: options.textarea || null,
|
|
3022
|
+
wrapper: options.wrapper || null,
|
|
3023
|
+
debug: options.debug || false,
|
|
3024
|
+
...options
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
this.isKeyboardVisible = false;
|
|
3028
|
+
this.lastViewportHeight = window.innerHeight;
|
|
3029
|
+
this.visualViewport = window.visualViewport;
|
|
3030
|
+
|
|
3031
|
+
if (this.options.debug) {
|
|
3032
|
+
console.log('[MobileKeyboardHandler] Initialized');
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
/**
|
|
3037
|
+
* Initialize keyboard detection
|
|
3038
|
+
*/
|
|
3039
|
+
init() {
|
|
3040
|
+
if (!this.options.textarea) {
|
|
3041
|
+
console.warn('[MobileKeyboardHandler] No textarea provided');
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// Use Visual Viewport API if available (preferred method)
|
|
3046
|
+
if (this.visualViewport) {
|
|
3047
|
+
this.visualViewport.addEventListener('resize', this.handleViewportResize);
|
|
3048
|
+
this.visualViewport.addEventListener('scroll', this.handleViewportScroll);
|
|
3049
|
+
|
|
3050
|
+
if (this.options.debug) {
|
|
3051
|
+
console.log('[MobileKeyboardHandler] Using Visual Viewport API');
|
|
3052
|
+
}
|
|
3053
|
+
} else {
|
|
3054
|
+
// Fallback to window resize
|
|
3055
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
3056
|
+
|
|
3057
|
+
if (this.options.debug) {
|
|
3058
|
+
console.log('[MobileKeyboardHandler] Using window resize fallback');
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// Handle focus events
|
|
3063
|
+
this.options.textarea.addEventListener('focus', this.handleFocus);
|
|
3064
|
+
this.options.textarea.addEventListener('blur', this.handleBlur);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/**
|
|
3068
|
+
* Handle Visual Viewport resize (keyboard appearance/disappearance)
|
|
3069
|
+
*/
|
|
3070
|
+
handleViewportResize = () => {
|
|
3071
|
+
if (!this.visualViewport) return;
|
|
3072
|
+
|
|
3073
|
+
const viewportHeight = this.visualViewport.height;
|
|
3074
|
+
const windowHeight = window.innerHeight;
|
|
3075
|
+
|
|
3076
|
+
if (this.options.debug) {
|
|
3077
|
+
console.log('[MobileKeyboardHandler] Viewport resize:', {
|
|
3078
|
+
viewportHeight,
|
|
3079
|
+
windowHeight,
|
|
3080
|
+
scale: this.visualViewport.scale
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// Keyboard is visible if viewport height is significantly smaller than window height
|
|
3085
|
+
const wasKeyboardVisible = this.isKeyboardVisible;
|
|
3086
|
+
this.isKeyboardVisible = viewportHeight < windowHeight * 0.75;
|
|
3087
|
+
|
|
3088
|
+
if (this.isKeyboardVisible !== wasKeyboardVisible) {
|
|
3089
|
+
if (this.isKeyboardVisible) {
|
|
3090
|
+
this.onKeyboardShow();
|
|
3091
|
+
} else {
|
|
3092
|
+
this.onKeyboardHide();
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// Always adjust layout when viewport changes
|
|
3097
|
+
if (this.isKeyboardVisible) {
|
|
3098
|
+
this.adjustLayout();
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
/**
|
|
3103
|
+
* Handle Visual Viewport scroll
|
|
3104
|
+
*/
|
|
3105
|
+
handleViewportScroll = () => {
|
|
3106
|
+
if (this.isKeyboardVisible) {
|
|
3107
|
+
// Ensure textarea stays in view during scroll
|
|
3108
|
+
this.ensureTextareaVisible();
|
|
3109
|
+
}
|
|
3110
|
+
};
|
|
3111
|
+
|
|
3112
|
+
/**
|
|
3113
|
+
* Handle window resize (fallback)
|
|
3114
|
+
*/
|
|
3115
|
+
handleWindowResize = () => {
|
|
3116
|
+
const currentHeight = window.innerHeight;
|
|
3117
|
+
const heightDifference = this.lastViewportHeight - currentHeight;
|
|
3118
|
+
|
|
3119
|
+
if (this.options.debug) {
|
|
3120
|
+
console.log('[MobileKeyboardHandler] Window resize:', {
|
|
3121
|
+
lastHeight: this.lastViewportHeight,
|
|
3122
|
+
currentHeight,
|
|
3123
|
+
difference: heightDifference
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Significant decrease in height suggests keyboard appeared
|
|
3128
|
+
if (heightDifference > 150) {
|
|
3129
|
+
if (!this.isKeyboardVisible) {
|
|
3130
|
+
this.isKeyboardVisible = true;
|
|
3131
|
+
this.onKeyboardShow();
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
// Significant increase suggests keyboard hidden
|
|
3135
|
+
else if (heightDifference < -150) {
|
|
3136
|
+
if (this.isKeyboardVisible) {
|
|
3137
|
+
this.isKeyboardVisible = false;
|
|
3138
|
+
this.onKeyboardHide();
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
this.lastViewportHeight = currentHeight;
|
|
3143
|
+
};
|
|
3144
|
+
|
|
3145
|
+
/**
|
|
3146
|
+
* Handle textarea focus
|
|
3147
|
+
*/
|
|
3148
|
+
handleFocus = () => {
|
|
3149
|
+
if (this.options.debug) {
|
|
3150
|
+
console.log('[MobileKeyboardHandler] Textarea focused');
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// Delay to allow keyboard to appear
|
|
3154
|
+
setTimeout(() => {
|
|
3155
|
+
this.ensureTextareaVisible();
|
|
3156
|
+
}, 300);
|
|
3157
|
+
};
|
|
3158
|
+
|
|
3159
|
+
/**
|
|
3160
|
+
* Handle textarea blur
|
|
3161
|
+
*/
|
|
3162
|
+
handleBlur = () => {
|
|
3163
|
+
if (this.options.debug) {
|
|
3164
|
+
console.log('[MobileKeyboardHandler] Textarea blurred');
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* Called when keyboard appears
|
|
3170
|
+
*/
|
|
3171
|
+
onKeyboardShow() {
|
|
3172
|
+
if (this.options.debug) {
|
|
3173
|
+
console.log('[MobileKeyboardHandler] Keyboard shown');
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
this.adjustLayout();
|
|
3177
|
+
this.ensureTextareaVisible();
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
/**
|
|
3181
|
+
* Called when keyboard hides
|
|
3182
|
+
*/
|
|
3183
|
+
onKeyboardHide() {
|
|
3184
|
+
if (this.options.debug) {
|
|
3185
|
+
console.log('[MobileKeyboardHandler] Keyboard hidden');
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Reset wrapper height to auto
|
|
3189
|
+
if (this.options.wrapper) {
|
|
3190
|
+
this.options.wrapper.style.maxHeight = '';
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
/**
|
|
3195
|
+
* Adjust layout to accommodate keyboard
|
|
3196
|
+
*/
|
|
3197
|
+
adjustLayout() {
|
|
3198
|
+
if (!this.visualViewport || !this.options.wrapper) return;
|
|
3199
|
+
|
|
3200
|
+
const viewportHeight = this.visualViewport.height;
|
|
3201
|
+
|
|
3202
|
+
// Set wrapper max-height to visible viewport height minus some padding
|
|
3203
|
+
const maxHeight = viewportHeight - 20; // 20px padding
|
|
3204
|
+
this.options.wrapper.style.maxHeight = `${maxHeight}px`;
|
|
3205
|
+
this.options.wrapper.style.overflow = 'auto';
|
|
3206
|
+
|
|
3207
|
+
if (this.options.debug) {
|
|
3208
|
+
console.log('[MobileKeyboardHandler] Adjusted wrapper height:', maxHeight);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
/**
|
|
3213
|
+
* Ensure textarea is visible above the keyboard
|
|
3214
|
+
*/
|
|
3215
|
+
ensureTextareaVisible() {
|
|
3216
|
+
if (!this.options.textarea) return;
|
|
3217
|
+
|
|
3218
|
+
// Scroll textarea into view
|
|
3219
|
+
this.options.textarea.scrollIntoView({
|
|
3220
|
+
behavior: 'smooth',
|
|
3221
|
+
block: 'center',
|
|
3222
|
+
inline: 'nearest'
|
|
3223
|
+
});
|
|
3224
|
+
|
|
3225
|
+
if (this.options.debug) {
|
|
3226
|
+
console.log('[MobileKeyboardHandler] Scrolled textarea into view');
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
/**
|
|
3231
|
+
* Cleanup event listeners
|
|
3232
|
+
*/
|
|
3233
|
+
destroy() {
|
|
3234
|
+
if (this.visualViewport) {
|
|
3235
|
+
this.visualViewport.removeEventListener('resize', this.handleViewportResize);
|
|
3236
|
+
this.visualViewport.removeEventListener('scroll', this.handleViewportScroll);
|
|
3237
|
+
} else {
|
|
3238
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (this.options.textarea) {
|
|
3242
|
+
this.options.textarea.removeEventListener('focus', this.handleFocus);
|
|
3243
|
+
this.options.textarea.removeEventListener('blur', this.handleBlur);
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
if (this.options.debug) {
|
|
3247
|
+
console.log('[MobileKeyboardHandler] Destroyed');
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3005
3252
|
// TrustQuery - Lightweight library to make textareas interactive
|
|
3006
3253
|
// Turns matching words into interactive elements with hover bubbles and click actions
|
|
3007
3254
|
|
|
@@ -3209,6 +3456,17 @@ class TrustQuery {
|
|
|
3209
3456
|
console.log('[TrustQuery] AutoGrow feature enabled');
|
|
3210
3457
|
}
|
|
3211
3458
|
|
|
3459
|
+
// Mobile keyboard handler (enabled by default, can be disabled via options)
|
|
3460
|
+
if (this.options.mobileKeyboard !== false) {
|
|
3461
|
+
this.features.mobileKeyboard = new MobileKeyboardHandler({
|
|
3462
|
+
textarea: this.textarea,
|
|
3463
|
+
wrapper: this.wrapper,
|
|
3464
|
+
debug: this.options.debug
|
|
3465
|
+
});
|
|
3466
|
+
this.features.mobileKeyboard.init();
|
|
3467
|
+
console.log('[TrustQuery] Mobile keyboard handler enabled');
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3212
3470
|
// Debug logging feature
|
|
3213
3471
|
if (this.options.debug) {
|
|
3214
3472
|
this.enableDebugLogging();
|
|
@@ -3499,6 +3757,16 @@ class TrustQuery {
|
|
|
3499
3757
|
this.interactionHandler.destroy();
|
|
3500
3758
|
}
|
|
3501
3759
|
|
|
3760
|
+
// Cleanup mobile keyboard handler
|
|
3761
|
+
if (this.features.mobileKeyboard) {
|
|
3762
|
+
this.features.mobileKeyboard.destroy();
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// Cleanup auto-grow
|
|
3766
|
+
if (this.features.autoGrow) {
|
|
3767
|
+
this.features.autoGrow.destroy();
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3502
3770
|
// Unwrap textarea
|
|
3503
3771
|
const parent = this.wrapper.parentNode;
|
|
3504
3772
|
parent.insertBefore(this.textarea, this.wrapper);
|