@flywheel-io/vision 19.2.0 → 19.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.
|
@@ -6253,7 +6253,7 @@ class FwSelectMenuComponent {
|
|
|
6253
6253
|
}
|
|
6254
6254
|
}
|
|
6255
6255
|
get disabledClass() {
|
|
6256
|
-
return this.disabled;
|
|
6256
|
+
return this.disabled();
|
|
6257
6257
|
}
|
|
6258
6258
|
get value() {
|
|
6259
6259
|
return this._value;
|
|
@@ -6261,36 +6261,59 @@ class FwSelectMenuComponent {
|
|
|
6261
6261
|
set value(newValue) {
|
|
6262
6262
|
this.updateValue(newValue);
|
|
6263
6263
|
}
|
|
6264
|
-
constructor(
|
|
6265
|
-
this._changeDetectorRef = _changeDetectorRef;
|
|
6264
|
+
constructor(ngControl) {
|
|
6266
6265
|
this.ngControl = ngControl;
|
|
6267
|
-
|
|
6268
|
-
this.
|
|
6269
|
-
this.
|
|
6270
|
-
this.
|
|
6271
|
-
this.
|
|
6272
|
-
this.
|
|
6273
|
-
this.
|
|
6274
|
-
this.
|
|
6275
|
-
this.
|
|
6276
|
-
this.
|
|
6277
|
-
this.
|
|
6278
|
-
this.
|
|
6279
|
-
this.
|
|
6280
|
-
this.
|
|
6281
|
-
this.
|
|
6266
|
+
this.options = input([]);
|
|
6267
|
+
this.valueProperty = input('value');
|
|
6268
|
+
this.useFullOptionAsValue = input(false);
|
|
6269
|
+
this.titleProperty = input('title');
|
|
6270
|
+
this.iconProperty = input('icon');
|
|
6271
|
+
this.staticIcon = input(undefined);
|
|
6272
|
+
this.descriptionProperty = input('description');
|
|
6273
|
+
this.showFilter = input(false);
|
|
6274
|
+
this.showReset = input(false);
|
|
6275
|
+
this.disabled = input(false);
|
|
6276
|
+
this.errored = input(false);
|
|
6277
|
+
this.width = input('200px');
|
|
6278
|
+
this.optionsWidth = input(undefined);
|
|
6279
|
+
this.minOptionsHeight = input(undefined);
|
|
6280
|
+
this.maxOptionsHeight = input(undefined);
|
|
6281
|
+
this.size = input('medium');
|
|
6282
|
+
this.placeholder = input('Select something...');
|
|
6282
6283
|
// eslint-disable-next-line @angular-eslint/no-output-native
|
|
6283
6284
|
this.change = new EventEmitter();
|
|
6284
6285
|
this.filterChanged = new EventEmitter();
|
|
6285
6286
|
this.selectValue = '';
|
|
6286
|
-
this.selectTitle = '';
|
|
6287
|
+
this.selectTitle = signal('');
|
|
6287
6288
|
this.selectIcon = '';
|
|
6288
|
-
this.filterValue = '';
|
|
6289
|
+
this.filterValue = signal('');
|
|
6289
6290
|
this.subscriptions = [];
|
|
6290
6291
|
this._isOpen = false;
|
|
6291
6292
|
this.focused = 0;
|
|
6292
6293
|
this.inFocusOpen = false;
|
|
6294
|
+
this.isTyping = signal(false);
|
|
6295
|
+
// Computed signal for the input display value
|
|
6296
|
+
this.inputDisplayValue = computed(() => this.isTyping() ? this.filterValue() : this.selectTitle());
|
|
6293
6297
|
this._value = '';
|
|
6298
|
+
this.filteredOptions = computed(() => {
|
|
6299
|
+
const filter = this.filterValue();
|
|
6300
|
+
const opts = this.options();
|
|
6301
|
+
const tProp = this.titleProperty();
|
|
6302
|
+
if (!filter || filter.trim() === '') {
|
|
6303
|
+
return opts;
|
|
6304
|
+
}
|
|
6305
|
+
return opts.filter(opt => opt[tProp]?.toString().toLowerCase()
|
|
6306
|
+
.includes(filter.toLowerCase()));
|
|
6307
|
+
});
|
|
6308
|
+
this.optionsWithValues = computed(() => {
|
|
6309
|
+
const useFull = this.useFullOptionAsValue();
|
|
6310
|
+
const valProp = this.valueProperty();
|
|
6311
|
+
return this.filteredOptions().map(item => ({
|
|
6312
|
+
raw: item,
|
|
6313
|
+
trackingId: useFull ? JSON.stringify(item) : item?.[valProp]?.toString(),
|
|
6314
|
+
value: useFull ? JSON.stringify(item) : item?.[valProp]?.toString(),
|
|
6315
|
+
}));
|
|
6316
|
+
});
|
|
6294
6317
|
this.onTouched = () => {
|
|
6295
6318
|
};
|
|
6296
6319
|
// this is just a different way of binding the controlValueAccessor
|
|
@@ -6298,31 +6321,29 @@ class FwSelectMenuComponent {
|
|
|
6298
6321
|
if (this.ngControl) {
|
|
6299
6322
|
this.ngControl.valueAccessor = this;
|
|
6300
6323
|
}
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6324
|
+
// Watch for options changes to update the displayed title
|
|
6325
|
+
effect(() => {
|
|
6326
|
+
const currentOptions = this.options();
|
|
6327
|
+
const vProp = this.valueProperty();
|
|
6328
|
+
const tProp = this.titleProperty();
|
|
6329
|
+
if (!currentOptions || currentOptions.length === 0) {
|
|
6330
|
+
return;
|
|
6331
|
+
}
|
|
6332
|
+
const selectedOption = currentOptions.find(item => item[vProp]?.toString() === this.selectValue);
|
|
6307
6333
|
if (selectedOption) {
|
|
6308
|
-
this.selectTitle
|
|
6334
|
+
this.selectTitle.set(selectedOption[tProp]);
|
|
6309
6335
|
}
|
|
6310
|
-
}
|
|
6336
|
+
});
|
|
6311
6337
|
}
|
|
6312
6338
|
ngAfterContentInit() {
|
|
6313
|
-
|
|
6314
|
-
|
|
6339
|
+
// When using content projection with <fw-menu-item> components,
|
|
6340
|
+
// subscribe to their click events and set initial selected state
|
|
6341
|
+
if (this.menuItems && this.menuItems.length > 0) {
|
|
6315
6342
|
this.menuItems.forEach(item => {
|
|
6316
|
-
this.options.push({
|
|
6317
|
-
value: item.value.toString(),
|
|
6318
|
-
title: item.title.toString(),
|
|
6319
|
-
icon: item.icon,
|
|
6320
|
-
description: item.description,
|
|
6321
|
-
});
|
|
6322
6343
|
const sub = item.click.subscribe(value => this.menu.writeValue(value));
|
|
6323
6344
|
this.subscriptions.push(sub);
|
|
6324
6345
|
if (item.value.toString() === this.selectValue) {
|
|
6325
|
-
this.selectTitle
|
|
6346
|
+
this.selectTitle.set(item.title.toString());
|
|
6326
6347
|
this.selectIcon = item.icon;
|
|
6327
6348
|
}
|
|
6328
6349
|
});
|
|
@@ -6351,11 +6372,6 @@ class FwSelectMenuComponent {
|
|
|
6351
6372
|
registerOnTouched(fn) {
|
|
6352
6373
|
this.onTouched = fn;
|
|
6353
6374
|
}
|
|
6354
|
-
setDisabledState(isDisabled) {
|
|
6355
|
-
this.disabled = isDisabled;
|
|
6356
|
-
// eslint-disable-next-line @rx-angular/no-explicit-change-detection-apis
|
|
6357
|
-
this._changeDetectorRef.markForCheck();
|
|
6358
|
-
}
|
|
6359
6375
|
writeValue(value) {
|
|
6360
6376
|
value = value ?? '';
|
|
6361
6377
|
if (value === this.value) {
|
|
@@ -6369,20 +6385,80 @@ class FwSelectMenuComponent {
|
|
|
6369
6385
|
this.close();
|
|
6370
6386
|
return;
|
|
6371
6387
|
}
|
|
6372
|
-
if (this.useFullOptionAsValue) {
|
|
6388
|
+
if (this.useFullOptionAsValue()) {
|
|
6373
6389
|
const parsedValue = JSON.parse(e);
|
|
6374
6390
|
this.updateValue(parsedValue);
|
|
6375
6391
|
}
|
|
6376
6392
|
else {
|
|
6377
6393
|
this.updateValue(e);
|
|
6378
6394
|
}
|
|
6395
|
+
this.isTyping.set(false);
|
|
6379
6396
|
this.close();
|
|
6397
|
+
this.inFocusOpen = false;
|
|
6398
|
+
}
|
|
6399
|
+
/**
|
|
6400
|
+
* Get all available items for navigation, either from options or menuItems
|
|
6401
|
+
*/
|
|
6402
|
+
getAvailableItems() {
|
|
6403
|
+
// If using options input, return filtered options
|
|
6404
|
+
if (this.options().length > 0) {
|
|
6405
|
+
return this.filteredOptions();
|
|
6406
|
+
}
|
|
6407
|
+
// If using content projection, return non-disabled menu items
|
|
6408
|
+
if (this.menuItems && this.menuItems.length > 0) {
|
|
6409
|
+
return this.menuItems.filter(item => !item.disabled);
|
|
6410
|
+
}
|
|
6411
|
+
return [];
|
|
6412
|
+
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Update highlighting for the currently selected item
|
|
6415
|
+
*/
|
|
6416
|
+
updateHighlighting() {
|
|
6417
|
+
const currentValue = this.selectValue;
|
|
6418
|
+
// Update highlighting for content-projected menu items
|
|
6419
|
+
if (this.menuItems && this.menuItems.length > 0) {
|
|
6420
|
+
this.menuItems.forEach(item => {
|
|
6421
|
+
item.selected = item.value === currentValue;
|
|
6422
|
+
});
|
|
6423
|
+
}
|
|
6424
|
+
// Update highlighting for options-based menu items via menu component
|
|
6425
|
+
if (this.menu && this.options().length > 0) {
|
|
6426
|
+
this.menu.value = currentValue;
|
|
6427
|
+
this.menu.updateLayout();
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
/**
|
|
6431
|
+
* Initialize focused index to the currently selected item
|
|
6432
|
+
*/
|
|
6433
|
+
initializeFocusedIndex() {
|
|
6434
|
+
const availableItems = this.getAvailableItems();
|
|
6435
|
+
const currentValue = this.selectValue;
|
|
6436
|
+
// Find the index of the currently selected item
|
|
6437
|
+
const selectedIndex = availableItems.findIndex(item => {
|
|
6438
|
+
if (item instanceof FwMenuItemComponent) {
|
|
6439
|
+
return item.value === currentValue;
|
|
6440
|
+
}
|
|
6441
|
+
else {
|
|
6442
|
+
// For options array items
|
|
6443
|
+
const vProp = this.valueProperty();
|
|
6444
|
+
const itemValue = this.useFullOptionAsValue()
|
|
6445
|
+
? JSON.stringify(item)
|
|
6446
|
+
: item?.[vProp]?.toString();
|
|
6447
|
+
return itemValue === currentValue;
|
|
6448
|
+
}
|
|
6449
|
+
});
|
|
6450
|
+
// Set focused to the selected index
|
|
6451
|
+
// If no item is selected (selectedIndex is -1), keep it at -1 so first arrow down goes to index 0
|
|
6452
|
+
this.focused = selectedIndex;
|
|
6453
|
+
// Also update the highlighting to show the current selection (if any)
|
|
6454
|
+
this.updateHighlighting();
|
|
6380
6455
|
}
|
|
6381
6456
|
moveFocused(direction) {
|
|
6457
|
+
const availableItems = this.getAvailableItems();
|
|
6382
6458
|
switch (direction) {
|
|
6383
6459
|
case 'down': {
|
|
6384
6460
|
this.focused++;
|
|
6385
|
-
if (this.focused >=
|
|
6461
|
+
if (this.focused >= availableItems.length) {
|
|
6386
6462
|
this.focused = 0;
|
|
6387
6463
|
}
|
|
6388
6464
|
break;
|
|
@@ -6390,7 +6466,7 @@ class FwSelectMenuComponent {
|
|
|
6390
6466
|
case 'up': {
|
|
6391
6467
|
this.focused--;
|
|
6392
6468
|
if (this.focused < 0) {
|
|
6393
|
-
this.focused =
|
|
6469
|
+
this.focused = availableItems.length - 1;
|
|
6394
6470
|
}
|
|
6395
6471
|
break;
|
|
6396
6472
|
}
|
|
@@ -6400,32 +6476,205 @@ class FwSelectMenuComponent {
|
|
|
6400
6476
|
}
|
|
6401
6477
|
}
|
|
6402
6478
|
}
|
|
6479
|
+
handleFocus() {
|
|
6480
|
+
// Select all text when focusing the input
|
|
6481
|
+
if (this.textInput?.inputRef?.nativeElement && !this.isTyping()) {
|
|
6482
|
+
const input = this.textInput.inputRef.nativeElement;
|
|
6483
|
+
// Select all text immediately - this will select the current value
|
|
6484
|
+
input.select();
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
handleInputClick() {
|
|
6488
|
+
// When clicking into the input, select all text if not already typing
|
|
6489
|
+
if (this.textInput?.inputRef?.nativeElement && !this.isTyping()) {
|
|
6490
|
+
const input = this.textInput.inputRef.nativeElement;
|
|
6491
|
+
// Use setTimeout to override the click's cursor positioning
|
|
6492
|
+
setTimeout(() => {
|
|
6493
|
+
input.select();
|
|
6494
|
+
}, 0);
|
|
6495
|
+
}
|
|
6496
|
+
// Initialize highlighting when dropdown opens via click
|
|
6497
|
+
// Use setTimeout to ensure menu is rendered before we try to update it
|
|
6498
|
+
setTimeout(() => {
|
|
6499
|
+
if (this.trigger?.isOpen()) {
|
|
6500
|
+
this.inFocusOpen = true;
|
|
6501
|
+
this.preFocusValue = this.value;
|
|
6502
|
+
this.initializeFocusedIndex();
|
|
6503
|
+
}
|
|
6504
|
+
}, 0);
|
|
6505
|
+
}
|
|
6403
6506
|
handleKeyDown(event) {
|
|
6507
|
+
// Handle Enter key when typing
|
|
6508
|
+
if (event.key === 'Enter' && this.isTyping()) {
|
|
6509
|
+
event.preventDefault();
|
|
6510
|
+
this.inFocusOpen = false;
|
|
6511
|
+
// Select the currently focused option (respects arrow key navigation)
|
|
6512
|
+
const availableItems = this.getAvailableItems();
|
|
6513
|
+
let newValue;
|
|
6514
|
+
if (availableItems.length > 0) {
|
|
6515
|
+
// Select the item at the focused index (which may have been changed by arrow keys)
|
|
6516
|
+
const selectedItem = availableItems[this.focused];
|
|
6517
|
+
// Handle both options (objects) and menuItems (FwMenuItemComponent)
|
|
6518
|
+
if (selectedItem instanceof FwMenuItemComponent) {
|
|
6519
|
+
newValue = selectedItem.value;
|
|
6520
|
+
}
|
|
6521
|
+
else {
|
|
6522
|
+
newValue = selectedItem;
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
else {
|
|
6526
|
+
// No available items, keep current value
|
|
6527
|
+
if (this.useFullOptionAsValue()) {
|
|
6528
|
+
try {
|
|
6529
|
+
newValue = JSON.parse(this.value);
|
|
6530
|
+
}
|
|
6531
|
+
catch {
|
|
6532
|
+
// If parse fails, keep the raw value
|
|
6533
|
+
newValue = this.value;
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
else {
|
|
6537
|
+
newValue = this.value;
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
// Clear filter and exit typing mode FIRST
|
|
6541
|
+
// This prevents onInputChange from interfering
|
|
6542
|
+
this.filterValue.set('');
|
|
6543
|
+
this.isTyping.set(false);
|
|
6544
|
+
// Update the value (this will set selectTitle signal)
|
|
6545
|
+
this.updateValue(newValue);
|
|
6546
|
+
this.close();
|
|
6547
|
+
return;
|
|
6548
|
+
}
|
|
6549
|
+
// Handle backspace to enter typing mode - only if text is selected or at the beginning
|
|
6550
|
+
if (event.key === 'Backspace' && !this.isTyping() && this.selectTitle()) {
|
|
6551
|
+
// Check if there's a text selection
|
|
6552
|
+
let hasSelection = false;
|
|
6553
|
+
if (this.textInput?.inputRef?.nativeElement) {
|
|
6554
|
+
const input = this.textInput.inputRef.nativeElement;
|
|
6555
|
+
const selectionStart = input.selectionStart ?? 0;
|
|
6556
|
+
const selectionEnd = input.selectionEnd ?? 0;
|
|
6557
|
+
hasSelection = selectionStart !== selectionEnd;
|
|
6558
|
+
}
|
|
6559
|
+
// Only enter typing mode if there's a selection (e.g., from clicking/tabbing in)
|
|
6560
|
+
// Otherwise, let the browser handle backspace naturally without entering typing mode
|
|
6561
|
+
if (hasSelection) {
|
|
6562
|
+
this.isTyping.set(true);
|
|
6563
|
+
this.trigger.open();
|
|
6564
|
+
this._isOpen = true;
|
|
6565
|
+
this.inFocusOpen = true;
|
|
6566
|
+
this.preFocusValue = this.value;
|
|
6567
|
+
this.initializeFocusedIndex();
|
|
6568
|
+
}
|
|
6569
|
+
// Let the default backspace behavior happen regardless
|
|
6570
|
+
return;
|
|
6571
|
+
}
|
|
6572
|
+
// Handle backspace while already typing
|
|
6573
|
+
if (event.key === 'Backspace' && this.isTyping()) {
|
|
6574
|
+
// Let default behavior happen, onInputChange will handle it
|
|
6575
|
+
return;
|
|
6576
|
+
}
|
|
6577
|
+
// Handle regular characters when entering typing mode (not already typing)
|
|
6578
|
+
// Exclude navigation keys like Arrow keys, Home, End, etc.
|
|
6579
|
+
const isNavigationKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', 'Delete'].includes(event.key);
|
|
6580
|
+
if (!this.trigger.isOpen() && !this.isTyping() && this.selectTitle() && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey && !isNavigationKey) {
|
|
6581
|
+
// Don't prevent default - let the browser handle the character insertion naturally
|
|
6582
|
+
// Just switch to typing mode and the input event will update filterValue
|
|
6583
|
+
this.isTyping.set(true);
|
|
6584
|
+
this.trigger.open();
|
|
6585
|
+
this._isOpen = true;
|
|
6586
|
+
this.inFocusOpen = true;
|
|
6587
|
+
this.preFocusValue = this.value;
|
|
6588
|
+
this.initializeFocusedIndex();
|
|
6589
|
+
// The input event will fire after this keydown, which will update filterValue
|
|
6590
|
+
// and trigger the dropdown to show filtered options
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6593
|
+
// Handle regular characters while already typing (including space)
|
|
6594
|
+
if (this.isTyping() && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
6595
|
+
// Let default behavior happen, onInputChange will handle it
|
|
6596
|
+
return;
|
|
6597
|
+
}
|
|
6404
6598
|
if (this.trigger.isOpen()) {
|
|
6405
6599
|
if (this.inFocusOpen) {
|
|
6406
6600
|
if (event.key === 'ArrowDown') {
|
|
6601
|
+
event.preventDefault();
|
|
6407
6602
|
this.moveFocused('down');
|
|
6408
|
-
|
|
6603
|
+
// Defer updateValue to avoid change detection errors
|
|
6604
|
+
setTimeout(() => {
|
|
6605
|
+
const availableItems = this.getAvailableItems();
|
|
6606
|
+
if (availableItems.length > 0) {
|
|
6607
|
+
const selectedItem = availableItems[this.focused];
|
|
6608
|
+
// Handle both options (objects) and menuItems (FwMenuItemComponent)
|
|
6609
|
+
if (selectedItem instanceof FwMenuItemComponent) {
|
|
6610
|
+
this.selectValue = selectedItem.value;
|
|
6611
|
+
// Manually update selected state for all menu items
|
|
6612
|
+
this.menuItems?.forEach(item => {
|
|
6613
|
+
item.selected = item.value === this.selectValue;
|
|
6614
|
+
});
|
|
6615
|
+
this.updateValue(selectedItem.value);
|
|
6616
|
+
}
|
|
6617
|
+
else {
|
|
6618
|
+
// For options array items, set selectValue first for immediate highlighting
|
|
6619
|
+
const vProp = this.valueProperty();
|
|
6620
|
+
this.selectValue = this.useFullOptionAsValue()
|
|
6621
|
+
? JSON.stringify(selectedItem)
|
|
6622
|
+
: selectedItem?.[vProp]?.toString();
|
|
6623
|
+
if (this.menu) {
|
|
6624
|
+
this.menu.value = this.selectValue;
|
|
6625
|
+
this.menu.updateLayout();
|
|
6626
|
+
}
|
|
6627
|
+
this.updateValue(selectedItem);
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
}, 0);
|
|
6409
6631
|
}
|
|
6410
|
-
if (event.key === 'ArrowUp') {
|
|
6632
|
+
else if (event.key === 'ArrowUp') {
|
|
6633
|
+
event.preventDefault();
|
|
6411
6634
|
this.moveFocused('up');
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6635
|
+
// Defer updateValue to avoid change detection errors
|
|
6636
|
+
setTimeout(() => {
|
|
6637
|
+
const availableItems = this.getAvailableItems();
|
|
6638
|
+
if (availableItems.length > 0) {
|
|
6639
|
+
const selectedItem = availableItems[this.focused];
|
|
6640
|
+
// Handle both options (objects) and menuItems (FwMenuItemComponent)
|
|
6641
|
+
if (selectedItem instanceof FwMenuItemComponent) {
|
|
6642
|
+
this.selectValue = selectedItem.value;
|
|
6643
|
+
// Manually update selected state for all menu items
|
|
6644
|
+
this.menuItems?.forEach(item => {
|
|
6645
|
+
item.selected = item.value === this.selectValue;
|
|
6646
|
+
});
|
|
6647
|
+
this.updateValue(selectedItem.value);
|
|
6648
|
+
}
|
|
6649
|
+
else {
|
|
6650
|
+
// For options array items, set selectValue first for immediate highlighting
|
|
6651
|
+
const vProp = this.valueProperty();
|
|
6652
|
+
this.selectValue = this.useFullOptionAsValue()
|
|
6653
|
+
? JSON.stringify(selectedItem)
|
|
6654
|
+
: selectedItem?.[vProp]?.toString();
|
|
6655
|
+
if (this.menu) {
|
|
6656
|
+
this.menu.value = this.selectValue;
|
|
6657
|
+
this.menu.updateLayout();
|
|
6658
|
+
}
|
|
6659
|
+
this.updateValue(selectedItem);
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
}, 0);
|
|
6418
6663
|
}
|
|
6419
|
-
if (event.key === '
|
|
6664
|
+
else if (event.key === 'Tab') {
|
|
6665
|
+
this.isTyping.set(false);
|
|
6420
6666
|
this.close();
|
|
6421
6667
|
this.inFocusOpen = false;
|
|
6422
|
-
|
|
6423
|
-
|
|
6668
|
+
// Defer updateValue to avoid change detection errors
|
|
6669
|
+
setTimeout(() => {
|
|
6670
|
+
this.updateValue(this.preFocusValue);
|
|
6671
|
+
}, 0);
|
|
6424
6672
|
}
|
|
6425
6673
|
}
|
|
6426
6674
|
else {
|
|
6427
6675
|
this.inFocusOpen = true;
|
|
6428
6676
|
this.preFocusValue = this.value;
|
|
6677
|
+
this.initializeFocusedIndex();
|
|
6429
6678
|
}
|
|
6430
6679
|
}
|
|
6431
6680
|
else {
|
|
@@ -6438,6 +6687,7 @@ class FwSelectMenuComponent {
|
|
|
6438
6687
|
handleKeyUp(event) {
|
|
6439
6688
|
if (this.trigger.isOpen()) {
|
|
6440
6689
|
if (event.key === 'Escape') {
|
|
6690
|
+
this.isTyping.set(false);
|
|
6441
6691
|
this.close();
|
|
6442
6692
|
this.inFocusOpen = false;
|
|
6443
6693
|
this.updateValue(this.preFocusValue);
|
|
@@ -6454,57 +6704,118 @@ class FwSelectMenuComponent {
|
|
|
6454
6704
|
updateValue(newValue) {
|
|
6455
6705
|
// do housekeeping first
|
|
6456
6706
|
this.onTouched();
|
|
6707
|
+
const vProp = this.valueProperty();
|
|
6708
|
+
const tProp = this.titleProperty();
|
|
6709
|
+
const iProp = this.iconProperty();
|
|
6710
|
+
const fullOption = this.useFullOptionAsValue();
|
|
6457
6711
|
// null guard
|
|
6458
6712
|
if (!newValue) {
|
|
6459
6713
|
this.selectValue = '';
|
|
6460
|
-
this.selectTitle
|
|
6714
|
+
this.selectTitle.set('');
|
|
6461
6715
|
this.selectIcon = '';
|
|
6462
6716
|
return this.onChange(newValue);
|
|
6463
6717
|
}
|
|
6464
6718
|
if (typeof newValue === 'object') {
|
|
6465
|
-
this.selectValue = newValue?.[
|
|
6466
|
-
this.selectTitle
|
|
6467
|
-
this.selectIcon = newValue?.[
|
|
6468
|
-
const changeToEmit =
|
|
6719
|
+
this.selectValue = newValue?.[vProp]?.toString();
|
|
6720
|
+
this.selectTitle.set(newValue?.[tProp]?.toString());
|
|
6721
|
+
this.selectIcon = newValue?.[iProp];
|
|
6722
|
+
const changeToEmit = fullOption ? newValue : newValue[vProp];
|
|
6469
6723
|
return this.onChange(changeToEmit);
|
|
6470
6724
|
}
|
|
6471
|
-
// try and find a matching option
|
|
6472
|
-
const matchedOption = this.options.find(option => {
|
|
6473
|
-
const matchesValue = option[
|
|
6474
|
-
const matchesTitle = option[
|
|
6725
|
+
// try and find a matching option in the options array
|
|
6726
|
+
const matchedOption = this.options().find(option => {
|
|
6727
|
+
const matchesValue = option[vProp] === newValue || option[vProp]?.toString() === newValue?.toString();
|
|
6728
|
+
const matchesTitle = option[tProp] === newValue || option[tProp]?.toString() === newValue?.toString();
|
|
6475
6729
|
return matchesValue || matchesTitle;
|
|
6476
6730
|
});
|
|
6477
6731
|
if (matchedOption) {
|
|
6478
|
-
this.selectValue = matchedOption[
|
|
6479
|
-
this.selectTitle
|
|
6480
|
-
this.selectIcon = matchedOption[
|
|
6481
|
-
const changeToEmit =
|
|
6732
|
+
this.selectValue = matchedOption[vProp]?.toString();
|
|
6733
|
+
this.selectTitle.set(matchedOption[tProp]?.toString());
|
|
6734
|
+
this.selectIcon = matchedOption[iProp];
|
|
6735
|
+
const changeToEmit = fullOption ? matchedOption : matchedOption[vProp];
|
|
6482
6736
|
return this.onChange(changeToEmit);
|
|
6483
6737
|
}
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
this.selectValue =
|
|
6488
|
-
this.selectTitle
|
|
6489
|
-
|
|
6738
|
+
// try and find a matching menu item in content projected items
|
|
6739
|
+
const matchedMenuItem = this.menuItems?.find(item => item.value === newValue);
|
|
6740
|
+
if (matchedMenuItem) {
|
|
6741
|
+
this.selectValue = matchedMenuItem.value?.toString();
|
|
6742
|
+
this.selectTitle.set(matchedMenuItem.title);
|
|
6743
|
+
this.selectIcon = matchedMenuItem.icon;
|
|
6744
|
+
return this.onChange(matchedMenuItem.value);
|
|
6490
6745
|
}
|
|
6746
|
+
// fall back to stringify
|
|
6747
|
+
const stringified = newValue.toString() || '';
|
|
6748
|
+
this.selectValue = stringified;
|
|
6749
|
+
this.selectTitle.set(stringified);
|
|
6750
|
+
return this.onChange(stringified);
|
|
6491
6751
|
}
|
|
6492
6752
|
handleReset() {
|
|
6493
|
-
if (this.showReset) {
|
|
6753
|
+
if (this.showReset()) {
|
|
6494
6754
|
this.updateValue(null);
|
|
6495
6755
|
}
|
|
6496
6756
|
}
|
|
6497
6757
|
close() {
|
|
6498
6758
|
this.trigger.close();
|
|
6499
|
-
this.filterValue
|
|
6500
|
-
this.filterChanged.emit(this.filterValue);
|
|
6759
|
+
this.filterValue.set('');
|
|
6760
|
+
this.filterChanged.emit(this.filterValue());
|
|
6761
|
+
this._isOpen = false;
|
|
6762
|
+
this.isTyping.set(false);
|
|
6501
6763
|
}
|
|
6502
6764
|
onFilterChanged(value) {
|
|
6503
|
-
|
|
6765
|
+
// Don't let the menu-container overwrite our filterValue when we're typing
|
|
6766
|
+
if (this.isTyping()) {
|
|
6767
|
+
return;
|
|
6768
|
+
}
|
|
6769
|
+
this.filterValue.set(value);
|
|
6504
6770
|
this.filterChanged.emit(value);
|
|
6505
6771
|
}
|
|
6506
|
-
|
|
6507
|
-
|
|
6772
|
+
onInputChange(event) {
|
|
6773
|
+
const inputElement = event.target;
|
|
6774
|
+
const value = inputElement.value;
|
|
6775
|
+
// Update filterValue with the current input value
|
|
6776
|
+
this.filterValue.set(value);
|
|
6777
|
+
// If there's a filter value and we're not already typing, enter typing mode
|
|
6778
|
+
if (value && !this.isTyping()) {
|
|
6779
|
+
this.isTyping.set(true);
|
|
6780
|
+
this.inFocusOpen = true;
|
|
6781
|
+
this.preFocusValue = this.value;
|
|
6782
|
+
this.initializeFocusedIndex();
|
|
6783
|
+
}
|
|
6784
|
+
else {
|
|
6785
|
+
// Reset focused index to 0 when filter changes (only if already typing)
|
|
6786
|
+
this.focused = 0;
|
|
6787
|
+
}
|
|
6788
|
+
// Defer selectValue update to avoid ExpressionChangedAfterItHasBeenCheckedError
|
|
6789
|
+
// This happens because we're updating state during change detection
|
|
6790
|
+
setTimeout(() => {
|
|
6791
|
+
// Update selectValue to first available item for highlighting
|
|
6792
|
+
const availableItems = this.getAvailableItems();
|
|
6793
|
+
if (availableItems.length > 0) {
|
|
6794
|
+
const firstItem = availableItems[0];
|
|
6795
|
+
// Handle both options (objects) and menuItems (FwMenuItemComponent)
|
|
6796
|
+
if (firstItem instanceof FwMenuItemComponent) {
|
|
6797
|
+
this.selectValue = firstItem.value;
|
|
6798
|
+
}
|
|
6799
|
+
else {
|
|
6800
|
+
this.selectValue = this.useFullOptionAsValue()
|
|
6801
|
+
? JSON.stringify(firstItem)
|
|
6802
|
+
: firstItem[this.valueProperty()]?.toString();
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
else {
|
|
6806
|
+
// Clear selection when no items match
|
|
6807
|
+
this.selectValue = '';
|
|
6808
|
+
}
|
|
6809
|
+
}, 0);
|
|
6810
|
+
this.filterChanged.emit(this.filterValue());
|
|
6811
|
+
// Auto-open dropdown when user starts typing
|
|
6812
|
+
if (this.filterValue() && !this.trigger.isOpen()) {
|
|
6813
|
+
this.trigger.open();
|
|
6814
|
+
this._isOpen = true;
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: FwSelectMenuComponent, deps: [{ token: i1$4.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6818
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: FwSelectMenuComponent, isStandalone: true, selector: "fw-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, valueProperty: { classPropertyName: "valueProperty", publicName: "valueProperty", isSignal: true, isRequired: false, transformFunction: null }, useFullOptionAsValue: { classPropertyName: "useFullOptionAsValue", publicName: "useFullOptionAsValue", isSignal: true, isRequired: false, transformFunction: null }, titleProperty: { classPropertyName: "titleProperty", publicName: "titleProperty", isSignal: true, isRequired: false, transformFunction: null }, iconProperty: { classPropertyName: "iconProperty", publicName: "iconProperty", isSignal: true, isRequired: false, transformFunction: null }, staticIcon: { classPropertyName: "staticIcon", publicName: "staticIcon", isSignal: true, isRequired: false, transformFunction: null }, descriptionProperty: { classPropertyName: "descriptionProperty", publicName: "descriptionProperty", isSignal: true, isRequired: false, transformFunction: null }, showFilter: { classPropertyName: "showFilter", publicName: "showFilter", isSignal: true, isRequired: false, transformFunction: null }, showReset: { classPropertyName: "showReset", publicName: "showReset", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errored: { classPropertyName: "errored", publicName: "errored", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, optionsWidth: { classPropertyName: "optionsWidth", publicName: "optionsWidth", isSignal: true, isRequired: false, transformFunction: null }, minOptionsHeight: { classPropertyName: "minOptionsHeight", publicName: "minOptionsHeight", isSignal: true, isRequired: false, transformFunction: null }, maxOptionsHeight: { classPropertyName: "maxOptionsHeight", publicName: "maxOptionsHeight", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { change: "change", filterChanged: "filterChanged" }, host: { listeners: { "document:click": "outsideClick($event.target)" }, properties: { "class.disabled": "this.disabledClass" } }, queries: [{ propertyName: "menuItems", predicate: FwMenuItemComponent, descendants: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: CdkMenuTrigger, descendants: true }, { propertyName: "menu", first: true, predicate: FwMenuComponent, descendants: true }, { propertyName: "textInput", first: true, predicate: FwTextInputComponent, descendants: true }], ngImport: i0, template: "<div #wrapper [style.width]=\"width()\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"inputDisplayValue()\"\n [leftIcon]=\"staticIcon() || selectIcon || null\"\n [rightIcon]=\"(selectTitle()&&showReset())?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder()\"\n [size]=\"size()\"\n [error]=\"errored() || (invalid && touched)\"\n (input)=\"onInputChange($event)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (focus)=\"handleFocus()\"\n (click)=\"handleInputClick()\"\n [readOnly]=\"false\">\n </fw-text-input>\n <ng-template #selectMenu>\n @if (!disabled()) {\n <fw-menu-container\n [filterText]=\"filterValue()\"\n [showFilter]=\"showFilter()\" [width]=\"optionsWidth() || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight()\" [minHeight]=\"minOptionsHeight()\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled()\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n @if (menuItems.length === 0) {\n @if (optionsWithValues().length > 0) {\n @for (item of optionsWithValues(); track item.trackingId) {\n <fw-menu-item\n [title]=\"item.raw[titleProperty()]?.toString()\"\n [description]=\"item.raw[descriptionProperty()]\"\n [value]=\"item.value\"\n [icon]=\"item.raw[iconProperty()]\"\n [disabled]=\"$any(item.raw).disabled\"\n >\n </fw-menu-item>\n }\n } @else {\n @if (isTyping() && filterValue()) {\n <fw-menu-item\n title=\"No matches found...\"\n [disabled]=\"true\"\n >\n </fw-menu-item>\n }\n }\n }\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n }\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host>div{cursor:pointer}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"], dependencies: [{ kind: "component", type: FwTextInputComponent, selector: "fw-text-input", inputs: ["disabled", "useActionableIcons", "leftIcon", "rightIcon", "prefix", "context", "helperText", "errorText", "errorInIconTooltip", "placeholder", "readOnly", "size", "type", "maxLength", "autofocus", "autocomplete", "value", "error", "width"], outputs: ["leftIconAction", "rightIconAction"] }, { kind: "directive", type: MenuRegisterDirective, selector: "[fwMenuRegister]" }, { kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: FwMenuContainerComponent, selector: "fw-menu-container, fw-menu-filter", inputs: ["width", "maxHeight", "minHeight", "border", "shadow", "showFilter", "filterText", "focusFilterOnMount", "offset", "emptyText", "filterFn", "additionalMenuItems", "keyHandler"], outputs: ["filteredMenuItemChange", "filterChanged"] }, { kind: "component", type: FwMenuComponent, selector: "fw-menu", inputs: ["disabled", "size", "multiSelect", "useCheckbox", "value"], outputs: ["change"] }, { kind: "component", type: FwMenuItemComponent, selector: "fw-menu-item", inputs: ["value", "size", "title", "description", "icon", "iconColor", "disabled", "showCheckbox", "checkboxColor", "multiSelect", "hidden", "collapsed", "href", "target", "subItemsOpen", "mouseEnterHandler", "focused", "selected"], outputs: ["mouseEnterHandlerChange", "click"] }] }); }
|
|
6508
6819
|
}
|
|
6509
6820
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: FwSelectMenuComponent, decorators: [{
|
|
6510
6821
|
type: Component,
|
|
@@ -6518,8 +6829,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
6518
6829
|
FwMenuComponent,
|
|
6519
6830
|
NgFor,
|
|
6520
6831
|
FwMenuItemComponent,
|
|
6521
|
-
], template: "<div #wrapper [
|
|
6522
|
-
}], ctorParameters: () => [{ type:
|
|
6832
|
+
], template: "<div #wrapper [style.width]=\"width()\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"inputDisplayValue()\"\n [leftIcon]=\"staticIcon() || selectIcon || null\"\n [rightIcon]=\"(selectTitle()&&showReset())?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder()\"\n [size]=\"size()\"\n [error]=\"errored() || (invalid && touched)\"\n (input)=\"onInputChange($event)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (focus)=\"handleFocus()\"\n (click)=\"handleInputClick()\"\n [readOnly]=\"false\">\n </fw-text-input>\n <ng-template #selectMenu>\n @if (!disabled()) {\n <fw-menu-container\n [filterText]=\"filterValue()\"\n [showFilter]=\"showFilter()\" [width]=\"optionsWidth() || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight()\" [minHeight]=\"minOptionsHeight()\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled()\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n @if (menuItems.length === 0) {\n @if (optionsWithValues().length > 0) {\n @for (item of optionsWithValues(); track item.trackingId) {\n <fw-menu-item\n [title]=\"item.raw[titleProperty()]?.toString()\"\n [description]=\"item.raw[descriptionProperty()]\"\n [value]=\"item.value\"\n [icon]=\"item.raw[iconProperty()]\"\n [disabled]=\"$any(item.raw).disabled\"\n >\n </fw-menu-item>\n }\n } @else {\n @if (isTyping() && filterValue()) {\n <fw-menu-item\n title=\"No matches found...\"\n [disabled]=\"true\"\n >\n </fw-menu-item>\n }\n }\n }\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n }\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host>div{cursor:pointer}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"] }]
|
|
6833
|
+
}], ctorParameters: () => [{ type: i1$4.NgControl, decorators: [{
|
|
6523
6834
|
type: Optional
|
|
6524
6835
|
}, {
|
|
6525
6836
|
type: Self
|
|
@@ -6529,46 +6840,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
|
|
|
6529
6840
|
}], disabledClass: [{
|
|
6530
6841
|
type: HostBinding,
|
|
6531
6842
|
args: ['class.disabled']
|
|
6532
|
-
}], options: [{
|
|
6533
|
-
type: Input
|
|
6534
|
-
}], valueProperty: [{
|
|
6535
|
-
type: Input
|
|
6536
|
-
}], useFullOptionAsValue: [{
|
|
6537
|
-
type: Input
|
|
6538
|
-
}], titleProperty: [{
|
|
6539
|
-
type: Input
|
|
6540
|
-
}], iconProperty: [{
|
|
6541
|
-
type: Input
|
|
6542
|
-
}], staticIcon: [{
|
|
6543
|
-
type: Input
|
|
6544
|
-
}], descriptionProperty: [{
|
|
6545
|
-
type: Input
|
|
6546
|
-
}], showFilter: [{
|
|
6547
|
-
type: Input
|
|
6548
|
-
}], showReset: [{
|
|
6549
|
-
type: Input
|
|
6550
|
-
}], disabled: [{
|
|
6551
|
-
type: Input
|
|
6552
|
-
}], errored: [{
|
|
6553
|
-
type: Input
|
|
6554
|
-
}], width: [{
|
|
6555
|
-
type: Input
|
|
6556
|
-
}], optionsWidth: [{
|
|
6557
|
-
type: Input
|
|
6558
|
-
}], minOptionsHeight: [{
|
|
6559
|
-
type: Input
|
|
6560
|
-
}], maxOptionsHeight: [{
|
|
6561
|
-
type: Input
|
|
6562
|
-
}], size: [{
|
|
6563
|
-
type: Input
|
|
6564
|
-
}], placeholder: [{
|
|
6565
|
-
type: Input
|
|
6566
6843
|
}], trigger: [{
|
|
6567
6844
|
type: ViewChild,
|
|
6568
6845
|
args: [CdkMenuTrigger]
|
|
6569
6846
|
}], menu: [{
|
|
6570
6847
|
type: ViewChild,
|
|
6571
6848
|
args: [FwMenuComponent]
|
|
6849
|
+
}], textInput: [{
|
|
6850
|
+
type: ViewChild,
|
|
6851
|
+
args: [FwTextInputComponent]
|
|
6572
6852
|
}], menuItems: [{
|
|
6573
6853
|
type: ContentChildren,
|
|
6574
6854
|
args: [FwMenuItemComponent, { descendants: true }]
|