group.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. // Composables
  2. import { useProxiedModel } from "./proxiedModel.mjs"; // Utilities
  3. import { computed, inject, onBeforeUnmount, onMounted, onUpdated, provide, reactive, toRef, unref, watch } from 'vue';
  4. import { consoleWarn, deepEqual, findChildrenWithProvide, getCurrentInstance, getUid, propsFactory, wrapInArray } from "../util/index.mjs"; // Types
  5. export const makeGroupProps = propsFactory({
  6. modelValue: {
  7. type: null,
  8. default: undefined
  9. },
  10. multiple: Boolean,
  11. mandatory: [Boolean, String],
  12. max: Number,
  13. selectedClass: String,
  14. disabled: Boolean
  15. }, 'group');
  16. export const makeGroupItemProps = propsFactory({
  17. value: null,
  18. disabled: Boolean,
  19. selectedClass: String
  20. }, 'group-item');
  21. // Composables
  22. export function useGroupItem(props, injectKey) {
  23. let required = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
  24. const vm = getCurrentInstance('useGroupItem');
  25. if (!vm) {
  26. throw new Error('[Vuetify] useGroupItem composable must be used inside a component setup function');
  27. }
  28. const id = getUid();
  29. provide(Symbol.for(`${injectKey.description}:id`), id);
  30. const group = inject(injectKey, null);
  31. if (!group) {
  32. if (!required) return group;
  33. throw new Error(`[Vuetify] Could not find useGroup injection with symbol ${injectKey.description}`);
  34. }
  35. const value = toRef(props, 'value');
  36. const disabled = computed(() => !!(group.disabled.value || props.disabled));
  37. group.register({
  38. id,
  39. value,
  40. disabled
  41. }, vm);
  42. onBeforeUnmount(() => {
  43. group.unregister(id);
  44. });
  45. const isSelected = computed(() => {
  46. return group.isSelected(id);
  47. });
  48. const isFirst = computed(() => {
  49. return group.items.value[0].id === id;
  50. });
  51. const isLast = computed(() => {
  52. return group.items.value[group.items.value.length - 1].id === id;
  53. });
  54. const selectedClass = computed(() => isSelected.value && [group.selectedClass.value, props.selectedClass]);
  55. watch(isSelected, value => {
  56. vm.emit('group:selected', {
  57. value
  58. });
  59. }, {
  60. flush: 'sync'
  61. });
  62. return {
  63. id,
  64. isSelected,
  65. isFirst,
  66. isLast,
  67. toggle: () => group.select(id, !isSelected.value),
  68. select: value => group.select(id, value),
  69. selectedClass,
  70. value,
  71. disabled,
  72. group
  73. };
  74. }
  75. export function useGroup(props, injectKey) {
  76. let isUnmounted = false;
  77. const items = reactive([]);
  78. const selected = useProxiedModel(props, 'modelValue', [], v => {
  79. if (v == null) return [];
  80. return getIds(items, wrapInArray(v));
  81. }, v => {
  82. const arr = getValues(items, v);
  83. return props.multiple ? arr : arr[0];
  84. });
  85. const groupVm = getCurrentInstance('useGroup');
  86. function register(item, vm) {
  87. // Is there a better way to fix this typing?
  88. const unwrapped = item;
  89. const key = Symbol.for(`${injectKey.description}:id`);
  90. const children = findChildrenWithProvide(key, groupVm?.vnode);
  91. const index = children.indexOf(vm);
  92. if (unref(unwrapped.value) == null) {
  93. unwrapped.value = index;
  94. unwrapped.useIndexAsValue = true;
  95. }
  96. if (index > -1) {
  97. items.splice(index, 0, unwrapped);
  98. } else {
  99. items.push(unwrapped);
  100. }
  101. }
  102. function unregister(id) {
  103. if (isUnmounted) return;
  104. // TODO: re-evaluate this line's importance in the future
  105. // should we only modify the model if mandatory is set.
  106. // selected.value = selected.value.filter(v => v !== id)
  107. forceMandatoryValue();
  108. const index = items.findIndex(item => item.id === id);
  109. items.splice(index, 1);
  110. }
  111. // If mandatory and nothing is selected, then select first non-disabled item
  112. function forceMandatoryValue() {
  113. const item = items.find(item => !item.disabled);
  114. if (item && props.mandatory === 'force' && !selected.value.length) {
  115. selected.value = [item.id];
  116. }
  117. }
  118. onMounted(() => {
  119. forceMandatoryValue();
  120. });
  121. onBeforeUnmount(() => {
  122. isUnmounted = true;
  123. });
  124. onUpdated(() => {
  125. // #19655 update the items that use the index as the value.
  126. for (let i = 0; i < items.length; i++) {
  127. if (items[i].useIndexAsValue) {
  128. items[i].value = i;
  129. }
  130. }
  131. });
  132. function select(id, value) {
  133. const item = items.find(item => item.id === id);
  134. if (value && item?.disabled) return;
  135. if (props.multiple) {
  136. const internalValue = selected.value.slice();
  137. const index = internalValue.findIndex(v => v === id);
  138. const isSelected = ~index;
  139. value = value ?? !isSelected;
  140. // We can't remove value if group is
  141. // mandatory, value already exists,
  142. // and it is the only value
  143. if (isSelected && props.mandatory && internalValue.length <= 1) return;
  144. // We can't add value if it would
  145. // cause max limit to be exceeded
  146. if (!isSelected && props.max != null && internalValue.length + 1 > props.max) return;
  147. if (index < 0 && value) internalValue.push(id);else if (index >= 0 && !value) internalValue.splice(index, 1);
  148. selected.value = internalValue;
  149. } else {
  150. const isSelected = selected.value.includes(id);
  151. if (props.mandatory && isSelected) return;
  152. selected.value = value ?? !isSelected ? [id] : [];
  153. }
  154. }
  155. function step(offset) {
  156. // getting an offset from selected value obviously won't work with multiple values
  157. if (props.multiple) consoleWarn('This method is not supported when using "multiple" prop');
  158. if (!selected.value.length) {
  159. const item = items.find(item => !item.disabled);
  160. item && (selected.value = [item.id]);
  161. } else {
  162. const currentId = selected.value[0];
  163. const currentIndex = items.findIndex(i => i.id === currentId);
  164. let newIndex = (currentIndex + offset) % items.length;
  165. let newItem = items[newIndex];
  166. while (newItem.disabled && newIndex !== currentIndex) {
  167. newIndex = (newIndex + offset) % items.length;
  168. newItem = items[newIndex];
  169. }
  170. if (newItem.disabled) return;
  171. selected.value = [items[newIndex].id];
  172. }
  173. }
  174. const state = {
  175. register,
  176. unregister,
  177. selected,
  178. select,
  179. disabled: toRef(props, 'disabled'),
  180. prev: () => step(items.length - 1),
  181. next: () => step(1),
  182. isSelected: id => selected.value.includes(id),
  183. selectedClass: computed(() => props.selectedClass),
  184. items: computed(() => items),
  185. getItemIndex: value => getItemIndex(items, value)
  186. };
  187. provide(injectKey, state);
  188. return state;
  189. }
  190. function getItemIndex(items, value) {
  191. const ids = getIds(items, [value]);
  192. if (!ids.length) return -1;
  193. return items.findIndex(item => item.id === ids[0]);
  194. }
  195. function getIds(items, modelValue) {
  196. const ids = [];
  197. modelValue.forEach(value => {
  198. const item = items.find(item => deepEqual(value, item.value));
  199. const itemByIndex = items[value];
  200. if (item?.value != null) {
  201. ids.push(item.id);
  202. } else if (itemByIndex != null) {
  203. ids.push(itemByIndex.id);
  204. }
  205. });
  206. return ids;
  207. }
  208. function getValues(items, ids) {
  209. const values = [];
  210. ids.forEach(id => {
  211. const itemIndex = items.findIndex(item => item.id === id);
  212. if (~itemIndex) {
  213. const item = items[itemIndex];
  214. values.push(item.value != null ? item.value : itemIndex);
  215. }
  216. });
  217. return values;
  218. }
  219. //# sourceMappingURL=group.mjs.map