@itfin/components 1.2.84 → 1.2.86

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.
Files changed (67) hide show
  1. package/package.json +7 -6
  2. package/src/components/customize/PropertiesEditMenu.vue +53 -0
  3. package/src/components/customize/PropertiesItemsMenu.vue +33 -0
  4. package/src/components/customize/PropertiesList.vue +179 -0
  5. package/src/components/customize/PropertiesPopupMenu.vue +46 -0
  6. package/src/components/customize/PropertyInlineEdit.vue +46 -0
  7. package/src/components/customize/index.stories.js +65 -0
  8. package/src/components/dropdown/Dropdown.vue +8 -19
  9. package/src/components/icon/components/type_checkbox.vue +2 -0
  10. package/src/components/icon/components/type_date.vue +2 -0
  11. package/src/components/icon/components/type_email.vue +2 -0
  12. package/src/components/icon/components/type_file.vue +2 -0
  13. package/src/components/icon/components/type_formula.vue +2 -0
  14. package/src/components/icon/components/type_id.vue +2 -0
  15. package/src/components/icon/components/type_multiselect.vue +2 -0
  16. package/src/components/icon/components/type_number.vue +2 -0
  17. package/src/components/icon/components/type_person.vue +2 -0
  18. package/src/components/icon/components/type_phone.vue +2 -0
  19. package/src/components/icon/components/type_relation.vue +2 -0
  20. package/src/components/icon/components/type_select.vue +2 -0
  21. package/src/components/icon/components/type_status.vue +2 -0
  22. package/src/components/icon/components/type_text.vue +2 -0
  23. package/src/components/icon/components/type_time.vue +2 -0
  24. package/src/components/icon/components/type_url.vue +2 -0
  25. package/src/components/icon/components/type_user.vue +2 -0
  26. package/src/components/icon/icons/type_checkbox.svg +1 -0
  27. package/src/components/icon/icons/type_date.svg +1 -0
  28. package/src/components/icon/icons/type_email.svg +1 -0
  29. package/src/components/icon/icons/type_file.svg +1 -0
  30. package/src/components/icon/icons/type_formula.svg +1 -0
  31. package/src/components/icon/icons/type_id.svg +1 -0
  32. package/src/components/icon/icons/type_multiselect.svg +1 -0
  33. package/src/components/icon/icons/type_number.svg +1 -0
  34. package/src/components/icon/icons/type_person.svg +1 -0
  35. package/src/components/icon/icons/type_phone.svg +1 -0
  36. package/src/components/icon/icons/type_relation.svg +1 -0
  37. package/src/components/icon/icons/type_select.svg +1 -0
  38. package/src/components/icon/icons/type_status.svg +1 -0
  39. package/src/components/icon/icons/type_text.svg +1 -0
  40. package/src/components/icon/icons/type_time.svg +1 -0
  41. package/src/components/icon/icons/type_url.svg +1 -0
  42. package/src/components/icon/icons/type_user.svg +1 -0
  43. package/src/components/icon/icons.js +295 -278
  44. package/src/components/modal/DeleteConfirmModal.vue +78 -0
  45. package/src/components/modal/ItemEditor.vue +219 -0
  46. package/src/components/modal/Modal.vue +15 -13
  47. package/src/components/modal/index.stories.js +15 -10
  48. package/src/components/popover/ConfirmPopover.vue +1 -1
  49. package/src/components/popover/DeleteConfirmPopover.vue +39 -0
  50. package/src/components/popover/IconPopover.vue +136 -0
  51. package/src/components/popover/SelectPopover.vue +127 -0
  52. package/src/components/popover/index.stories.js +26 -10
  53. package/src/components/sortable/AutoScroll.vue +142 -0
  54. package/src/components/sortable/Sortable.scss +93 -0
  55. package/src/components/sortable/Sortable.vue +399 -0
  56. package/src/components/sortable/sortable-item-list/axis.js +9 -0
  57. package/src/components/sortable/sortable-item-list/mocked-sortable-item-list.js +415 -0
  58. package/src/components/sortable/sortable-item-list/original-sortable-item-list.js +105 -0
  59. package/src/components/sortable/utils/event-outside.js +30 -0
  60. package/src/components/sortable/utils/get-relative-position.js +41 -0
  61. package/src/components/sortable/utils/sort-item-list.js +13 -0
  62. package/src/components/sortable/utils/stop-event.js +16 -0
  63. package/src/components/sortable/utils/vibrate.js +18 -0
  64. package/src/components/text-field/MoneyField.vue +103 -0
  65. package/src/components/text-field/index.stories.js +53 -0
  66. package/src/directives/tooltip.js +1 -1
  67. package/src/locales/en.js +28 -0
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <div>
3
+ <slot name="activator" :on="{ click: onClick }" :loading="loading">
4
+ <itf-button
5
+ icon
6
+ :disabled="loading || disabled"
7
+ :loading="loading"
8
+ @click.stop.prevent="onClick"
9
+ >
10
+ <itf-icon name="trash" />
11
+ </itf-button>
12
+ </slot>
13
+
14
+ <itf-modal :visible.sync="isModalShown" @keyup.native.enter="onConfirm">
15
+ <div slot="content" class="modal-content rounded-3 shadow itf-append-context">
16
+ <div class="modal-body p-4 text-center">
17
+ <slot>
18
+ <h5 class="mb-0">{{$t('components.popover.confirmDelete')}}</h5>
19
+ <p class="mb-0">{{$t('components.popover.areYouSureToDeleteThis')}}</p>
20
+ </slot>
21
+ </div>
22
+ <div class="modal-footer flex-nowrap p-0">
23
+ <button
24
+ type="button"
25
+ class="btn btn-lg btn-link fs-6 text-decoration-none col-6 py-3 m-0 rounded-0"
26
+ data-bs-dismiss="modal"
27
+ @click="$emit('cancel')"
28
+ >
29
+ <span v-html="$t('components.popover.noKeepIt')"></span>
30
+ <kbd>Esc</kbd>
31
+ </button>
32
+ <button
33
+ type="button"
34
+ class="btn btn-lg btn-link fs-6 text-decoration-none col-6 py-3 m-0 rounded-0 border-end fw-bold"
35
+ :class="confirmClass"
36
+ @click="onConfirm"
37
+ >
38
+ <span v-html="$t('components.popover.yesDelete')"></span>
39
+ <kbd>↵</kbd>
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ </itf-modal>
45
+ </div>
46
+ </template>
47
+ <script>
48
+ import { Vue, Component, Prop } from 'vue-property-decorator';
49
+ import itfIcon from '../icon/Icon';
50
+ import itfButton from '../button/Button';
51
+ import itfModal from './Modal.vue';
52
+
53
+ export default @Component({
54
+ components: {
55
+ itfIcon,
56
+ itfButton,
57
+ itfModal
58
+ }
59
+ })
60
+ class itfDeleteConfirmModal extends Vue {
61
+ @Prop(Boolean) loading;
62
+ @Prop({ type: Boolean, default: false }) disabled;
63
+ @Prop({ type: String, default: 'text-danger' }) confirmClass;
64
+ @Prop({ type: String, default () { return this.$t('noKeepIt'); } }) cancelCaption;
65
+ @Prop({ type: String, default () { return this.$t('yesDelete'); } }) deleteCaption;
66
+
67
+ isModalShown = false;
68
+
69
+ onClick() {
70
+ this.isModalShown = true;
71
+ }
72
+
73
+ onConfirm() {
74
+ this.isModalShown = false;
75
+ this.$emit('delete');
76
+ }
77
+ }
78
+ </script>
@@ -0,0 +1,219 @@
1
+ <template>
2
+ <itf-modal
3
+ ref="modal"
4
+ :data-test="dataTest"
5
+ append-to-body
6
+ :visible="value"
7
+ persistent
8
+ close-icon
9
+ :fullscreen="fullscreen"
10
+ :title="title"
11
+ content
12
+ :size="size"
13
+ @keyup.native.enter="submitSaveItem()"
14
+ @update:visible="$emit('input', $event)"
15
+ @close="onClose"
16
+ >
17
+ <itf-alert
18
+ v-if="showError"
19
+ outlined
20
+ type="danger"
21
+ dismissible
22
+ :message="errorMessage"
23
+ />
24
+
25
+ <itf-form v-if="showContent" ref="form">
26
+ <slot />
27
+ </itf-form>
28
+
29
+ <slot name="after-body" />
30
+
31
+ <template #footer>
32
+ <slot name="footer">
33
+ <itf-delete-confirm-popover v-if="canDelete" :loading="loading" @delete="deleteItem">
34
+ <template #activator="{ on }">
35
+ <itf-button color="outline-danger" v-on="on">
36
+ {{ $t('components.modal.delete') }}
37
+ </itf-button>
38
+ </template>
39
+ </itf-delete-confirm-popover>
40
+ <slot name="help"></slot>
41
+
42
+ <div class="itf-spacer" />
43
+
44
+ <itf-button
45
+ v-if="!readonly"
46
+ squircle
47
+ data-test="item-editor-cancel"
48
+ :disabled="loading"
49
+ secondary
50
+ class="me-2"
51
+ @click="$emit('input', false)"
52
+ >
53
+ <span class="me-1">{{ $t('components.modal.cancel') }}</span>
54
+ <kbd>Esc</kbd>
55
+ </itf-button>
56
+
57
+ <slot
58
+ name="save-btn"
59
+ :loading="loading"
60
+ >
61
+ <itf-button
62
+ primary
63
+ squircle
64
+ data-test="item-editor-save"
65
+ :disabled="loading"
66
+ :loading="loading"
67
+ @click="submitSaveItem()"
68
+ >
69
+ <span class="me-1">{{ saveBtnCaption }}</span>
70
+ <kbd>↵</kbd>
71
+ </itf-button>
72
+ </slot>
73
+ </slot>
74
+ </template>
75
+ </itf-modal>
76
+ </template>
77
+ <script>
78
+ import { Vue, Component, Model, Prop, Watch, Provide } from 'vue-property-decorator';
79
+ import itfAlert from '../alert/Alert';
80
+ import itfForm from '../form/Form';
81
+ import itfModal from '../modal/Modal';
82
+ import itfButton from '../button/Button';
83
+ import itfIcon from '../icon/Icon';
84
+ import itfDeleteConfirmPopover from '../popover/DeleteConfirmPopover.vue';
85
+
86
+ export default @Component({
87
+ components: {
88
+ itfIcon,
89
+ itfAlert,
90
+ itfForm,
91
+ itfModal,
92
+ itfButton,
93
+ itfDeleteConfirmPopover
94
+ }
95
+ })
96
+ class itfItemEditor extends Vue {
97
+ @Provide('itemEditorForm')
98
+ getForm () {
99
+ return this.$refs.form;
100
+ }
101
+
102
+ @Model('input') value;
103
+ @Prop({ type: String, default () { return this.$t('components.modal.itemHasBeenSuccessfullySaved'); } }) successMessage;
104
+ @Prop({ type: String, default () { return this.$t('components.modal.itemHasBeenSuccessfullyDeleted'); } }) successDeleteMessage;
105
+ @Prop() saveFunc;
106
+ @Prop() deleteFunc;
107
+ @Prop() dataTest;
108
+ @Prop(String) title;
109
+ @Prop({ type: String, default () { return this.$t('components.modal.save'); } }) saveBtnCaption;
110
+ @Prop({ type: String, default: 'lg' }) size;
111
+ @Prop(Boolean) canDelete;
112
+ @Prop(Boolean) fullscreen;
113
+ @Prop(Boolean) readonly; // показує форму, але кнопка Save не викликає saveFunc
114
+ @Prop() createdAt;
115
+ @Prop() creator;
116
+
117
+ loading = false;
118
+ showError = false;
119
+ showContent = false;
120
+
121
+ errorMessage = null;
122
+
123
+ details = {};
124
+
125
+ mounted () {
126
+ this.onShowChange();
127
+ }
128
+
129
+ resetValidation () {
130
+ this.$refs.form.resetValidation();
131
+ }
132
+
133
+ reassignFormInputs (form) {
134
+ const inputs = [];
135
+ // Copied from VForm's previous life* which had a getInputs() function
136
+ const search = (children, depth = 0) => {
137
+ for (let index = 0; index < children.length; index++) {
138
+ const child = children[index];
139
+ if (child.errorBucket !== undefined) { inputs.push(child); } else { search(child.$children, depth + 1); }
140
+ }
141
+ if (depth === 0) { return inputs; }
142
+ };
143
+ search(form.$children);
144
+ form.components = inputs;
145
+ }
146
+
147
+ async submitSaveItem (successCallback) {
148
+ if (!this.$refs.form) {
149
+ return;
150
+ }
151
+ this.reassignFormInputs(this.$refs.form); // це потрібно щоб працювала валідація вкладених v-form, наприклад, кастомні поля
152
+
153
+ if (!this.readonly && !this.$refs.form.doValidation()) {
154
+ this.$refs.modal.scrollModalTop();
155
+ return;
156
+ }
157
+
158
+ this.loading = true;
159
+ this.showError = false;
160
+
161
+ try {
162
+ if (!this.readonly) {
163
+ const data = await this.saveFunc();
164
+ if (this.successMessage) {
165
+ this.$showSuccess(this.successMessage);
166
+ }
167
+ setTimeout(() => {
168
+ this.$emit('saved', data);
169
+ if (typeof successCallback === 'function') {
170
+ successCallback();
171
+ }
172
+ }, 600); // modal animation 500 + 100 delay
173
+ }
174
+ this.$emit('input', false);
175
+ } catch (err) {
176
+ this.$emit('save-error');
177
+ this.errorMessage = err.message || this.$t('components.modal.networkProblem');
178
+ this.$refs.modal && this.$refs.modal.scrollModalTop();
179
+ this.$showError(this.$t('components.modal.pleaseFixTheErrorsBelowToProceed'));
180
+
181
+ this.showError = true;
182
+ }
183
+ this.loading = false;
184
+ }
185
+
186
+ async deleteItem () {
187
+ this.loading = true;
188
+ this.showError = false;
189
+
190
+ try {
191
+ const data = await this.deleteFunc();
192
+
193
+ this.$emit('deleted', data);
194
+
195
+ this.$emit('input', false);
196
+
197
+ this.$showSuccess(this.successDeleteMessage);
198
+ } catch (err) {
199
+ this.errorMessage = err.message || this.$t('components.modal.networkProblem');
200
+
201
+ this.showError = true;
202
+ }
203
+ this.loading = false;
204
+ }
205
+
206
+ @Watch('value')
207
+ onShowChange () {
208
+ this.$nextTick(() => this.$refs.form && this.$refs.form.resetValidation());
209
+ this.showError = false;
210
+ if (this.value) {
211
+ this.showContent = true;
212
+ }
213
+ }
214
+
215
+ onClose() {
216
+ this.showContent = false;
217
+ }
218
+ }
219
+ </script>
@@ -5,20 +5,22 @@
5
5
  class="modal-dialog"
6
6
  :class="{[`modal-${size}`]: size, 'modal-fullscreen': fullscreen, 'modal-fullscreen-sm-down': !fullscreen}"
7
7
  >
8
- <div class="modal-content itf-append-context" ref="content" @click.stop>
9
- <div class="modal-header">
10
- <slot name="title">
11
- <h5 class="modal-title" :id="modalId">{{title}}</h5>
12
- </slot>
13
- <itf-button icon @click="close" :aria-label="$t('components.close')" class="btn-close"></itf-button>
8
+ <slot name="content">
9
+ <div class="modal-content itf-append-context" ref="content" @click.stop>
10
+ <div v-if="title" class="modal-header">
11
+ <slot name="title">
12
+ <h5 class="modal-title" :id="modalId">{{title}}</h5>
13
+ </slot>
14
+ <itf-button icon @click="close" :aria-label="$t('components.close')" class="btn-close"></itf-button>
15
+ </div>
16
+ <div class="modal-body" v-if="value">
17
+ <slot></slot>
18
+ </div>
19
+ <div class="modal-footer" v-if="$slots.footer">
20
+ <slot name="footer"></slot>
21
+ </div>
14
22
  </div>
15
- <div class="modal-body" v-if="value">
16
- <slot></slot>
17
- </div>
18
- <div class="modal-footer" v-if="$slots.footer">
19
- <slot name="footer"></slot>
20
- </div>
21
- </div>
23
+ </slot>
22
24
  </div>
23
25
  </div>
24
26
 
@@ -1,9 +1,10 @@
1
1
  import { storiesOf } from '@storybook/vue';
2
2
  import itfModal from './Modal.vue';
3
+ import itfDeleteConfirmModal from './DeleteConfirmModal.vue';
4
+ import itfItemEditor from './ItemEditor.vue';
3
5
  import itfForm from '../form/Form.vue';
6
+ import itfTextField from '../text-field/TextField.vue';
4
7
  import itfLabel from '../form/Label.vue';
5
- import itfDatePicker from '../datepicker/DatePicker.vue';
6
- import itfDateRangePicker from '../datepicker/DateRangePicker.vue';
7
8
  import itfApp from '../app/App.vue';
8
9
 
9
10
  storiesOf('Common', module)
@@ -13,12 +14,14 @@ storiesOf('Common', module)
13
14
  itfForm,
14
15
  itfLabel,
15
16
  itfModal,
16
- itfDatePicker,
17
- itfDateRangePicker
17
+ itfTextField,
18
+ itfDeleteConfirmModal,
19
+ itfItemEditor
18
20
  },
19
21
  data() {
20
22
  return {
21
23
  isVisible: false,
24
+ isItemEdit: false,
22
25
  from: '2021-01-16',
23
26
  to: null,
24
27
  dates: [],
@@ -29,7 +32,7 @@ storiesOf('Common', module)
29
32
  },
30
33
  methods: {
31
34
  submit() {
32
- this.$refs.form.doValidation();
35
+ console.info('saved');
33
36
  },
34
37
  reset() {
35
38
  this.$refs.form.resetValidation();
@@ -42,17 +45,19 @@ storiesOf('Common', module)
42
45
 
43
46
  <pre>
44
47
 
45
- &lt;itf-datepicker>&lt;/itf-datepicker>
46
-
47
- &lt;button @click="$showSuccess('Success')">Show success&lt;/button>
48
- &lt;button @click="$showError('Error')">Show error&lt;/button>
49
- &lt;button @click="$try(() => { throw new Error('Invalid function'); })">Try function with error&lt;/button>
50
48
  </pre>
51
49
 
52
50
  <h2>Example</h2>
53
51
 
54
52
  <itf-app ref="app">
55
53
 
54
+ <itf-delete-confirm-modal></itf-delete-confirm-modal>
55
+
56
+ <button @click="isItemEdit = !isItemEdit" class="btn btn-primary" type="button">Show item edit</button>
57
+ <itf-item-editor v-model="isItemEdit" :save-func="submit" title="Edit item">
58
+ <itf-text-field />
59
+ </itf-item-editor>
60
+
56
61
  <button @click="isVisible = !isVisible" class="btn btn-primary" type="button">Show modal</button>
57
62
  {{isVisible}}
58
63
  <itf-modal title="Test" :visible.sync="isVisible">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <itf-popover :visible.sync="value" ref="popover" :trigger="trigger" custom-class="nopadding">
2
+ <itf-popover :visible.sync="value" ref="popover" :trigger="trigger" custom-class="nopadding" :placement="placement">
3
3
  <template #activator="{ on }">
4
4
  <slot name="activator" :on="on"></slot>
5
5
  </template>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <itf-confirm-popover :cancel-caption="cancelCaption" :confirm-class="confirmClass" :confirm-caption="deleteCaption" @confirm="$emit('delete')">
3
+ <template #activator="{ on }">
4
+ <slot name="activator" :on="on" :loading="loading">
5
+ <itf-button
6
+ icon
7
+ :disabled="loading || disabled"
8
+ :loading="loading"
9
+ v-on="on"
10
+ @click.stop.prevent
11
+ >
12
+ <itf-icon name="trash" />
13
+ </itf-button>
14
+ </slot>
15
+ </template>
16
+ <slot>{{$t('components.popover.areYouSureToDeleteThis')}}</slot>
17
+ </itf-confirm-popover>
18
+ </template>
19
+ <script>
20
+ import { Vue, Component, Prop } from 'vue-property-decorator';
21
+ import itfIcon from '../icon/Icon';
22
+ import itfButton from '../button/Button';
23
+ import itfConfirmPopover from './ConfirmPopover';
24
+
25
+ export default @Component({
26
+ components: {
27
+ itfIcon,
28
+ itfButton,
29
+ itfConfirmPopover
30
+ }
31
+ })
32
+ class itfDeletePopover extends Vue {
33
+ @Prop(Boolean) loading;
34
+ @Prop({ type: Boolean, default: false }) disabled;
35
+ @Prop({ type: String, default: 'text-danger' }) confirmClass;
36
+ @Prop({ type: String, default () { return this.$t('components.popover.noKeepIt'); } }) cancelCaption;
37
+ @Prop({ type: String, default () { return this.$t('components.popover.yesDelete'); } }) deleteCaption;
38
+ }
39
+ </script>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <itf-popover :visible.sync="value" ref="popover" :trigger="trigger" custom-class="nopadding" :placement="placement">
3
+ <template #activator="{ on }">
4
+ <slot name="activator" :on="on"></slot>
5
+ </template>
6
+
7
+ <div class="modal-body py-2 px-3 text-center itf-select-popover">
8
+ <div class="d-flex align-items-center justify-content-between">
9
+ <div class="mb-0" v-if="title" v-html="title"></div>
10
+
11
+ <itf-button v-if="removable" small secondary @click="onSelectIcon(null)">
12
+ Remove
13
+ </itf-button>
14
+ </div>
15
+
16
+ <div class="d-flex py-2">
17
+ <itf-text-field
18
+ class="flex-grow-1 me-2"
19
+ v-model="search"
20
+ placeholder="Filter"
21
+ prepend-icon="search"
22
+ clearable
23
+ />
24
+
25
+ <itf-button v-tooltip="$t('random')" @click="onRandom">
26
+ <itf-icon name="shuffle" />
27
+ </itf-button>
28
+ </div>
29
+
30
+ <div class="itf-select-popover__scroll text-start">
31
+ <template v-if="!search && recentIcons.length">
32
+ <div class="py-1 text-muted">Recent</div>
33
+ <div class="d-flex align-items-start flex-wrap flex-grow-0">
34
+ <itf-button v-for="icon of recentIcons" :key="icon" icon v-tooltip="icon" @click="onSelectIcon(icon)">
35
+ <itf-icon :name="icon" />
36
+ </itf-button>
37
+ </div>
38
+
39
+ <div class="py-1 text-muted">Icons</div>
40
+ </template>
41
+ <div class="d-flex align-items-start flex-wrap flex-grow-0">
42
+ <itf-button v-for="icon of iconsList" :key="icon" icon v-tooltip="icon" @click="onSelectIcon(icon)">
43
+ <itf-icon :name="icon" />
44
+ </itf-button>
45
+ </div>
46
+ <div v-if="!iconsList.length" class="text-muted pt-1">
47
+ No results
48
+ </div>
49
+ </div>
50
+ <slot>
51
+ </slot>
52
+ </div>
53
+ </itf-popover>
54
+ </template>
55
+ <style lang="scss">
56
+ .itf-select-popover {
57
+ width: 280px;
58
+ min-width: 180px;
59
+ max-width: calc(100vw - 24px);
60
+ height: 356px;
61
+ max-height: 70vh;
62
+ overflow: hidden;
63
+ display: flex;
64
+ flex-direction: column;
65
+
66
+ &__scroll {
67
+ overflow: hidden auto;
68
+ flex-grow: 1;
69
+ }
70
+ }
71
+ </style>
72
+ <script>
73
+ import { Vue, Component, Prop, PropSync } from 'vue-property-decorator';
74
+ import itfPopover from './Popover';
75
+ import itfIcon from '../icon/Icon';
76
+ import iconsList from '../icon/icons';
77
+ import itfButton from '../button/Button';
78
+ import itfTextField from '../text-field/TextField.vue';
79
+ import tooltip from '../../directives/tooltip';
80
+
81
+ const RECENT_ITEMS_KEY = 'itfSelectPopoverRecent';
82
+
83
+ export default @Component({
84
+ name: 'itfSelectPopover',
85
+ directives: {
86
+ tooltip
87
+ },
88
+ components: {
89
+ itfIcon,
90
+ itfPopover,
91
+ itfButton,
92
+ itfTextField
93
+ },
94
+ })
95
+ class itfSelectPopover extends Vue {
96
+ @PropSync('visible') value;
97
+ @Prop({ type: String, default: '' }) title;
98
+ @Prop({ type: Boolean, default: '' }) removable;
99
+ @Prop({ type: String, default: 'bottom', validator: (value) => ['bottom', 'left', 'right', 'top'].includes(value) }) placement;
100
+ @Prop({ type: String, default: 'click', validator: (value) => ['click', 'focus', 'hover', 'manual'].includes(value) }) trigger;
101
+
102
+ search = '';
103
+ recentIcons = [];
104
+
105
+ created() {
106
+ this.recentIcons = this.getRecentIcons();
107
+ }
108
+
109
+ get iconsList() {
110
+ return Object.keys(iconsList).filter((icon) => icon.includes(this.search.toLowerCase()));
111
+ }
112
+
113
+ onSelectIcon(icon) {
114
+ this.search = '';
115
+ this.$emit('input', icon);
116
+ this.$refs.popover.hide();
117
+
118
+ if (icon) {
119
+ const icons = this.getRecentIcons();
120
+ icons.unshift(icon);
121
+ this.recentIcons = [...new Set(icons.slice(0, 14))];
122
+ localStorage.setItem(RECENT_ITEMS_KEY, JSON.stringify(this.recentIcons));
123
+ }
124
+ }
125
+
126
+ getRecentIcons() {
127
+ return localStorage.getItem(RECENT_ITEMS_KEY) ? JSON.parse(localStorage.getItem(RECENT_ITEMS_KEY)) : [];
128
+ }
129
+
130
+ onRandom() {
131
+ const keys = Object.keys(iconsList);
132
+ const icon = keys[Math.floor(Math.random() * keys.length)];
133
+ this.onSelectIcon(icon);
134
+ }
135
+ }
136
+ </script>