superdesk-ui-framework 3.0.1-beta.10 → 3.0.1-beta.11

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.
@@ -263,11 +263,17 @@ tags-input,
263
263
  }
264
264
  .suggestion-list--multi-select {
265
265
  padding: 4px 0 !important;
266
+ min-height: 40px;
266
267
  }
267
268
  .suggestion-list--loader {
268
269
  padding: 4px 0 !important;
269
270
  position: relative;
270
- min-height: 56px;
271
+ min-height: 3.2rem;
272
+ >div {
273
+ padding: 0.5rem 1rem;
274
+ min-height: $sd-base-increment * 4;
275
+ position: relative;
276
+ }
271
277
  }
272
278
  .suggestion-item {
273
279
  padding: 0.5rem 1rem;
@@ -309,6 +315,19 @@ tags-input,
309
315
  .suggestion-item--disabled {
310
316
  opacity: 0.5;
311
317
  }
318
+ .suggestion-item--nothing-found {
319
+ padding: 0.5rem 1rem;
320
+ cursor: not-allowed;
321
+ white-space: nowrap;
322
+ overflow: hidden;
323
+ text-overflow: ellipsis;
324
+ color: $sd-text-light;
325
+ transition: all ease 0.2s;
326
+ min-height: $sd-base-increment * 4;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ }
312
331
  }
313
332
  }
314
333
 
@@ -1,22 +1,25 @@
1
1
  import * as React from "react";
2
2
  import { Icon } from "./Icon";
3
3
  import { Loader } from "./Loader";
4
- import classNames from 'classnames';
5
4
  import nextId from "react-id-generator";
5
+ import _debounce from 'lodash/debounce';
6
+ import { InputWrapper } from "./Form";
7
+ import { createPopper } from '@popperjs/core';
8
+ import {isEqual} from 'lodash';
6
9
 
7
10
  interface IState<T> {
8
11
  value: Array<T>;
9
12
  options: Array<ITreeNode<T>>;
10
- firstBranchOptions: Array<any>; // to return on first branch in dropdown
11
- openDropdown: boolean; // open/close dropdown
12
- activeTree: Array<any>; // for filtered array
13
- filterArr: Array<any>; // for filtered array
14
- searchFieldValue: string; // filter value of input
15
- buttonTree: Array<any>; // array of button (for backButton)
16
- buttonValue: any; // button for name of category
17
- buttonMouseEvent: boolean; // valueButton hover
13
+ firstBranchOptions: Array<any>;
14
+ openDropdown: boolean;
15
+ activeTree: Array<any>;
16
+ filterArr: Array<any>;
17
+ searchFieldValue: string;
18
+ buttonTree: Array<any>;
19
+ buttonValue: any;
20
+ buttonMouseEvent: boolean;
18
21
  loading: boolean;
19
- invalid: boolean;
22
+ // provera: boolean;
20
23
  }
21
24
 
22
25
  interface IPropsBase<T> {
@@ -28,6 +31,7 @@ interface IPropsBase<T> {
28
31
  loading?: boolean;
29
32
  singleLevelSearch?: boolean;
30
33
  placeholder?: string;
34
+ searchPlaceholder?: string;
31
35
  invalid?: boolean;
32
36
  inlineLabel?: boolean;
33
37
  labelHidden?: boolean;
@@ -67,6 +71,7 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
67
71
  private dropdownRef: React.RefObject<HTMLInputElement>;
68
72
  private openDropdownRef: React.RefObject<HTMLButtonElement>;
69
73
  private htmlId: string = nextId();
74
+ private popperInstance: any;
70
75
 
71
76
  constructor(props: IProps<T>) {
72
77
  super(props);
@@ -81,8 +86,8 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
81
86
  buttonValue: [],
82
87
  buttonMouseEvent: false,
83
88
  openDropdown: false,
84
- loading: this.props.loading ? this.props.loading : false,
85
- invalid: this.props.invalid ? this.props.invalid : false,
89
+ loading: false,
90
+ // provera: false
86
91
  };
87
92
 
88
93
  this.removeClick = this.removeClick.bind(this);
@@ -93,8 +98,57 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
93
98
  this.handleTree = this.handleTree.bind(this);
94
99
  this.filteredItem = this.filteredItem.bind(this);
95
100
  this.banchButton = this.banchButton.bind(this);
101
+ this.handleDebounce = this.handleDebounce.bind(this);
102
+ this.toggleMenu = this.toggleMenu.bind(this);
96
103
  this.dropdownRef = React.createRef();
97
104
  this.openDropdownRef = React.createRef();
105
+
106
+ }
107
+
108
+ componentDidMount = () => {
109
+ this.recursion(this.state.options);
110
+ document.addEventListener("mousedown", (event: any) => {
111
+ if ((this.dropdownRef.current && !this.dropdownRef.current.contains(event.target))
112
+ && (this.openDropdownRef.current && !this.openDropdownRef.current.contains(event.target))) {
113
+ this.setState({openDropdown: false});
114
+ }
115
+ });
116
+ }
117
+
118
+ componentDidUpdate(prevProps: Readonly<IProps<T>>, prevState: Readonly<IState<T>>): void {
119
+ if (!isEqual(prevState.value, this.state.value)) {
120
+ this.props.onChange(this.state.value);
121
+ } else if (!isEqual(prevProps.value, this.props.value)) {
122
+ this.props.onChange(this.state.value);
123
+ }
124
+ if (prevState.openDropdown !== this.state.openDropdown) {
125
+ this.toggleMenu();
126
+ }
127
+ if (this.props.kind === 'synchronous') {
128
+ if ((prevState.activeTree !== this.state.activeTree)
129
+ || (prevState.filterArr !== this.state.filterArr)
130
+ || (prevState.options !== this.state.options)) {
131
+ this.popperInstance.update();
132
+ }
133
+ }
134
+ }
135
+
136
+ toggleMenu() {
137
+ if (this.state.openDropdown) {
138
+ if (this.openDropdownRef.current && this.dropdownRef.current) {
139
+ this.popperInstance = createPopper(this.openDropdownRef.current, this.dropdownRef.current, {
140
+ placement: 'bottom-start',
141
+ modifiers: [
142
+ {
143
+ name: 'offset',
144
+ options: {
145
+ offset: [-4, 4],
146
+ },
147
+ },
148
+ ],
149
+ });
150
+ }
151
+ }
98
152
  }
99
153
 
100
154
  removeClick(i: number) {
@@ -253,27 +307,9 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
253
307
  });
254
308
  }
255
309
 
256
- componentDidMount = () => {
257
- this.recursion(this.state.options);
258
- document.addEventListener("mousedown", (event: any) => {
259
- if ((this.dropdownRef.current && !this.dropdownRef.current.contains(event.target))
260
- && (this.openDropdownRef.current && !this.openDropdownRef.current.contains(event.target))) {
261
- this.setState({openDropdown: false});
262
- }
263
- });
264
- }
265
-
266
- componentDidUpdate(prevProps: Readonly<IProps<T>>, prevState: Readonly<IState<T>>): void {
267
- if (prevState.value !== this.state.value) {
268
- this.props.onChange(this.state.value);
269
- } else if (prevProps.value !== this.props.value) {
270
- this.props.onChange(this.state.value);
271
- }
272
- }
273
-
274
310
  filteredItem(arr: Array<ITreeNode<T>>) {
275
311
  if (this.props.kind === 'synchronous') {
276
- return arr.filter((item) => {
312
+ let filteredArr = arr.filter((item) => {
277
313
  if (this.state.searchFieldValue) {
278
314
  if (this.props.getLabel(item.value)
279
315
  .toLowerCase().includes(this.state.searchFieldValue.toLowerCase())) {
@@ -284,33 +320,39 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
284
320
  } else {
285
321
  return item.value;
286
322
  }
287
- }).map((option, i) => {
288
- let selectedItem = this.state.value.some((obj) =>
289
- this.props.getId(obj) === this.props.getId(option.value),
290
- );
291
- return <li key={i}
292
- className={`suggestion-item suggestion-item--multi-select`}
293
- onClick={(event) => {
294
- this.setState({
295
- searchFieldValue: '',
296
- }),
297
- event.preventDefault();
298
- event.stopPropagation();
299
- this.handleTree(event, option);
300
- }}>
301
- {this.props.optionTemplate
302
- ? this.props.optionTemplate(option.value)
303
- : <span
304
- className={selectedItem
305
- ? 'suggestion-item--disabled' : undefined}
306
- >
307
- {this.props.getLabel(option.value)}
308
- </span>}
309
- {option.children && <span className="suggestion-item__icon">
310
- <Icon name="chevron-right-thin"></Icon>
311
- </span>}
312
- </li>;
313
323
  });
324
+
325
+ if (filteredArr.length === 0) {
326
+ return <li className="suggestion-item--nothing-found">Nothing fonud</li>;
327
+ } else {
328
+ return filteredArr.map((option, i) => {
329
+ let selectedItem = this.state.value.some((obj) =>
330
+ this.props.getId(obj) === this.props.getId(option.value),
331
+ );
332
+ return <li key={i}
333
+ className={`suggestion-item suggestion-item--multi-select`}
334
+ onClick={(event) => {
335
+ this.setState({
336
+ searchFieldValue: '',
337
+ }),
338
+ event.preventDefault();
339
+ event.stopPropagation();
340
+ this.handleTree(event, option);
341
+ }}>
342
+ {this.props.optionTemplate
343
+ ? this.props.optionTemplate(option.value)
344
+ : <span
345
+ className={selectedItem
346
+ ? 'suggestion-item--disabled' : undefined}
347
+ >
348
+ {this.props.getLabel(option.value)}
349
+ </span>}
350
+ {option.children && <span className="suggestion-item__icon">
351
+ <Icon name="chevron-right-thin"></Icon>
352
+ </span>}
353
+ </li>;
354
+ });
355
+ }
314
356
  } else if (this.props.kind === 'asynchronous') {
315
357
  return this.state.options.map((item, i) => {
316
358
  let selectedItem = this.state.value.some((obj) =>
@@ -345,52 +387,68 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
345
387
 
346
388
  if (!selectedButton) {
347
389
  return <button
348
- className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '')}
349
- onMouseOver={() => this.setState({buttonMouseEvent: true})}
350
- onMouseOut={() => this.setState({buttonMouseEvent: false})}
351
- value={this.state.buttonValue}
352
- onClick={(event) => this.handleBranchValue(event, this.state.buttonValue)}>
353
- Choose entire category
354
- </button>;
390
+ className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '')}
391
+ onMouseOver={() => this.setState({buttonMouseEvent: true})}
392
+ onMouseOut={() => this.setState({buttonMouseEvent: false})}
393
+ value={this.state.buttonValue}
394
+ onClick={(event) => this.handleBranchValue(event, this.state.buttonValue)}>
395
+ Choose entire category
396
+ </button>;
355
397
  } else {
356
398
  return <button
357
- className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '') + ' autocomplete__button--disabled'}
358
- value={this.state.buttonValue}>
359
- Category selected
360
- </button>;
399
+ className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '') + ' autocomplete__button--disabled'}
400
+ value={this.state.buttonValue}>
401
+ Category selected
402
+ </button>;
361
403
  }
362
404
  }
363
405
 
364
- render() {
365
-
366
- const labelClasses = classNames('sd-input__label', {
367
- 'a11y-only': this.props.labelHidden,
368
- });
406
+ private debounceFn = _debounce(this.handleDebounce, 500);
407
+ private ICancelFn: any;
369
408
 
370
- const classesLabel = classNames('sd-input', {
371
- 'sd-input--inline-label': this.props.inlineLabel,
372
- 'sd-input--required': this.props.required,
373
- 'sd-input--disabled': this.props.disabled,
374
- 'sd-input--full-width': this.props.fullWidth,
375
- 'sd-input--invalid': this.props.invalid || this.state.invalid,
376
- });
409
+ handleDebounce() {
410
+ this.setState({options: []});
411
+ if (this.props.kind === 'asynchronous') {
412
+ if (this.state.searchFieldValue) {
413
+ this.setState({
414
+ loading: true,
415
+ // provera: false
416
+ });
417
+ this.ICancelFn = this.props.searchOptions(this.state.searchFieldValue, (items) => {
418
+ // if (items.length === 0) {
419
+ // this.setState({provera: true, loading: false})
420
+ // } else {
421
+ this.setState({options: items, loading: false});
422
+ this.popperInstance.update();
423
+ // }
424
+ });
425
+ }
426
+ }
427
+ }
377
428
 
429
+ render() {
378
430
  return (
379
- <div className={classesLabel}>
380
- <label className={labelClasses} htmlFor={this.htmlId} id={this.htmlId + 'label'}
381
- tabIndex={this.props.tabindex === undefined ? undefined : -1}>
382
- {this.props.label}
383
- </label>
384
-
431
+ <InputWrapper
432
+ label={this.props.label}
433
+ error={this.props.error}
434
+ required={this.props.required}
435
+ disabled={this.props.disabled}
436
+ invalid={this.props.invalid}
437
+ info={this.props.info}
438
+ inlineLabel={this.props.inlineLabel}
439
+ labelHidden={this.props.labelHidden}
440
+ fullWidth={this.props.fullWidth}
441
+ htmlId={this.htmlId}
442
+ tabindex={this.props.tabindex}>
385
443
  <div className={`tags-input tags-input--${this.props.allowMultiple ? 'multi-select' : 'single-select'} sd-input__input`}>
386
444
  {this.props.allowMultiple
387
445
  ? <div className="tags-input__tags">
388
- {this.props.readOnly
389
- || <button ref={this.openDropdownRef}
390
- className="tags-input__add-button"
391
- onClick={() => this.setState({openDropdown: !this.state.openDropdown})}>
392
- <i className="icon-plus-large"></i>
393
- </button>}
446
+ {this.props.readOnly
447
+ || <button ref={this.openDropdownRef}
448
+ className="tags-input__add-button"
449
+ onClick={() => this.setState({openDropdown: !this.state.openDropdown})}>
450
+ <i className="icon-plus-large"></i>
451
+ </button>}
394
452
  <ul className="tags-input__tag-list">
395
453
  {this.state.value.map((item, i: number) => {
396
454
  return <React.Fragment key={i}>
@@ -411,81 +469,85 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
411
469
  </React.Fragment>;
412
470
  })}
413
471
  </ul>
414
- {this.state.value.length > 0 ?
415
- this.props.readOnly
472
+ {this.state.value.length > 0
473
+ ? this.props.readOnly
416
474
  || <button className="tags-input__remove-value"
417
475
  onClick={() => this.setState({value: []})}>
418
476
  <Icon name='remove-sign'></Icon>
419
477
  </button> : null}
420
478
  </div>
421
479
  : <div className="tags-input__tags">
422
- <button
480
+ {this.props.readOnly
481
+ || <button
423
482
  className="tags-input__overlay-button"
424
483
  ref={this.openDropdownRef}
425
484
  onClick={() => this.setState({openDropdown: !this.state.openDropdown})}>
426
- </button>
485
+ </button>}
427
486
  {this.state.value.length < 1 && <span className={ 'tags-input__single-item'
428
487
  + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}>
429
488
  <span className="tags-input__placeholder">
430
489
  {this.props.placeholder}
431
490
  </span>
432
491
  </span>}
433
- {this.state.value.map((item, i: number) => {
434
- return <React.Fragment key={i}>
435
- <span
436
- className={ 'tags-input__single-item'
437
- + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
438
- onClick={() => this.props.readOnly || this.removeClick(i)}>
439
- <span className="tags-input__helper-box">
440
- {this.props.valueTemplate
441
- ? this.props.valueTemplate(item)
442
- : <span>{this.props.getLabel(item)}</span>}
443
- {this.props.readOnly
444
- || <span className="tags-input__remove-button">
445
- <Icon name='remove-sign'></Icon>
446
- </span>}
447
- </span>
492
+ {this.state.value.map((item, i: number) => {
493
+ return <React.Fragment key={i}>
494
+ <span
495
+ className={ 'tags-input__single-item'
496
+ + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
497
+ onClick={() => this.props.readOnly || this.removeClick(i)}>
498
+ <span className="tags-input__helper-box">
499
+ {this.props.valueTemplate
500
+ ? this.props.valueTemplate(item)
501
+ : <span>{this.props.getLabel(item)}</span>}
502
+ {this.props.readOnly
503
+ || <span className="tags-input__remove-button">
504
+ <Icon name='remove-sign'></Icon>
505
+ </span>}
448
506
  </span>
449
- </React.Fragment>;
450
- })}
507
+ </span>
508
+ </React.Fragment>;
509
+ })}
451
510
  </div>
452
511
  }
453
512
 
454
- {this.state.openDropdown &&
455
- <div className={"autocomplete autocomplete--multi-select" + (this.props.width === 'medium' ? ' autocomplete--fixed-width' : '')} ref={this.dropdownRef}>
513
+ {this.state.openDropdown
514
+ && <div className={"autocomplete autocomplete--multi-select" + (this.props.width === 'medium' ? ' autocomplete--fixed-width' : '')} ref={this.dropdownRef}>
456
515
  <div className='autocomplete__header'>
457
516
  <div className="autocomplete__icon" onClick={() => {
458
517
  this.backButtonValue();
459
518
  this.backButton();
460
519
  }}>
461
- <Icon name="search" className="search"></Icon>
520
+ <Icon name="search" className="search"></Icon>
462
521
  </div>
463
522
  <div className='autocomplete__filter'>
464
523
  <input
465
- placeholder={this.props.singleLevelSearch ? 'Search this category...' : 'Search...'}
524
+ placeholder={this.props.searchPlaceholder}
466
525
  type="text"
467
526
  className="autocomplete__input"
468
527
  ref={(input) => input && input.focus()}
469
528
  value={this.state.searchFieldValue}
470
529
  onChange={(event) => {
471
- const value = event.target.value;
472
- if (this.props.kind === 'asynchronous') {
473
- this.setState({
474
- searchFieldValue: value,
475
- loading: true,
476
- }),
477
- this.props.searchOptions(value, (items) => {
478
- this.setState({options: items, loading: false});
479
- });
480
- } else if (this.props.kind === 'synchronous') {
481
- this.setState({searchFieldValue: value});
530
+ if (this.props.kind === 'synchronous') {
531
+ this.setState({searchFieldValue: event.target.value});
532
+ this.popperInstance.update();
533
+ } else if (this.props.kind === 'asynchronous') {
534
+ if (this.ICancelFn) {
535
+ this.ICancelFn();
536
+ }
537
+ this.setState({searchFieldValue: event.target.value, options: []});
538
+ this.popperInstance.update();
539
+ this.debounceFn();
540
+ } else {
541
+ return;
482
542
  }
483
- }}
484
- />
543
+ // if(!this.state.searchFieldValue) {
544
+ // this.setState({provera: false});
545
+ // }
546
+ }} />
485
547
  </div>
486
548
  </div>
487
- {this.state.activeTree.length > 0 &&
488
- <div className='autocomplete__category-header'>
549
+ {this.state.activeTree.length > 0
550
+ && <div className='autocomplete__category-header'>
489
551
  <div className="autocomplete__icon" onClick={() => {
490
552
  this.backButtonValue();
491
553
  this.backButton();
@@ -505,54 +567,46 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
505
567
  </div>}
506
568
  {this.state.loading
507
569
  ? <ul className="suggestion-list--loader"><Loader overlay={true}></Loader></ul>
508
- :
509
- this.state.searchFieldValue === ''
510
- ? this.props.getOptions ?
511
- <ul className="suggestion-list suggestion-list--multi-select">
512
- {this.state.options
513
- .map((option, i: React.Key | undefined) => {
514
- let selectedItem = this.state.value.some((obj) =>
515
- this.props.getId(obj) === this.props.getLabel(option.value),
516
- );
517
- return (
518
- <li key={i}
519
- className={`suggestion-item suggestion-item--multi-select`}
520
- onClick={(event) => {
521
- event.preventDefault();
522
- event.stopPropagation();
523
- this.handleTree(event, option);
524
- }}>
525
- {this.props.optionTemplate
526
- ? this.props.optionTemplate(option.value)
527
- : <span
528
- className={selectedItem
529
- ? 'suggestion-item--disabled' : undefined}
530
- >
531
- {this.props.getLabel(option.value)}
532
- </span>}
533
- {option.children && <span className="suggestion-item__icon">
534
- <Icon name="chevron-right-thin"></Icon>
535
- </span>}
536
- </li>
537
- );
538
- })}</ul> : null
539
- :
540
- <ul className="suggestion-list suggestion-list--multi-select">
570
+ : this.state.searchFieldValue === ''
571
+ ? this.props.getOptions
572
+ ? <ul className="suggestion-list suggestion-list--multi-select">
573
+ {this.state.options
574
+ .map((option, i: React.Key | undefined) => {
575
+ let selectedItem = this.state.value.some((obj) =>
576
+ this.props.getId(obj) === this.props.getLabel(option.value),
577
+ );
578
+ return (
579
+ <li key={i}
580
+ className={`suggestion-item suggestion-item--multi-select`}
581
+ onClick={(event) => {
582
+ event.preventDefault();
583
+ event.stopPropagation();
584
+ this.handleTree(event, option);
585
+ }}>
586
+ {this.props.optionTemplate
587
+ ? this.props.optionTemplate(option.value)
588
+ : <span
589
+ className={selectedItem
590
+ ? 'suggestion-item--disabled' : undefined}
591
+ >
592
+ {this.props.getLabel(option.value)}
593
+ </span>}
594
+ {option.children && <span className="suggestion-item__icon">
595
+ <Icon name="chevron-right-thin"></Icon>
596
+ </span>}
597
+ </li>
598
+ );
599
+ })}</ul> : null
600
+ : <ul className="suggestion-list suggestion-list--multi-select">
541
601
  {this.filteredItem(this.props.singleLevelSearch
542
602
  ? this.state.options : this.state.filterArr)}
603
+ {/* {this.state.provera
604
+ && <li className="suggestion-item--nothing-found">Nothing fonud</li>} */}
543
605
  </ul>
544
- }
606
+ }
545
607
  </div>}
546
608
  </div>
547
-
548
- <div className='sd-input__message-box'>
549
- {this.props.info && !this.props.invalid && !this.state.invalid ?
550
- <div className='sd-input__hint'>{this.props.info}</div> : null}
551
- {this.props.invalid || this.state.invalid ?
552
- <div className='sd-input__message'>{this.props.error}</div>
553
- : null}
554
- </div>
555
- </div>
609
+ </InputWrapper>
556
610
  );
557
611
  }
558
612
  }