Field.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <template>
  2. <div class="control">
  3. <label for="name">{{ name }}</label>
  4. <button v-if="entries.length === 0 && !readonly" @click="addEntry()" type="button">+</button>
  5. <div class="control-row" v-for="(entry, entryIndex) in entries">
  6. <div class="control-col" v-for="(fieldType, fieldIndex) in fieldTypes" :class="{ 'fill-remaining': fieldType.fill }">
  7. <input :ref="`input:${entryIndex}:${fieldType.fieldTypeId}`" :disabled="readonly" name="name" type="text" v-if="fieldType.type === 'text'" v-model="entry[fieldType.fieldTypeId]" @change="onInputChange()" v-on:blur="blurInput(entryIndex, fieldType.fieldTypeId)" v-on:focus="focusInput(entryIndex, fieldType.fieldTypeId)" v-on:keydown="keydownInput(entryIndex, fieldType.fieldTypeId)" @keyup.ctrl.exact.59="fillInput(entryIndex, fieldType.fieldTypeId, 'date')" @keyup.ctrl.shift.exact.59="fillInput(entryIndex, fieldType.fieldTypeId, 'time')" />
  8. <select name="name" v-if="fieldType.type === 'select'" :disabled="readonly" class="fill-remaining" v-model="entry[fieldType.fieldTypeId]" @change="onSelectChange()">
  9. <option v-for="option in fieldType.options" :value="option.value">{{option.text}}</option>
  10. </select>
  11. <div tabindex="0" v-on:keyup.enter="toggleCheckbox(entryIndex, fieldType.fieldTypeId)" v-on:keyup.space="toggleCheckbox(entryIndex, fieldType.fieldTypeId)" name="name" class="checkbox" v-if="fieldType.type === 'checkbox'" :class="{ checked: entry[fieldType.fieldTypeId], disabled: readonly }" @click="toggleCheckbox(entryIndex, fieldType.fieldTypeId)"></div>
  12. <button v-if="fieldType.extraButtons && !readonly" v-for="buttonInfo in fieldType.extraButtons" type="button" :class="[buttonInfo.style]">{{buttonInfo.icon}}</button>
  13. <button v-if="entryIndex + 1 === entries.length && entryIndex + 1 < maxEntries && fieldIndex + 1 === fieldTypes.length && !readonly" @click="addEntry()" type="button">+</button>
  14. <button v-if="entries.length > minEntries && fieldIndex + 1 === fieldTypes.length && !readonly" @click="removeEntry(entryIndex)" type="button">-</button>
  15. <div v-if="fieldType.autosuggestGroup && (focusedInput === `${entryIndex}.${fieldType.fieldTypeId}` || autosuggestHover === `${entryIndex}.${fieldType.fieldTypeId}`)" class="autosuggest-container" @mouseover="focusAutosuggestContainer(entryIndex, fieldType.fieldTypeId)" @mouseleave="blurAutosuggestContainer()">
  16. <div v-for="autosuggestItem in filter(autosuggest[fieldType.autosuggestGroup], entry[fieldType.fieldTypeId])" class="autosuggest-item" @click="selectAutosuggest(entryIndex, fieldType.fieldTypeId, autosuggestItem)">
  17. {{ autosuggestItem }}
  18. </div>
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. </template>
  24. <script>
  25. function Field() {
  26. }
  27. export default {
  28. data: function() {
  29. return {
  30. entries: [...this.initialEntries],
  31. focusedInput: "",
  32. activeAutosuggestHover: "",
  33. autosuggestHover: ""
  34. };
  35. },
  36. props: {
  37. name: String,
  38. fieldTypes: Array,
  39. minEntries: Number,
  40. maxEntries: Number,
  41. initialEntries: Array,
  42. autosuggest: Object,
  43. onChange: Function,
  44. readonly: Boolean
  45. },
  46. mounted() {
  47. },
  48. methods: {
  49. addEntry() {
  50. let emptyEntry = {};
  51. this.fieldTypes.forEach((fieldType) => {
  52. if (fieldType.type === "text" || fieldType.type === "select") emptyEntry[fieldType.fieldTypeId] = "";
  53. else if (fieldType.type === "checkbox") emptyEntry[fieldType.fieldTypeId] = false;
  54. });
  55. this.entries.push(emptyEntry);
  56. this.onChange();
  57. },
  58. removeEntry(index) {
  59. this.entries.splice(index, 1);
  60. this.onChange();
  61. },
  62. toggleCheckbox(entryIndex, fieldTypeId) {
  63. if (this.readonly) return;
  64. this.entries[entryIndex][fieldTypeId] = !this.entries[entryIndex][fieldTypeId];
  65. this.onChange();
  66. },
  67. selectAutosuggest(entryIndex, fieldTypeId, autosuggestItem) {
  68. this.entries[entryIndex][fieldTypeId] = autosuggestItem;
  69. },
  70. blurInput(entryIndex, fieldTypeId) {
  71. this.focusedInput = "";
  72. },
  73. focusInput(entryIndex, fieldTypeId) {
  74. this.focusedInput = `${entryIndex}.${fieldTypeId}`;
  75. },
  76. keydownInput(entryIndex, fieldTypeId) {
  77. },
  78. focusAutosuggestContainer(entryIndex, fieldTypeId) {
  79. this.autosuggestHover = `${entryIndex}.${fieldTypeId}`;
  80. },
  81. blurAutosuggestContainer() {
  82. this.autosuggestHover = "";
  83. },
  84. onSelectChange() {
  85. this.onChange();
  86. },
  87. onInputChange() {
  88. this.onChange();
  89. },
  90. filter(autosuggest, value) {
  91. return autosuggest.filter(autosuggestItem => autosuggestItem.toLowerCase().startsWith(value.toLowerCase())).sort();
  92. },
  93. fillInput(entryIndex, fieldTypeId, fillType) {
  94. let input = this.$refs[`input:${entryIndex}:${fieldTypeId}`][0];
  95. const cursorPoint = input.selectionStart;
  96. const currentValue = this.entries[entryIndex][fieldTypeId];
  97. const firstPart = currentValue.substring(0, cursorPoint);
  98. const lastPart = currentValue.substring(cursorPoint, currentValue.length);
  99. let fillValue = "";
  100. const currentDate = new Date();
  101. switch(fillType) {
  102. case "date":
  103. const year = currentDate.getFullYear();
  104. const month = `0${(currentDate.getMonth() + 1)}`.slice(-2);
  105. const day = `0${currentDate.getDate()}`.slice(-2);
  106. fillValue = `${year}-${month}-${day}`;
  107. break;
  108. case "time":
  109. const hour = `0${currentDate.getHours()}`.slice(-2);
  110. const minute = `0${currentDate.getMinutes()}`.slice(-2);
  111. fillValue = `${hour}:${minute}`;
  112. break;
  113. }
  114. const newValue = `${firstPart}${fillValue}${lastPart}`;
  115. this.entries[entryIndex][fieldTypeId] = newValue;
  116. const newCaretPosition = firstPart.length + fillValue.length;
  117. this.$nextTick(() => {
  118. input.focus();
  119. input.setSelectionRange(newCaretPosition, newCaretPosition);
  120. });
  121. }
  122. }
  123. };
  124. </script>
  125. <style lang="scss" scoped>
  126. .control {
  127. width: 100%;
  128. margin-bottom: 16px;
  129. label {
  130. display: block;
  131. margin-bottom: 4px;
  132. }
  133. .control-row {
  134. height: 32px;
  135. display: flex;
  136. grid-column-gap: 12px;
  137. }
  138. .control-row:not(:last-of-type) {
  139. margin-bottom: 8px;
  140. }
  141. .control-col {
  142. display: flex;
  143. position: relative;
  144. button:last-of-type {
  145. border-radius: 0 5px 5px 0;
  146. }
  147. > input:not(:last-child) {
  148. border-right: none;
  149. border-top-right-radius: 0;
  150. border-bottom-right-radius: 0;
  151. }
  152. .autosuggest-container {
  153. position: absolute;
  154. top: 31px;
  155. width: 100%;
  156. border-radius: 5px 5px 5px 5px;
  157. border: solid .5px #464646;
  158. z-index: 10;
  159. .autosuggest-item {
  160. padding: 8px;
  161. cursor: pointer;
  162. background-color: white;
  163. &:hover, &:focus {
  164. background-color: lightgray;
  165. }
  166. }
  167. }
  168. }
  169. input, select {
  170. border: solid .5px #464646;
  171. border-radius: 5px;
  172. padding: 4px;
  173. }
  174. input {
  175. width: 100%;
  176. }
  177. button {
  178. width: 32px;
  179. height: 32px;
  180. border: none;
  181. font-size: 24px;
  182. background-color: green;
  183. color: white;
  184. cursor: pointer;
  185. }
  186. button:hover, button:focus {
  187. background-color: darkgreen;
  188. }
  189. }
  190. .fill-remaining {
  191. width: 100%;
  192. width: -moz-available; /* WebKit-based browsers will ignore this. */
  193. width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
  194. width: fill-available;
  195. }
  196. .checkbox {
  197. height: 32px;
  198. width: 32px;
  199. background-color: white;
  200. border: .5px #464646 solid;
  201. border-radius: 3px;
  202. cursor: pointer;
  203. position: relative;
  204. box-sizing: border-box;
  205. &.disabled {
  206. cursor:auto;
  207. }
  208. }
  209. .checkbox.checked::after {
  210. content: "";
  211. width: 26px;
  212. height: 26px;
  213. left: 2px;
  214. top: 2px;
  215. display: inline-block;
  216. position: absolute;
  217. border-radius: 3px;
  218. background-color: #69B862;
  219. }
  220. </style>