@xy-planning-network/trees 0.4.1 → 0.4.2

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": "@xy-planning-network/trees",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": "github:xy-planning-network/trees",
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { Pagination } from "@/composables/nav"
3
- import { computed, ref } from "vue"
3
+ import { computed } from "vue"
4
4
 
5
5
  const props = defineProps<{
6
6
  modelValue: Pagination
@@ -10,23 +10,19 @@ const emit = defineEmits<{
10
10
  (e: "update:modelValue", pagination: Pagination): void
11
11
  }>()
12
12
 
13
- const pagination = ref<Pagination>(props.modelValue)
14
-
15
- const updateModelValue = () => {
16
- emit("update:modelValue", pagination.value)
17
- }
18
-
19
13
  const changePage = (page: number): void => {
20
- pagination.value.page = page
21
- updateModelValue()
14
+ emit("update:modelValue", {
15
+ ...props.modelValue,
16
+ page: page,
17
+ })
22
18
  }
23
19
 
24
20
  const pageShortcuts = computed((): number[] => {
25
21
  const shortcuts: number[] = []
26
22
 
27
23
  // If total pages is less than or equal to 4, just return 1, 2, 3, 4
28
- if (pagination.value.totalPages <= 4) {
29
- for (let i = 0; i < pagination.value.totalPages; i++) {
24
+ if (props.modelValue.totalPages <= 4) {
25
+ for (let i = 0; i < props.modelValue.totalPages; i++) {
30
26
  shortcuts.push(i + 1)
31
27
  }
32
28
  return shortcuts
@@ -34,10 +30,10 @@ const pageShortcuts = computed((): number[] => {
34
30
 
35
31
  // If there are more than 3 pages left, show these
36
32
  // e.g. [4, 5, 6, 7] when there are 8 total pages and the current page is 4
37
- const pagesLeft: number = pagination.value.totalPages - pagination.value.page
33
+ const pagesLeft: number = props.modelValue.totalPages - props.modelValue.page
38
34
  if (pagesLeft >= 3) {
39
35
  for (let i = 0; i < 4; i++) {
40
- shortcuts.push(pagination.value.page + i)
36
+ shortcuts.push(props.modelValue.page + i)
41
37
  }
42
38
  return shortcuts
43
39
  }
@@ -45,7 +41,7 @@ const pageShortcuts = computed((): number[] => {
45
41
  // If there are less than 3 pages left, count backwards from the last page
46
42
  // e.g. [5, 6, 7, 8] when on page 5, 6, 7, and 8 and there are 8 total pages
47
43
  for (let i = 0; i < 4; i++) {
48
- shortcuts.unshift(pagination.value.totalPages - i)
44
+ shortcuts.unshift(props.modelValue.totalPages - i)
49
45
  }
50
46
  return shortcuts
51
47
  })
@@ -56,9 +52,9 @@ const pageShortcuts = computed((): number[] => {
56
52
  <a
57
53
  href="#"
58
54
  class="-mt-px border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm leading-5 font-medium focus:outline-none focus:text-gray-700 focus:border-gray-400"
59
- @click.prevent="changePage(pagination.page - 1)"
55
+ @click.prevent="changePage(modelValue.page - 1)"
60
56
  :class="
61
- pagination.page == 1
57
+ modelValue.page == 1
62
58
  ? 'text-gray-500 cursor-not-allowed pointer-events-none'
63
59
  : 'text-gray-700 hover:text-gray-900 hover:border-gray-300'
64
60
  "
@@ -82,7 +78,7 @@ const pageShortcuts = computed((): number[] => {
82
78
  :key="i"
83
79
  v-text="i"
84
80
  :class="
85
- pagination.page === i
81
+ modelValue.page === i
86
82
  ? 'border-blue-500 text-blue-600 focus:outline-none focus:text-blue-800 focus:border-blue-700'
87
83
  : 'border-transparent text-gray-700 hover:text-gray-900 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400'
88
84
  "
@@ -94,9 +90,9 @@ const pageShortcuts = computed((): number[] => {
94
90
  <a
95
91
  href="#"
96
92
  class="-mt-px border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm leading-5 font-medium focus:outline-none focus:text-gray-700 focus:border-gray-400"
97
- @click.prevent="changePage(pagination.page + 1)"
93
+ @click.prevent="changePage(modelValue.page + 1)"
98
94
  :class="
99
- pagination.page >= pagination.totalPages
95
+ modelValue.page >= modelValue.totalPages
100
96
  ? 'text-gray-500 cursor-not-allowed pointer-events-none'
101
97
  : 'text-gray-700 hover:text-gray-900 hover:border-gray-300'
102
98
  "
@@ -8,62 +8,102 @@ export type PopoverPosition =
8
8
  | "bottom-right"
9
9
  | "left"
10
10
  | "right"
11
+ | "auto"
12
+ | "none"
11
13
  </script>
12
14
  <script lang="ts" setup>
15
+ import { throttle } from "@/helpers/Throttle"
13
16
  import {
14
17
  Popover as HeadlessPopover,
15
18
  PopoverButton as HeadlessPopoverButton,
16
19
  PopoverPanel as HeadlessPopoverPanel,
17
20
  } from "@headlessui/vue"
18
- import { computed } from "vue"
21
+ import { computed, onMounted, onUnmounted, ref } from "vue"
19
22
 
20
23
  const props = withDefaults(
21
24
  defineProps<{
25
+ as?: string
22
26
  position?: PopoverPosition
23
27
  }>(),
24
28
  {
25
- position: "top-center",
29
+ as: "div",
30
+ position: "auto",
26
31
  }
27
32
  )
28
33
 
29
- const positionClasses = computed(() => {
30
- // NOTE: by always pushing the screen width wrapper classes to the left we can avoid overflow scrolling
34
+ const getViewportDimensions = () => {
35
+ return {
36
+ vw: document.documentElement.clientWidth,
37
+ vh: document.documentElement.clientHeight,
38
+ }
39
+ }
40
+
41
+ const trigger = ref<typeof HeadlessPopoverButton>()
42
+ const wrapper = ref<typeof HeadlessPopoverPanel>()
43
+ const viewport = ref<{ vw: number; vh: number }>(getViewportDimensions())
44
+
45
+ const classes = computed(() => {
46
+ const classes = {
47
+ wrapper: "",
48
+ content: "",
49
+ }
31
50
 
51
+ if (props.position === "none") {
52
+ return classes
53
+ }
54
+
55
+ // defaults classes when positioning
56
+ classes.wrapper = "h-0 flex w-screen"
57
+ classes.content = "absolute"
58
+
59
+ // merge static positioning classes
60
+ if (props.position !== "auto") {
61
+ classes.wrapper += ` ${staticPosition.value.wrapper}`
62
+ classes.content += ` ${staticPosition.value.content}`
63
+ }
64
+
65
+ return classes
66
+ })
67
+
68
+ const staticPosition = computed(() => {
32
69
  let wrapperClasses = ""
33
70
  let contentClasses = ""
34
71
 
35
72
  switch (props.position) {
36
73
  case "top-left":
37
- wrapperClasses =
38
- "top-0 left-0 -translate-y-full -translate-x-full justify-end"
74
+ wrapperClasses = "top-0 right-0 -translate-y-full justify-end"
75
+ contentClasses = "bottom-full"
39
76
  break
40
77
  case "top-center":
41
78
  wrapperClasses =
42
79
  "top-0 -translate-y-full -translate-x-full left-1/2 justify-end"
43
- contentClasses = "translate-x-1/2"
80
+ contentClasses = "bottom-full translate-x-1/2"
44
81
  break
45
82
  case "top-right":
46
- wrapperClasses = "top-0 -translate-y-full right-0 justify-end"
47
- contentClasses = "translate-x-full"
83
+ wrapperClasses =
84
+ "top-0 -translate-y-full left-0 -translate-x-full justify-end"
85
+ contentClasses = "bottom-full translate-x-full"
48
86
  break
49
87
  case "bottom-left":
50
- wrapperClasses = "top-full left-0 -translate-x-full justify-end"
88
+ wrapperClasses = "top-full right-0 justify-end"
89
+ contentClasses = "top-full"
51
90
  break
52
91
  case "bottom-center":
53
92
  wrapperClasses = "top-full -translate-x-full left-1/2 justify-end"
54
- contentClasses = "translate-x-1/2"
93
+ contentClasses = "top-full translate-x-1/2"
55
94
  break
56
95
  case "bottom-right":
57
- wrapperClasses = "top-full right-0 justify-end"
58
- contentClasses = "translate-x-full"
96
+ wrapperClasses = "top-full left-0 -translate-x-full justify-end"
97
+ contentClasses = "top-full translate-x-full"
59
98
  break
60
99
  case "left":
61
100
  wrapperClasses =
62
101
  "top-1/2 left-0 -translate-y-1/2 -translate-x-full justify-end"
102
+ contentClasses = "-translate-y-1/2"
63
103
  break
64
104
  case "right":
65
105
  wrapperClasses = "top-1/2 -translate-y-1/2 right-0 justify-end"
66
- contentClasses = "translate-x-full"
106
+ contentClasses = "translate-x-full -translate-y-1/2"
67
107
  break
68
108
  }
69
109
 
@@ -73,37 +113,117 @@ const positionClasses = computed(() => {
73
113
  }
74
114
  })
75
115
 
76
- // TODO: maybe auto positioning - dynamic based on button location and closed overflow hidden container?
116
+ const autoPosition = computed(() => {
117
+ if (!wrapper?.value?.el || !trigger?.value?.el) {
118
+ return {}
119
+ }
120
+
121
+ const { vw, vh } = viewport.value
122
+
123
+ // avoid bumping up against the edge of the browser when possible
124
+ const offset = 10
125
+
126
+ // base the anchor rectangle off of the entire trigger dom element to move around it
127
+ const anchorRect: DOMRect = trigger.value.el.getBoundingClientRect()
128
+ // the content rectangle is best calculated by our first child (content) element inside the wrapper
129
+ const contentRect: DOMRect =
130
+ wrapper.value.el.firstChild.getBoundingClientRect()
131
+ const distToBottom = vh - anchorRect.bottom
132
+ // NOTE: edge case - there may be more space below in the viewport
133
+ // but less document space for display
134
+ // the inverse could also be true - but will be very rare
135
+ // occurring with unreasonably large popover content
136
+ const positionAbove = anchorRect.top > distToBottom
137
+ const distToRight = vw - anchorRect.left
138
+ const flowLeft = anchorRect.left > distToRight
139
+
140
+ // translate the content container on the x axis to the correct position
141
+ // considering the flow the content should take
142
+ let xPos = 0
143
+ if (flowLeft) {
144
+ if (contentRect.width > anchorRect.right) {
145
+ xPos =
146
+ anchorRect.right -
147
+ contentRect.width +
148
+ (contentRect.width - anchorRect.right)
149
+ } else {
150
+ xPos = anchorRect.right - contentRect.width
151
+ }
152
+
153
+ if (vw > contentRect.width + offset) {
154
+ xPos = xPos + offset
155
+ }
156
+ } else {
157
+ if (contentRect.width > distToRight) {
158
+ xPos = anchorRect.left - (contentRect.width - distToRight)
159
+ } else {
160
+ xPos = anchorRect.left
161
+ }
162
+
163
+ if (vw > contentRect.width + offset) {
164
+ xPos = xPos - offset
165
+ }
166
+ }
167
+
168
+ return {
169
+ wrapper: {
170
+ top: positionAbove ? "auto" : `100%`,
171
+ bottom: positionAbove ? "100%" : `auto`,
172
+ transform: `translate(${anchorRect.left * -1}px, 0)`, // pin to left of window
173
+ width: `${vw}px`,
174
+ },
175
+ content: {
176
+ top: positionAbove ? "auto" : `100%`,
177
+ bottom: positionAbove ? "100%" : `auto`,
178
+ transform: `translate(${xPos}px, 0)`,
179
+ },
180
+ }
181
+ })
182
+
183
+ if (props.position === "auto") {
184
+ const throttledSetPositions = throttle(() => {
185
+ viewport.value = getViewportDimensions()
186
+ })
187
+
188
+ onMounted(() => {
189
+ window.addEventListener("resize", throttledSetPositions)
190
+ window.addEventListener("scroll", throttledSetPositions)
191
+ })
192
+
193
+ onUnmounted(() => {
194
+ window.removeEventListener("resize", throttledSetPositions)
195
+ window.removeEventListener("scroll", throttledSetPositions)
196
+ })
197
+ }
77
198
  </script>
78
199
 
79
200
  <template>
80
- <div class="flex">
81
- <HeadlessPopover v-slot="{ open, close }" class="relative leading-none">
82
- <HeadlessPopoverButton>
83
- <slot name="button" :open="open" :close="close"></slot>
84
- </HeadlessPopoverButton>
85
-
86
- <transition
87
- enter-active-class="transition-opacity transition-faster ease-out-quad"
88
- leave-active-class="transition-opacity transition-faster ease-in-quad"
89
- enter-from-class="opacity-0"
90
- enter-to-class="opacity-100"
91
- leave-from-class="opacity-100"
92
- leave-to-class="opacity-0"
201
+ <HeadlessPopover v-slot="{ open, close }" class="relative" :as="as">
202
+ <HeadlessPopoverButton ref="trigger">
203
+ <slot name="button" :open="open" :close="close"></slot>
204
+ </HeadlessPopoverButton>
205
+
206
+ <transition
207
+ enter-active-class="transition-opacity transition-faster ease-out-quad"
208
+ leave-active-class="transition-opacity transition-fastest ease-in-quad"
209
+ enter-from-class="opacity-0"
210
+ enter-to-class="opacity-100"
211
+ leave-from-class="opacity-100"
212
+ leave-to-class="opacity-0"
213
+ >
214
+ <HeadlessPopoverPanel
215
+ ref="wrapper"
216
+ class="absolute z-10"
217
+ :class="classes.wrapper"
218
+ :style="position === 'auto' ? autoPosition.wrapper : {}"
93
219
  >
94
- <!--NOTE: use prop "static" for dev work to keep the tooptip visible-->
95
- <HeadlessPopoverPanel>
96
- <!--positioning wrappers-->
97
- <div
98
- class="absolute z-10 transform w-screen flex"
99
- :class="positionClasses.wrapper"
100
- >
101
- <div :class="positionClasses.content">
102
- <slot :open="open" :close="close"></slot>
103
- </div>
104
- </div>
105
- </HeadlessPopoverPanel>
106
- </transition>
107
- </HeadlessPopover>
108
- </div>
220
+ <div
221
+ :class="classes.content"
222
+ :style="position === 'auto' ? autoPosition.content : {}"
223
+ >
224
+ <slot :open="open" :close="close"></slot>
225
+ </div>
226
+ </HeadlessPopoverPanel>
227
+ </transition>
228
+ </HeadlessPopover>
109
229
  </template>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <!--styling wrapper - top level element will merge attrs for class overrides -->
3
3
  <div
4
- class="max-w-xs bg-white rounded-md p-2 border border-gray-100 shadow-md"
4
+ class="w-full max-w-xs bg-white rounded-md p-2 border border-gray-100 shadow-md"
5
5
  >
6
6
  <slot></slot>
7
7
  </div>
@@ -1,31 +1,34 @@
1
1
  <script lang="ts" setup>
2
2
  import Popover, { PopoverPosition } from "./Popover/Popover.vue"
3
3
  import { InformationCircleIcon } from "@heroicons/vue/outline"
4
- import PopoverContent from "./Popover/PopoverContent.vue"
5
4
 
6
5
  withDefaults(
7
6
  defineProps<{
8
- position: PopoverPosition
7
+ as?: string
8
+ position?: PopoverPosition
9
9
  }>(),
10
10
  {
11
- position: "top-center",
11
+ as: "span",
12
+ position: "auto",
12
13
  }
13
14
  )
14
15
  </script>
15
16
 
16
17
  <template>
17
- <Popover :position="position">
18
+ <Popover :position="position" :as="as">
18
19
  <template #button>
19
- <div class="relative">
20
- <!--creates a larger clickable surface area-->
20
+ <div class="leading-none w-4 h-4">
21
+ <InformationCircleIcon />
22
+ <!--creates a larger clickable surface area 40 x 40-->
21
23
  <div
22
- class="p-4 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
24
+ class="p-5 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
23
25
  ></div>
24
- <InformationCircleIcon class="w-4 h-4" />
25
26
  </div>
26
27
  </template>
27
- <PopoverContent class="text-xs leading-tight font-medium">
28
+ <div
29
+ class="w-full max-w-xs bg-white rounded-md px-3 py-2 border border-gray-100 drop-shadow-md text-xs text-gray-900 leading-snug font-medium"
30
+ >
28
31
  <slot></slot>
29
- </PopoverContent>
32
+ </div>
30
33
  </Popover>
31
34
  </template>
@@ -0,0 +1 @@
1
+ export declare function debounce(func: () => void, timeout?: number): () => void;
@@ -0,0 +1 @@
1
+ export declare function throttle(func: () => void, timeout?: number): (...args: any[]) => void;
@@ -1,15 +1,23 @@
1
- export declare type PopoverPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "left" | "right";
1
+ export declare type PopoverPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "left" | "right" | "auto" | "none";
2
2
  declare const _default: import("vue").DefineComponent<{
3
+ as: {
4
+ type: import("vue").PropType<string>;
5
+ } & {
6
+ default: string;
7
+ };
3
8
  position: {
4
9
  type: import("vue").PropType<PopoverPosition>;
5
10
  } & {
6
11
  default: string;
7
12
  };
8
13
  }, () => void, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<{
14
+ as?: unknown;
9
15
  position?: unknown;
10
16
  } & {
17
+ as: string;
11
18
  position: PopoverPosition;
12
19
  } & {}>, {
20
+ as: string;
13
21
  position: PopoverPosition;
14
22
  }>;
15
23
  export default _default;
@@ -1,16 +1,23 @@
1
1
  import { PopoverPosition } from "./Popover/Popover.vue";
2
2
  declare const _default: import("vue").DefineComponent<{
3
+ as: {
4
+ type: import("vue").PropType<string>;
5
+ } & {
6
+ default: string;
7
+ };
3
8
  position: {
4
9
  type: import("vue").PropType<PopoverPosition>;
5
- required: true;
6
10
  } & {
7
11
  default: string;
8
12
  };
9
13
  }, () => void, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<{
14
+ as?: unknown;
10
15
  position?: unknown;
11
16
  } & {
17
+ as: string;
12
18
  position: PopoverPosition;
13
19
  } & {}>, {
20
+ as: string;
14
21
  position: PopoverPosition;
15
22
  }>;
16
23
  export default _default;