@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.
@@ -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);