@itfin/components 1.2.86 → 1.2.88

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itfin/components",
3
- "version": "1.2.86",
3
+ "version": "1.2.88",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -1,5 +1,7 @@
1
1
  @import '../variables';
2
2
 
3
3
  .itf-icon {
4
- display: inline-block;
4
+ display: inline-flex;
5
+ align-items: center;
6
+ vertical-align: middle;
5
7
  }
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="itf-inline-edit">
3
+ <div ref="input" class="input" :role="editable ? 'button' : 'textbox'" tabindex="1" @keydown.space="onEnter" @keydown.enter="onEnter" @focus="$emit('focus')" @blur="$emit('blur')">
4
+ <span v-if="value">{{ value | formatDate }}</span>
5
+ <span v-else class="text-muted">Empty</span>
6
+ </div>
7
+
8
+ <div style="display: none">
9
+ <div ref="dropdown" class="itf-datepicker__dropdown">
10
+ <itf-date-picker-inline
11
+ :value="value"
12
+ start-view="days"
13
+ display-format="MM/dd/yyyy"
14
+ value-format="yyyy-MM-dd"
15
+ @input="selectInlineDate"
16
+ ></itf-date-picker-inline>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+ <style lang="scss" scoped>
22
+ .itf-inline-edit .input {
23
+ border: 0 none;
24
+ margin: 0;
25
+ outline: 0;
26
+ resize: none;
27
+ background: transparent;
28
+ }
29
+ </style>
30
+ <script>
31
+ import {Vue, Component, Model, Watch, Prop} from 'vue-property-decorator';
32
+ import itfTextField from '../../text-field/TextField.vue';
33
+ import itfDatePickerInline from '../../datepicker/DatePickerInline.vue';
34
+ import tippy from 'tippy.js';
35
+ import { formatDate } from '../../../helpers/formatters';
36
+ import PropertyInlineEdit from "@/components/customize/PropertyInlineEdit.vue";
37
+
38
+ export default @Component({
39
+ name: 'itfCustomizeInlineDate',
40
+ components: {
41
+ PropertyInlineEdit,
42
+ itfTextField,
43
+ itfDatePickerInline
44
+ },
45
+ filters: {
46
+ formatDate
47
+ }
48
+ })
49
+
50
+ class itfCustomizeInlineText extends Vue {
51
+ @Model('input') value;
52
+ @Prop({ type: String, default: 'bottom-start' }) placement;
53
+ @Prop({ type: Boolean, default: false }) editable;
54
+
55
+ tooltip = null;
56
+
57
+ mounted() {
58
+ if (!this.editable) {
59
+ return;
60
+ }
61
+ const context = this.$el.closest('.itf-append-context') || document.body;
62
+ this.tooltip = tippy(this.$refs.input, {
63
+ interactiveBorder: 30,
64
+ interactiveDebounce: 75,
65
+ animation: 'scale',
66
+ arrow: true,
67
+ content: this.$refs.dropdown,
68
+ allowHTML: true,
69
+ trigger: 'click',
70
+ interactive: true,
71
+ placement: this.placement,
72
+ appendTo: context
73
+ });
74
+ }
75
+
76
+ selectInlineDate(date) {
77
+ this.$emit('input', date);
78
+ this.tooltip.hide();
79
+ this.isFocused = false;
80
+ }
81
+
82
+ onEnter() {
83
+ this.tooltip.show();
84
+ }
85
+ }
86
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="itf-inline-edit">
3
+ <itf-select
4
+ ref="input"
5
+ class="input"
6
+ role="button"
7
+ multiple
8
+ :placeholder="focused ? 'Search for an option...' : 'Empty'"
9
+ :tabindex="1"
10
+ :disabled="!editable"
11
+ :options="field && field.Options"
12
+ :reduce="option => option.Name"
13
+ :get-option-label="option => option.Name"
14
+ @search:focus="$emit('focus')"
15
+ @search:blur="$emit('blur')">
16
+ <template #option="{ option }">
17
+ <div>{{ option.Name }}</div>
18
+ </template>
19
+ </itf-select>
20
+ </div>
21
+ </template>
22
+ <style lang="scss">
23
+ .itf-inline-edit .itf-select .form-control {
24
+ border: 0 none;
25
+ margin: 0;
26
+ outline: 0;
27
+ resize: none;
28
+ min-height: auto;
29
+ padding: 0.1rem 0;
30
+ font-family: var(--bs-body-font-family);
31
+ font-size: var(--bs-body-font-size);
32
+ box-shadow: none !important;
33
+ background: transparent !important;
34
+ }
35
+ </style>
36
+ <script>
37
+ import {Vue, Component, Model, Watch, Prop} from 'vue-property-decorator';
38
+ import itfTextField from '../../text-field/TextField.vue';
39
+ import itfSelect from '../../select/Select.vue';
40
+
41
+ export default @Component({
42
+ name: 'itfCustomizeInlineMultiselect',
43
+ components: {
44
+ itfTextField,
45
+ itfSelect
46
+ }
47
+ })
48
+
49
+ class itfCustomizeInlineMultiselect extends Vue {
50
+ @Model('input') value;
51
+ @Prop(Boolean) focused;
52
+ @Prop(Boolean) editable;
53
+ @Prop() field;
54
+ }
55
+ </script>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <div class="itf-inline-edit">
3
+ <div
4
+ :role="editable ? 'button' : 'textbox'"
5
+ tabindex="1"
6
+ ref="input"
7
+ @focus="onFocus"
8
+ @blur="onBlur"
9
+ @keydown.enter="onEnter"
10
+ @keydown.esc="onCancel"
11
+ class="notranslate"
12
+ spellcheck="true"
13
+ placeholder=" "
14
+ :contenteditable="editable"
15
+ v-html="value">
16
+ </div>
17
+ </div>
18
+ </template>
19
+ <style lang="scss" scoped>
20
+ .itf-inline-edit {
21
+ margin: -5px;
22
+ padding: 5px;
23
+ }
24
+ .notranslate {
25
+ border: 0 none;
26
+ margin: 0;
27
+ outline: 0;
28
+ resize: none;
29
+ background: transparent;
30
+ max-width: 100%;
31
+ width: 100%;
32
+ white-space: pre-wrap;
33
+ word-break: break-word;
34
+ caret-color: var(--bs-body-color);
35
+ }
36
+ </style>
37
+ <script>
38
+ import { Vue, Component, Model, Prop } from 'vue-property-decorator';
39
+ import itfTextField from '../../text-field/TextField.vue';
40
+ import PropertyInlineEdit from "@/components/customize/PropertyInlineEdit.vue";
41
+
42
+ export default @Component({
43
+ name: 'itfCustomizeInlineText',
44
+ components: {
45
+ PropertyInlineEdit,
46
+ itfTextField
47
+ }
48
+ })
49
+
50
+ class itfCustomizeInlineText extends Vue {
51
+ @Model('input') value;
52
+ @Prop(Boolean) editable;
53
+
54
+ mounted() {
55
+ this.$refs.input.addEventListener('paste', function(e) {
56
+ // cancel paste
57
+ e.preventDefault();
58
+
59
+ // get text representation of clipboard
60
+ var text = (e.originalEvent || e).clipboardData.getData('text/plain');
61
+
62
+ // insert text manually
63
+ document.execCommand("insertHTML", false, text);
64
+ });
65
+ }
66
+
67
+ onEnter(event) {
68
+ if (event.shiftKey) {
69
+ return;
70
+ }
71
+ event.preventDefault();
72
+ event.stopPropagation();
73
+
74
+ this.$refs.input.blur();
75
+ }
76
+
77
+ onBlur() {
78
+ this.$emit('input', this.$refs.input.innerHTML);
79
+ this.$emit('blur');
80
+ }
81
+
82
+ onCancel(event) {
83
+ event.target.value = this.value;
84
+ this.$refs.input.blur();
85
+ }
86
+
87
+ onFocus() {
88
+ this.$emit('focus');
89
+ }
90
+ }
91
+ </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-append-context">
2
+ <div class="itf-append-context" style="min-width: 300px">
3
3
  <div class="d-flex align-items-center px-3 py-2">
4
4
  <div class="me-1">
5
5
  <itf-icon-popover title="Icon" removable :value="value.Icon" @input="setParam({ Icon: $event })">
@@ -10,20 +10,77 @@
10
10
  </div>
11
11
 
12
12
  <div class="flex-grow-1">
13
- <itf-text-field :value="value.Name" @input="setParam({ Name: $event })" />
13
+ <itf-text-field ref="input" :value="value.Name" @input="setParam({ Name: $event })" />
14
14
  </div>
15
15
  </div>
16
16
  <div>
17
17
  <a class="dropdown-item d-flex justify-content-between" @click.prevent="$emit('edit')">
18
18
  <span>{{ $t('components.customize.type') }}</span>
19
- <span class="text-muted">{{ $t('components.customize.select') }} ></span>
19
+ <span class="text-muted">{{ itemType }} ></span>
20
20
  </a>
21
+ <ul class="itf-dropdown__menu shadow dropdown-menu dropdown-submenu">
22
+ <li><h6 class="dropdown-header">{{ $t('components.customize.type') }}</h6></li>
23
+ <li v-for="(type) of types">
24
+ <a class="dropdown-item d-flex align-items-center" href="" @click.prevent="setParam({ Type: type.Type })">
25
+ <itf-icon :name="type.Icon" :size="16" class="me-1" />
26
+ <span v-text="type.Name"></span>
27
+ <itf-icon v-if="value.Type === type.Type" name="check" />
28
+ </a>
29
+ </li>
30
+ </ul>
21
31
  </div>
32
+ <div v-if="value.Type === 'multiselect'" sortable-skip>
33
+ <div class="d-flex justify-content-between pe-2">
34
+ <h6 class="dropdown-header">{{ $t('components.customize.options') }}</h6>
35
+ <div v-if="value.Options && value.Options.length">
36
+ <itf-button icon small @click.prevent="addNewOption">
37
+ <itf-icon name="plus" />
38
+ </itf-button>
39
+ </div>
40
+ </div>
41
+
42
+ <div v-if="isNewOptionVisible" class="px-3">
43
+ <itf-text-field
44
+ ref="newOptionInput"
45
+ v-model="newOption"
46
+ placeholder="Type a new option..."
47
+ @blur="onBlurOption"
48
+ @keydown.enter="submitNewOption"
49
+ />
50
+ </div>
51
+ <div v-else-if="!value.Options || !value.Options.length" class="px-3">
52
+ <itf-button small block class="text-start" @click="addNewOption">
53
+ <itf-icon name="plus" />
54
+ {{ $t('components.customize.addOption') }}
55
+ </itf-button>
56
+ </div>
57
+
58
+ <sortable
59
+ :value="value.Options"
60
+ @input="setParam({ Options: $event })"
61
+ axis="y"
62
+ require-sortable-attribute
63
+ auto-scroll="window"
64
+ >
65
+ <div v-for="option of value.Options" class="dropdown-item d-flex justify-content-between" sortable>
66
+ <div>
67
+ <itf-icon class="dragHandle" name="drag_vertical" />
68
+ <span>{{ option.Name }}</span>
69
+ </div>
70
+ <span class="text-muted">></span>
71
+ </div>
72
+ </sortable>
73
+ </div>
74
+ <div><hr class="dropdown-divider"></div>
75
+ <div><a class="dropdown-item" @click.prevent="$emit('duplicate')"><itf-icon name="duplicate" /> {{ $t('components.customize.duplicateProperty') }}</a></div>
22
76
  <div>
23
- <a class="dropdown-item d-flex justify-content-between" @click.prevent="$emit('edit')">
24
- <span>{{ $t('components.customize.numberFormat') }}</span>
25
- <span class="text-muted">{{ $t('components.customize.money') }} ></span>
26
- </a>
77
+ <itf-delete-confirm-modal class="me-1" @delete="$emit('delete')">
78
+ <template #activator="{ on }">
79
+ <a class="dropdown-item text-danger" @click.prevent="on.click"><itf-icon name="trash" /> {{ $t('components.customize.deleteProperty') }}</a>
80
+ </template>
81
+ <h5 class="mb-0">{{$t('components.popover.confirmDelete')}}</h5>
82
+ <p class="mb-0">{{ $t('components.customize.areYouSureYouWantToDeleteThisField') }}</p>
83
+ </itf-delete-confirm-modal>
27
84
  </div>
28
85
  </div>
29
86
  </template>
@@ -32,22 +89,67 @@ import { Vue, Component, Model } from 'vue-property-decorator';
32
89
  import itfTextField from '../text-field/TextField.vue';
33
90
  import itfIcon from '../icon/Icon';
34
91
  import itfButton from '../button/Button';
92
+ import Sortable from '../sortable/Sortable';
35
93
  import itfIconPopover from '../popover/IconPopover.vue';
94
+ import {INLINE_TYPES} from "@/components/customize/constants";
95
+ import itfDeleteConfirmModal from "@/components/modal/DeleteConfirmModal.vue";
96
+ import PropertyItem from "@/components/customize/PropertyItem.vue";
36
97
 
37
98
  export default @Component({
38
99
  components: {
100
+ PropertyItem,
101
+ itfDeleteConfirmModal,
39
102
  itfButton,
40
103
  itfIcon,
41
104
  itfTextField,
42
- itfIconPopover
105
+ itfIconPopover,
106
+ Sortable
43
107
  }
44
108
  })
45
109
 
46
110
  class PropertiesEditMenu extends Vue {
47
111
  @Model('input') value;
48
112
 
113
+ isNewOptionVisible = false;
114
+ newOption = '';
115
+
116
+ get types() {
117
+ return INLINE_TYPES;
118
+ }
119
+
120
+ get itemType() {
121
+ const type = INLINE_TYPES.find((type) => type.Type === this.value.Type);
122
+ return type ? type.Name : '';
123
+ }
124
+
125
+ mounted() {
126
+ this.$nextTick(() => {
127
+ this.$refs.input.focus();
128
+ });
129
+ }
130
+
49
131
  setParam(obj) {
50
132
  this.$emit('input', { ...this.value, ...obj });
51
133
  }
134
+
135
+ onBlurOption() {
136
+ this.isNewOptionVisible = false;
137
+ }
138
+
139
+ addNewOption() {
140
+ this.isNewOptionVisible = true;
141
+ this.$nextTick(() => {
142
+ this.$refs.newOptionInput.focus();
143
+ });
144
+ }
145
+
146
+ submitNewOption() {
147
+ const newOption = this.newOption;
148
+ if (newOption) {
149
+ this.setParam({ Options: [{ Name: newOption }, ...(this.value.Options || [])] });
150
+ this.newOption = '';
151
+ this.isNewOptionVisible = false;
152
+ }
153
+ }
52
154
  }
53
155
  </script>
@@ -4,55 +4,37 @@
4
4
  require-sortable-attribute
5
5
  v-model="list"
6
6
  axis="y"
7
+ :read-only="!editable"
7
8
  auto-scroll="window"
8
9
  >
9
- <div v-for="(field, n) of list" :key="n" class="b-properties-list__inner">
10
- <div class="b-properties-list__name">
11
- <div class="b-properties-list__icon">
12
- <div role="button" tabindex="-1" class="b-properties-list__draghandler" sortable>
13
- <itf-icon class="dragHandle" name="drag_vertical" />
14
- </div>
15
- </div>
16
-
17
- <itf-dropdown autoclose="outside" class="flex-grow-1" shadow :button-options="{ icon: true, block: true }" @close="onClose">
18
- <template #button>
19
- <div class="d-flex align-items-center">
20
- <itf-icon :name="field.Icon" :size="24" class="me-1" />
21
-
22
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" v-text="field.Name" />
23
- </div>
24
- </template>
25
- <properties-edit-menu
26
- v-if="isEditMode"
27
- sortable-skip
28
- :value="field"
29
- :loading="loading"
30
- @input="onChange($event, n)"
31
- />
32
- <properties-popup-menu
33
- v-else
34
- sortable-skip
35
- :field="field"
36
- :loading="loading"
37
- @edit="isEditMode = true"
38
- />
39
- </itf-dropdown>
40
- </div>
41
- <div class="b-properties-list__value" sortable-skip>
42
- <div role="button" tabindex="1">
43
- <property-inline-edit :value="value[field.Id]" @input="onValueSet(field, $event)" />
44
- <span v-if="value && value[field.Id]">{{ value[field.Id] }}</span>
45
- <span v-else class="text-muted"> ({{ $t('notSet') }})</span>
46
- </div>
47
- </div>
48
- </div>
10
+ <property-item
11
+ v-for="(field, n) of list"
12
+ :key="n"
13
+ ref="properties"
14
+ :field="field"
15
+ :editable="editable"
16
+ :value="value[field.Id]"
17
+ @input="$emit('input', { ...value, [field.Id]: $event })"
18
+ @delete="onDelete(field)"
19
+ @duplicate="onDuplicate(field)"
20
+ @update:field="onChange($event, n)"
21
+ />
49
22
  </sortable>
50
23
 
51
- <div class="text-start">
52
- <itf-button class="text-muted" small>
53
- <itf-icon name="plus" />
54
- Add property
55
- </itf-button>
24
+ <div v-if="editable" class="text-start">
25
+ <itf-dropdown ref="newItemDd" autoclose="outside" class="flex-grow-1 mw-100" shadow :button-options="{ class: 'text-muted', small: true }">
26
+ <template #button>
27
+ <itf-icon name="plus" />
28
+ Add property
29
+ </template>
30
+ <div><h6 class="dropdown-header">{{ $t('components.customize.type') }}</h6></div>
31
+ <div v-for="type of types" :keys="type.Type">
32
+ <a class="dropdown-item d-flex align-items-center" @click.prevent="createNewField(type)">
33
+ <itf-icon :name="type.Icon" :size="16" class="me-1" />
34
+ {{ type.Name }}
35
+ </a>
36
+ </div>
37
+ </itf-dropdown>
56
38
  </div>
57
39
  </div>
58
40
  </template>
@@ -79,55 +61,6 @@
79
61
  display: flex;
80
62
  flex-direction: column;
81
63
  padding: 5px 3px 5px 15px;
82
-
83
- &__draghandler {
84
- user-select: none;
85
- transition: opacity 150ms ease-in 0s;
86
- cursor: grab;
87
- display: flex;
88
- align-items: center;
89
- justify-content: center;
90
- width: 18px;
91
- height: 24px;
92
- opacity: 0;
93
- border-radius: 3px;
94
- fill: rgba(55, 53, 47, 0.35);
95
- }
96
- &__inner:hover &__draghandler {
97
- opacity: 1;
98
- }
99
- &__inner {
100
- display: flex;
101
- padding-bottom: 4px;
102
- width: 100%;
103
- }
104
- &__name, &__value {
105
- cursor: pointer;
106
- display: flex;
107
- align-items: center;
108
- height: 100%;
109
- width: 100%;
110
- border-radius: 3px;
111
- padding: 0 6px;
112
- }
113
- &__name {
114
- display: flex;
115
- align-items: center;
116
- height: 34px;
117
- width: 160px;
118
- flex: 0 0 auto;
119
- }
120
- &__icon {
121
- width: 22px;
122
- margin-left: -22px;
123
- opacity: 1;
124
- }
125
- &__value {
126
- display: flex;
127
- margin-left: 4px;
128
- height: 100%;
129
- min-width: 0;
130
- }
131
64
  }
132
65
  </style>
133
66
  <script>
@@ -138,11 +71,11 @@ import itfDropdown from '../dropdown/Dropdown.vue';
138
71
  import itfLabel from '../form/Label.vue';
139
72
  import itfTextField from '../text-field/TextField.vue';
140
73
  import Sortable from '../sortable/Sortable';
141
- import PropertiesPopupMenu from './PropertiesPopupMenu.vue';
142
- import PropertiesEditMenu from './PropertiesEditMenu.vue';
143
- import PropertyInlineEdit from './PropertyInlineEdit.vue';
74
+ import PropertyItem from './PropertyItem';
75
+ import { INLINE_TYPES } from './constants';
144
76
 
145
77
  export default @Component({
78
+ name: 'itfPropertiesList',
146
79
  components: {
147
80
  itfIcon,
148
81
  itfDropdown,
@@ -150,20 +83,17 @@ export default @Component({
150
83
  itfLabel,
151
84
  itfButton,
152
85
  Sortable,
153
- PropertiesPopupMenu,
154
- PropertiesEditMenu,
155
- PropertyInlineEdit
86
+ PropertyItem
156
87
  }
157
88
  })
158
89
  class itfPropertiesList extends Vue {
159
90
  @Model('input') value;
160
91
  @PropSync('fields') list;
161
92
  @Prop({ type: Boolean, default: false }) loading;
93
+ @Prop({ type: Boolean, default: false }) editable;
162
94
 
163
- isEditMode = false;
164
-
165
- onClose() {
166
- this.isEditMode = false;
95
+ get types() {
96
+ return INLINE_TYPES;
167
97
  }
168
98
 
169
99
  onChange(item, index) {
@@ -172,8 +102,50 @@ class itfPropertiesList extends Vue {
172
102
  this.$emit('update:fields', list);
173
103
  }
174
104
 
175
- onValueSet(item, value) {
176
- this.$emit('input', { ...this.value, [item.Id]: value });
105
+ onDelete(item) {
106
+ const list = [...this.list].filter((i) => i.Id !== item.Id);
107
+ this.$emit('update:fields', list);
108
+ }
109
+
110
+ onDuplicate(item) {
111
+ const list = [...this.list];
112
+ const newItem = { ...item };
113
+ newItem.Id = `new-${Math.random().toString(36).substr(2, 9)}`;
114
+ const index = newItem.Name.match(/\((\d+)\)$/);
115
+ // додає вкінці номер (1), (2) і т.д.
116
+ newItem.Name = newItem.Name.replace(/\s\(\d+\)$/, '');
117
+ newItem.Name = `${newItem.Name}${index ? ` (${parseInt(index[1], 10) + 1})` : ' (1)'}`;
118
+ list.push(newItem);
119
+ this.$emit('update:fields', list);
120
+ this.$refs.newItemDd.hide();
121
+
122
+ this.$nextTick(() => {
123
+ for (const prop of this.$refs.properties) {
124
+ prop.hideEditMenu();
125
+ }
126
+ setTimeout(() => {
127
+ this.$refs.properties[list.length - 1].showEditMenu();
128
+ }, 10);
129
+ });
130
+ }
131
+
132
+ createNewField(type) {
133
+ const list = [...this.list];
134
+ list.push({
135
+ Id: `new-${Math.random().toString(36).substr(2, 9)}`,
136
+ Name: type.Name,
137
+ Type: type.Type,
138
+ Icon: type.Icon,
139
+ Visible: 'show'
140
+ });
141
+ this.$emit('update:fields', list);
142
+ this.$refs.newItemDd.hide();
143
+
144
+ this.$nextTick(() => {
145
+ setTimeout(() => {
146
+ this.$refs.properties[list.length - 1].showEditMenu();
147
+ }, 10);
148
+ });
177
149
  }
178
150
  }
179
151
  </script>
@@ -5,14 +5,14 @@
5
5
  <div>
6
6
  <a class="dropdown-item" @click.prevent><itf-icon name="eye_no" /> {{ $t('components.customize.hideProperty') }}</a>
7
7
  <ul class="itf-dropdown__menu shadow dropdown-menu dropdown-submenu">
8
- <li><a class="dropdown-item" href="#">{{ $t('components.customize.alwaysShow') }}<itf-icon name="check" /></a></li>
9
- <li><a class="dropdown-item" href="#">{{ $t('components.customize.hideWhenEmpty') }}</a></li>
10
- <li><a class="dropdown-item" href="#">{{ $t('components.customize.alwaysHide') }}</a></li>
8
+ <li><a class="dropdown-item" href="" @click.prevent="$emit('visibility', 'show')">{{ $t('components.customize.alwaysShow') }}<itf-icon v-if="field.Visible === 'show'" name="check" /></a></li>
9
+ <li><a class="dropdown-item" href="" @click.prevent="$emit('visibility', 'filled')">{{ $t('components.customize.hideWhenEmpty') }}<itf-icon v-if="field.Visible === 'filled'" name="check" /></a></li>
10
+ <li><a class="dropdown-item" href="" @click.prevent="$emit('visibility', 'hide')">{{ $t('components.customize.alwaysHide') }}<itf-icon v-if="field.Visible === 'hide'" name="check" /></a></li>
11
11
  </ul>
12
12
  </div>
13
13
  <div><a class="dropdown-item" @click.prevent="$emit('duplicate')"><itf-icon name="duplicate" /> {{ $t('components.customize.duplicateProperty') }}</a></div>
14
14
  <div>
15
- <itf-delete-confirm-modal class="me-1" :loading="loading" @delete="$emit('delete')">
15
+ <itf-delete-confirm-modal class="me-1" @delete="$emit('delete')">
16
16
  <template #activator="{ on }">
17
17
  <a class="dropdown-item text-danger" @click.prevent="on.click"><itf-icon name="trash" /> {{ $t('components.customize.deleteProperty') }}</a>
18
18
  </template>
@@ -42,5 +42,6 @@ export default @Component({
42
42
  })
43
43
 
44
44
  class PropertiesEditMenu extends Vue {
45
+ @Prop() field;
45
46
  }
46
47
  </script>
@@ -1,46 +1,44 @@
1
1
  <template>
2
- <div class="itf-inline-edit" :class="{'shadow': isFocused}">
3
- <textarea @focus="onFocus" :value="value" @blur="onBlur"></textarea>
2
+ <div>
3
+ <component
4
+ v-if="component"
5
+ :is="component"
6
+ :value="value"
7
+ :field="field"
8
+ :focused="focused"
9
+ :editable="editable"
10
+ @input="$emit('input', $event)"
11
+ @focus="$emit('focus')"
12
+ @blur="$emit('blur')"
13
+ />
4
14
  </div>
5
15
  </template>
6
- <style lang="scss">
7
- .itf-inline-edit {
8
- textarea {
9
- border: 0 none;
10
- margin: 0;
11
- outline: 0;
12
- resize: none;
13
- }
14
- }
15
- </style>
16
16
  <script>
17
- import { Vue, Component, Model, Watch } from 'vue-property-decorator';
18
- import itfTextField from '../text-field/TextField.vue';
17
+ import { Vue, Component, Model, Prop } from 'vue-property-decorator';
19
18
  import itfIcon from '../icon/Icon';
20
19
  import itfButton from '../button/Button';
21
- import itfDeleteConfirmModal from '../modal/DeleteConfirmModal.vue';
20
+ import { INLINE_TYPES } from './constants';
22
21
 
23
22
  export default @Component({
23
+ name: 'itfPropertyInlineEdit',
24
24
  components: {
25
- itfButton,
26
25
  itfIcon,
27
- itfTextField,
28
- itfDeleteConfirmModal
26
+ itfButton,
29
27
  }
30
28
  })
31
29
 
32
- class PropertyInlineEdit extends Vue {
30
+ class itfPropertyInlineEdit extends Vue {
33
31
  @Model('input') value;
32
+ @Prop() field;
33
+ @Prop(Boolean) focused;
34
+ @Prop(Boolean) editable;
34
35
 
35
- isFocused = false;
36
-
37
- onBlur(event) {
38
- this.$emit('input', event.target.value);
39
- this.isFocused = false;
40
- }
41
-
42
- onFocus() {
43
- this.isFocused = true;
36
+ get component() {
37
+ if (!this.field) {
38
+ return null;
39
+ }
40
+ const type = INLINE_TYPES.find(({ Type }) => Type === this.field.Type);
41
+ return type ? type.Component : null;
44
42
  }
45
43
  }
46
44
  </script>
@@ -0,0 +1,191 @@
1
+ <template>
2
+ <div class="b-properties-list__inner">
3
+ <div class="b-properties-list__name" :class="{'editable': editable}">
4
+ <div class="b-properties-list__icon">
5
+ <div v-if="editable" role="button" tabindex="-1" class="b-properties-list__draghandler" sortable>
6
+ <itf-icon class="dragHandle" name="drag_vertical" />
7
+ </div>
8
+ </div>
9
+
10
+ <itf-dropdown :disabled="!editable" ref="editDd" autoclose="outside" class="flex-grow-1 mw-100" shadow :button-options="{ icon: true, block: true }" @close="onClose">
11
+ <template #button>
12
+ <div class="d-flex align-items-center">
13
+ <itf-icon :name="field.Icon" :size="16" class="me-1" />
14
+
15
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" v-text="field.Name" />
16
+ </div>
17
+ </template>
18
+ <properties-edit-menu
19
+ v-if="isEditMode"
20
+ sortable-skip
21
+ :value="field"
22
+ :loading="loading"
23
+ @input="onChange($event)"
24
+ @delete="$emit('delete')"
25
+ @duplicate="$emit('duplicate')"
26
+ />
27
+ <properties-popup-menu
28
+ v-else
29
+ sortable-skip
30
+ :field="field"
31
+ :loading="loading"
32
+ @edit="isEditMode = true"
33
+ @delete="$emit('delete')"
34
+ @duplicate="$emit('duplicate')"
35
+ @customize="$emit('customize')"
36
+ @visibility="onVisibilityChange(field, $event)"
37
+ />
38
+ </itf-dropdown>
39
+ </div>
40
+ <div class="b-properties-list__value rounded-2" :class="{'editable': editable, 'active shadow rounded-2 bg-body': focusId === field.Id}" sortable-skip>
41
+ <property-inline-edit
42
+ @focus="onFocus(field.Id)"
43
+ @blur="onBlur(field.Id)"
44
+ class="flex-grow-1 b-properties-list__inline-editor"
45
+ :field="field"
46
+ :value="value"
47
+ :editable="editable"
48
+ :focused="focusId === field.Id"
49
+ @input="$emit('input', $event)"
50
+ />
51
+ </div>
52
+ </div>
53
+ </template>
54
+ <style lang="scss">
55
+ .b-properties-list {
56
+ &__draghandler {
57
+ user-select: none;
58
+ transition: opacity 150ms ease-in 0s;
59
+ cursor: grab;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 18px;
64
+ height: 24px;
65
+ opacity: 0;
66
+ border-radius: 3px;
67
+ fill: rgba(55, 53, 47, 0.35);
68
+ }
69
+ &__inner:hover &__draghandler {
70
+ opacity: 1;
71
+ }
72
+ &__inner {
73
+ display: flex;
74
+ padding-bottom: 4px;
75
+ width: 100%;
76
+ }
77
+ &__name, &__value {
78
+ display: flex;
79
+ align-items: center;
80
+ height: 100%;
81
+ width: 100%;
82
+ border-radius: 3px;
83
+ padding: 0 6px;
84
+
85
+ &.editable {
86
+ cursor: pointer;
87
+ }
88
+ }
89
+ &__name {
90
+ display: flex;
91
+ align-items: center;
92
+ height: 34px;
93
+ width: 250px;
94
+ flex: 0 0 auto;
95
+ }
96
+ &__icon {
97
+ width: 22px;
98
+ margin-left: -22px;
99
+ opacity: 1;
100
+ }
101
+ &__value {
102
+ display: flex;
103
+ margin-left: 4px;
104
+ height: 100%;
105
+ min-width: 0;
106
+ padding: 0.25rem;
107
+ min-height: 34px;
108
+
109
+ &.editable &:hover, &.editable.active {
110
+ background-color: rgba(55, 53, 47, 0.08);
111
+ }
112
+ }
113
+ }
114
+ </style>
115
+ <script>
116
+ import { Vue, Component, Model, Prop, PropSync } from 'vue-property-decorator';
117
+ import itfIcon from '../icon/Icon';
118
+ import itfButton from '../button/Button';
119
+ import itfDropdown from '../dropdown/Dropdown.vue';
120
+ import itfLabel from '../form/Label.vue';
121
+ import itfTextField from '../text-field/TextField.vue';
122
+ import Sortable from '../sortable/Sortable';
123
+ import PropertiesPopupMenu from './PropertiesPopupMenu.vue';
124
+ import PropertiesEditMenu from './PropertiesEditMenu.vue';
125
+ import PropertyInlineEdit from './PropertyInlineEdit.vue';
126
+ import { INLINE_TYPES } from './constants';
127
+
128
+ export default @Component({
129
+ name: 'itfPropertyItem',
130
+ components: {
131
+ itfIcon,
132
+ itfDropdown,
133
+ itfTextField,
134
+ itfLabel,
135
+ itfButton,
136
+ Sortable,
137
+ PropertiesPopupMenu,
138
+ PropertiesEditMenu,
139
+ PropertyInlineEdit
140
+ }
141
+ })
142
+ class itfPropertyItem extends Vue {
143
+ @Model('input') value;
144
+ @Prop() field;
145
+ @Prop({ type: Boolean, default: false }) loading;
146
+ @Prop({ type: Boolean, default: false }) editable;
147
+
148
+ isEditMode = false;
149
+ focusId = null;
150
+
151
+ get types() {
152
+ return INLINE_TYPES;
153
+ }
154
+
155
+ onFocus(id) {
156
+ if (!this.editable) {
157
+ return;
158
+ }
159
+ this.focusId = id;
160
+ }
161
+
162
+ onBlur() {
163
+ this.focusId = null;
164
+ }
165
+
166
+ onClose() {
167
+ this.isEditMode = false;
168
+ }
169
+
170
+ onChange(item) {
171
+ this.$emit('update:field', item);
172
+ }
173
+
174
+ onVisibilityChange(field, value) {
175
+ const newField = { ...field };
176
+ newField.Visible = value;
177
+ this.$emit('update:field', newField);
178
+ }
179
+
180
+ hideEditMenu() {
181
+ this.$refs.editDd.hide();
182
+ }
183
+
184
+ showEditMenu() {
185
+ this.$refs.editDd.show();
186
+ this.$nextTick(() => {
187
+ this.isEditMode = true;
188
+ });
189
+ }
190
+ }
191
+ </script>
@@ -0,0 +1,9 @@
1
+ import InlineText from './Inline/Text';
2
+ import InlineDate from './Inline/Date';
3
+ import InlineMultiselect from './Inline/Multiselect.vue';
4
+
5
+ export const INLINE_TYPES = [
6
+ { Type: 'text', Name: 'Text', Icon: 'type_text', Component: InlineText },
7
+ { Type: 'date', Name: 'Date', Icon: 'type_date', Component: InlineDate },
8
+ { Type: 'multiselect', Name: 'Multiselect', Icon: 'type_multiselect', Component: InlineMultiselect },
9
+ ];
@@ -27,8 +27,8 @@ storiesOf('Common', module)
27
27
  '2021-10-21': { text: '🎉', class: 'test' }
28
28
  },
29
29
  list: [
30
- { Id: 'test1', Name: 'Test', Icon: 'eye' },
31
- { Id: 'test2', Name: 'Test2', Icon: 'eye' },
30
+ { Id: 'test1', Name: 'Last perfomance appraisal date', Icon: 'eye', Type: 'text' },
31
+ { Id: 'test2', Name: 'Test2', Icon: 'eye', Type: 'date' },
32
32
  ],
33
33
  value: {
34
34
  test1: 'test1'
@@ -58,7 +58,7 @@ storiesOf('Common', module)
58
58
 
59
59
  {{value}}
60
60
 
61
- <itf-properties-list :fields.sync="list" v-model="value"></itf-properties-list>
61
+ <itf-properties-list editable :fields.sync="list" v-model="value"></itf-properties-list>
62
62
 
63
63
  </itf-app>
64
64
  </div>`,
@@ -1,7 +1,9 @@
1
1
  <template>
2
2
 
3
3
  <div class="itf-dropdown" :class="`drop${placement}`">
4
+ <div v-if="disabled"><slot name="button">{{label}}</slot></div>
4
5
  <itf-button
6
+ v-else
5
7
  :class="{ 'dropdown-toggle': toggle }"
6
8
  v-bind="buttonOptions"
7
9
  ref="toggle"
@@ -41,6 +43,7 @@ class itfDropdown extends Vue {
41
43
  @Prop({ type: Boolean }) right;
42
44
  @Prop({ type: Boolean }) toggle;
43
45
  @Prop({ type: Boolean }) shadow;
46
+ @Prop({ type: Boolean }) disabled;
44
47
  @Prop({ validator: (value) => [true, false, 'inside', 'outside'].includes(value), default: true }) autoclose;
45
48
  @Prop({ type: Object, default: () => ({}) }) buttonOptions;
46
49
 
@@ -69,7 +72,7 @@ class itfDropdown extends Vue {
69
72
  }
70
73
 
71
74
  async mounted() {
72
- if (typeof window === 'undefined') {
75
+ if (typeof window === 'undefined' || !this.$refs.toggle) {
73
76
  return;
74
77
  }
75
78
  const { default: Dropdown } = await import('bootstrap/js/src/dropdown.js');
@@ -72,7 +72,9 @@ class itfDeleteConfirmModal extends Vue {
72
72
 
73
73
  onConfirm() {
74
74
  this.isModalShown = false;
75
- this.$emit('delete');
75
+ this.$nextTick(() => {
76
+ this.$emit('delete');
77
+ });
76
78
  }
77
79
  }
78
80
  </script>
@@ -10,7 +10,7 @@
10
10
  :title="title"
11
11
  content
12
12
  :size="size"
13
- @keyup.native.enter="submitSaveItem()"
13
+ @keydown.native.shift.enter.stop.prevent="submitSaveItem()"
14
14
  @update:visible="$emit('input', $event)"
15
15
  @close="onClose"
16
16
  >
@@ -67,7 +67,7 @@
67
67
  @click="submitSaveItem()"
68
68
  >
69
69
  <span class="me-1">{{ saveBtnCaption }}</span>
70
- <kbd>↵</kbd>
70
+ <kbd>Shift + ↵</kbd>
71
71
  </itf-button>
72
72
  </slot>
73
73
  </slot>
@@ -111,8 +111,6 @@ class itfItemEditor extends Vue {
111
111
  @Prop(Boolean) canDelete;
112
112
  @Prop(Boolean) fullscreen;
113
113
  @Prop(Boolean) readonly; // показує форму, але кнопка Save не викликає saveFunc
114
- @Prop() createdAt;
115
- @Prop() creator;
116
114
 
117
115
  loading = false;
118
116
  showError = false;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <itf-popover :visible.sync="value" ref="popover" :trigger="trigger" custom-class="nopadding" :placement="placement">
2
+ <itf-popover :visible.sync="isVisible" 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>
@@ -27,7 +27,7 @@
27
27
  </itf-button>
28
28
  </div>
29
29
 
30
- <div class="itf-select-popover__scroll text-start">
30
+ <div v-if="isVisible" class="itf-select-popover__scroll text-start">
31
31
  <template v-if="!search && recentIcons.length">
32
32
  <div class="py-1 text-muted">Recent</div>
33
33
  <div class="d-flex align-items-start flex-wrap flex-grow-0">
@@ -70,7 +70,7 @@
70
70
  }
71
71
  </style>
72
72
  <script>
73
- import { Vue, Component, Prop, PropSync } from 'vue-property-decorator';
73
+ import { Vue, Component, Model, Prop, PropSync } from 'vue-property-decorator';
74
74
  import itfPopover from './Popover';
75
75
  import itfIcon from '../icon/Icon';
76
76
  import iconsList from '../icon/icons';
@@ -81,7 +81,7 @@ import tooltip from '../../directives/tooltip';
81
81
  const RECENT_ITEMS_KEY = 'itfSelectPopoverRecent';
82
82
 
83
83
  export default @Component({
84
- name: 'itfSelectPopover',
84
+ name: 'itfIconPopover',
85
85
  directives: {
86
86
  tooltip
87
87
  },
@@ -92,13 +92,14 @@ export default @Component({
92
92
  itfTextField
93
93
  },
94
94
  })
95
- class itfSelectPopover extends Vue {
96
- @PropSync('visible') value;
95
+ class itfIconPopover extends Vue {
96
+ @Model('input') value;
97
97
  @Prop({ type: String, default: '' }) title;
98
98
  @Prop({ type: Boolean, default: '' }) removable;
99
99
  @Prop({ type: String, default: 'bottom', validator: (value) => ['bottom', 'left', 'right', 'top'].includes(value) }) placement;
100
100
  @Prop({ type: String, default: 'click', validator: (value) => ['click', 'focus', 'hover', 'manual'].includes(value) }) trigger;
101
101
 
102
+ isVisible = false;
102
103
  search = '';
103
104
  recentIcons = [];
104
105
 
@@ -82,7 +82,7 @@
82
82
  </slot>
83
83
 
84
84
  <slot name="spinner" v-bind="scope.spinner">
85
- <div v-show="mutableLoading" class="itf-spinner itf-select__loader">Loading...</div>
85
+ <div v-show="mutableLoading" class="itf-spinner itf-select__loader">{{ $t('components.select.loading') }}</div>
86
86
  </slot>
87
87
  </div>
88
88
  </div>
@@ -127,7 +127,7 @@
127
127
  </li>
128
128
  <li v-if="filteredOptions.length === 0" class="vs__no-options text-muted">
129
129
  <slot name="no-options" v-bind="scope.noOptions"
130
- ><span v-if="!mutableLoading">Sorry, no matching options.</span></slot
130
+ ><span v-if="!mutableLoading">{{ $t('components.select.noOptions') }}</span></slot
131
131
  >
132
132
  </li>
133
133
  <slot name="list-footer" v-bind="scope.listFooter" />
@@ -13,7 +13,7 @@
13
13
 
14
14
  <div v-if="currencySelect" class="itf-money-field__currency">
15
15
  <span>{{ selectedCurrencyCode }}</span>
16
- <select v-if="!currencyDisabled || !disabled" :value="currencyId" @input="onCurrencyChanged">
16
+ <select v-if="!disabled && !currencyDisabled" :value="currencyId" @input="onCurrencyChanged">
17
17
  <option v-for="currency in currencies" :key="currency.Id" :value="currency.Id" :selected="currencyId === currency.Id">
18
18
  {{ currency.Symbol }}, {{ currency.Code }} - {{ currency.Title }}
19
19
  </option>
@@ -24,9 +24,14 @@
24
24
  <style lang="scss">
25
25
  .itf-money-field {
26
26
  position: relative;
27
+
27
28
  .itf-text-field input {
28
29
  padding-left: var(--itf-money-field-padding-left, 1.5rem);
29
30
  padding-right: 3rem;
31
+
32
+ &.is-invalid, &.is-valid {
33
+ padding-right: 4rem;
34
+ }
30
35
  }
31
36
 
32
37
  &__prepend {
@@ -40,12 +45,16 @@
40
45
  position: absolute;
41
46
  right: .75rem;
42
47
  top: .5rem;
48
+ .is-invalid &, .is-valid & {
49
+ right: 1.75rem;
50
+ }
43
51
 
44
52
  select {
45
53
  position: absolute;
46
54
  top: 0;
47
55
  right: 0;
48
56
  opacity: 0;
57
+ left: 0;
49
58
  }
50
59
  }
51
60
  }
@@ -33,6 +33,9 @@ export function parseHours (str, { hoursInDay } = { hoursInDay: 8 }) {
33
33
  }
34
34
 
35
35
  export function formatDate (date, inputFormat = 'yyyy-MM-dd', toFormat) {
36
+ if (!date) {
37
+ return date;
38
+ }
36
39
  return DateTime.fromFormat(date, inputFormat).toFormat(toFormat || ITFSettings.defaultDisplayDateFormat);
37
40
  }
38
41
 
package/src/locales/en.js CHANGED
@@ -57,6 +57,10 @@ module.exports = {
57
57
  close: 'Close',
58
58
  wholeYear: 'Whole year',
59
59
 
60
+ select: {
61
+ loading: 'Loading...',
62
+ noOptions: 'Select an option or create a new one',
63
+ },
60
64
  modal: {
61
65
  delete: 'Delete',
62
66
  cancel: 'Cancel',
@@ -83,5 +87,7 @@ module.exports = {
83
87
  areYouSureYouWantToDeleteThisField: 'Are you sure you want to delete this property?',
84
88
  customizePage: 'Customize page',
85
89
  type: 'Type',
90
+ options: 'Options',
91
+ addOption: 'Add an option',
86
92
  }
87
93
  };