group.mjs 6.5 KB

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