pimelon-ui 0.0.19 → 0.0.57

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.
Files changed (63) hide show
  1. package/package.json +28 -8
  2. package/src/components/Alert.vue +3 -3
  3. package/src/components/Autocomplete.vue +146 -0
  4. package/src/components/Avatar.vue +2 -2
  5. package/src/components/Badge.vue +38 -19
  6. package/src/components/Button.vue +26 -11
  7. package/src/components/Card.vue +4 -4
  8. package/src/components/DatePicker.vue +247 -0
  9. package/src/components/Dialog.vue +14 -17
  10. package/src/components/Dropdown.vue +9 -9
  11. package/src/components/ErrorMessage.vue +1 -1
  12. package/src/components/FeatherIcon.vue +8 -4
  13. package/src/components/Input.vue +31 -17
  14. package/src/components/Link.vue +1 -1
  15. package/src/components/LoadingText.vue +1 -1
  16. package/src/components/Modal.vue +1 -1
  17. package/src/components/Popover.vue +124 -81
  18. package/src/components/SuccessMessage.vue +1 -1
  19. package/src/components/TextEditor/InsertImage.vue +72 -0
  20. package/src/components/TextEditor/MentionList.vue +93 -0
  21. package/src/components/TextEditor/Menu.vue +113 -8
  22. package/src/components/TextEditor/TextEditor.vue +180 -25
  23. package/src/components/TextEditor/commands.js +185 -10
  24. package/src/components/TextEditor/icons/align-center.vue +14 -0
  25. package/src/components/TextEditor/icons/align-justify.vue +14 -0
  26. package/src/components/TextEditor/icons/align-left.vue +14 -0
  27. package/src/components/TextEditor/icons/align-right.vue +14 -0
  28. package/src/components/TextEditor/icons/arrow-go-back-line.vue +14 -0
  29. package/src/components/TextEditor/icons/arrow-go-forward-line.vue +14 -0
  30. package/src/components/TextEditor/icons/bold.vue +14 -0
  31. package/src/components/TextEditor/icons/code-view.vue +14 -0
  32. package/src/components/TextEditor/icons/double-quotes-r.vue +14 -0
  33. package/src/components/TextEditor/icons/font-color.vue +14 -0
  34. package/src/components/TextEditor/icons/format-clear.vue +14 -0
  35. package/src/components/TextEditor/icons/h-1.vue +14 -0
  36. package/src/components/TextEditor/icons/h-2.vue +14 -0
  37. package/src/components/TextEditor/icons/h-3.vue +14 -0
  38. package/src/components/TextEditor/icons/h-4.vue +14 -0
  39. package/src/components/TextEditor/icons/h-5.vue +14 -0
  40. package/src/components/TextEditor/icons/h-6.vue +14 -0
  41. package/src/components/TextEditor/icons/image-add-line.vue +14 -0
  42. package/src/components/TextEditor/icons/italic.vue +14 -0
  43. package/src/components/TextEditor/icons/link.vue +14 -0
  44. package/src/components/TextEditor/icons/list-ordered.vue +14 -0
  45. package/src/components/TextEditor/icons/list-unordered.vue +14 -0
  46. package/src/components/TextEditor/icons/readme.md +1 -0
  47. package/src/components/TextEditor/icons/separator.vue +14 -0
  48. package/src/components/TextEditor/icons/strikethrough.vue +14 -0
  49. package/src/components/TextEditor/icons/table-2.vue +14 -0
  50. package/src/components/TextEditor/icons/text.vue +11 -0
  51. package/src/components/TextEditor/icons/underline.vue +14 -0
  52. package/src/components/TextEditor/image-extension.js +152 -0
  53. package/src/components/TextEditor/mention.js +72 -0
  54. package/src/components/Toast.vue +26 -20
  55. package/src/components/Tooltip.vue +35 -0
  56. package/src/index.js +9 -0
  57. package/src/style.css +2 -2
  58. package/src/utils/file-to-base64.js +9 -0
  59. package/src/utils/pageMeta.js +50 -0
  60. package/src/utils/plugin.js +2 -2
  61. package/src/utils/resources.js +176 -25
  62. package/src/utils/socketio.js +10 -8
  63. package/src/utils/vite-dev-server.js +1 -1
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.0.19",
3
+ "version": "0.0.57",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
7
7
  "test": "npx prettier --check ./src",
8
- "prettier": "npx prettier -w ./src"
8
+ "prettier": "npx prettier -w ./src",
9
+ "prepare": "husky install",
10
+ "bump-and-release": "yarn version --patch && git push && git push --tags"
9
11
  },
10
12
  "files": [
11
13
  "src"
@@ -15,19 +17,37 @@
15
17
  "url": "https://github.com/amonak/pimelon-ui.git"
16
18
  },
17
19
  "author": "Alphamonak Solutions",
20
+ "license": "MIT",
18
21
  "dependencies": {
19
22
  "@headlessui/vue": "^1.5.0",
20
23
  "@popperjs/core": "^2.11.2",
21
24
  "@tailwindcss/forms": "^0.4.0",
22
25
  "@tailwindcss/typography": "^0.5.0",
23
- "@tiptap/extension-image": "^2.0.0-beta.27",
24
- "@tiptap/extension-placeholder": "^2.0.0-beta.48",
25
- "@tiptap/starter-kit": "^2.0.0-beta.183",
26
- "@tiptap/vue-3": "^2.0.0-beta.90",
26
+ "@tiptap/extension-image": "^2.0.0-beta.30",
27
+ "@tiptap/extension-link": "^2.0.0-beta.43",
28
+ "@tiptap/extension-mention": "^2.0.0-beta.102",
29
+ "@tiptap/extension-placeholder": "^2.0.0-beta.53",
30
+ "@tiptap/extension-table": "^2.0.0-beta.54",
31
+ "@tiptap/extension-table-cell": "^2.0.0-beta.23",
32
+ "@tiptap/extension-table-header": "^2.0.0-beta.25",
33
+ "@tiptap/extension-table-row": "^2.0.0-beta.22",
34
+ "@tiptap/extension-text-align": "^2.0.0-beta.31",
35
+ "@tiptap/starter-kit": "^2.0.0-beta.191",
36
+ "@tiptap/vue-3": "^2.0.0-beta.96",
27
37
  "autoprefixer": "^10.4.2",
28
38
  "feather-icons": "^4.28.0",
29
39
  "postcss": "^8.4.5",
30
- "socket.io-client": "^2.4.0",
31
- "tailwindcss": "^3.0.12"
40
+ "socket.io-client": "^4.5.1",
41
+ "tailwindcss": "^3.0.12",
42
+ "tippy.js": "^6.3.7"
43
+ },
44
+ "devDependencies": {
45
+ "husky": "^8.0.3",
46
+ "lint-staged": ">=10",
47
+ "prettier": "2.7.1",
48
+ "prettier-plugin-tailwindcss": "^0.1.13"
49
+ },
50
+ "lint-staged": {
51
+ "*.{js,css,md,vue}": "prettier --write"
32
52
  }
33
53
  }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="block w-full">
3
3
  <div
4
- class="items-start px-4 md:px-5 py-3.5 text-base rounded-md flex"
4
+ class="flex items-start rounded-md px-4 py-3.5 text-base md:px-5"
5
5
  :class="classes"
6
6
  >
7
7
  <svg
@@ -19,8 +19,8 @@
19
19
  fill="#318AD8"
20
20
  />
21
21
  </svg>
22
- <div class="w-full ml-2">
23
- <div class="flex flex-col md:items-baseline md:flex-row">
22
+ <div class="ml-2 w-full">
23
+ <div class="flex flex-col md:flex-row md:items-baseline">
24
24
  <h3 class="text-lg font-medium text-gray-900" v-if="title">
25
25
  {{ title }}
26
26
  </h3>
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
3
+ <Popover class="w-full">
4
+ <template #target="{ open: openPopover }">
5
+ <div class="w-full">
6
+ <ComboboxButton
7
+ class="flex w-full items-center justify-between rounded-md bg-gray-100 py-1.5 pl-3 pr-2"
8
+ :class="{ 'rounded-b-none': isComboboxOpen }"
9
+ @click="
10
+ () => {
11
+ openPopover()
12
+ }
13
+ "
14
+ >
15
+ <span
16
+ class="overflow-hidden text-ellipsis text-base"
17
+ v-if="selectedValue"
18
+ >
19
+ {{ displayValue(selectedValue) }}
20
+ </span>
21
+ <span class="text-base text-gray-500" v-else>
22
+ {{ placeholder || '' }}
23
+ </span>
24
+ <FeatherIcon
25
+ name="chevron-down"
26
+ class="h-4 w-4 text-gray-500"
27
+ aria-hidden="true"
28
+ />
29
+ </ComboboxButton>
30
+ </div>
31
+ </template>
32
+ <template #body>
33
+ <ComboboxOptions
34
+ class="max-h-[11rem] overflow-y-auto rounded-md rounded-t-none bg-white px-1.5 pb-1.5 shadow-md"
35
+ static
36
+ v-show="isComboboxOpen"
37
+ >
38
+ <div
39
+ class="items-st sticky top-0 mb-1.5 flex items-stretch space-x-1.5 bg-white pt-1.5"
40
+ >
41
+ <ComboboxInput
42
+ class="form-input w-full placeholder-gray-500"
43
+ type="text"
44
+ @change="
45
+ (e) => {
46
+ query = e.target.value
47
+ }
48
+ "
49
+ :value="query"
50
+ autocomplete="off"
51
+ placeholder="Search by keyword"
52
+ />
53
+ <Button icon="x" @click="selectedValue = null" />
54
+ </div>
55
+ <ComboboxOption
56
+ as="template"
57
+ v-for="option in filteredOptions"
58
+ :key="option.value"
59
+ :value="option"
60
+ v-slot="{ active, selected }"
61
+ >
62
+ <li
63
+ :class="[
64
+ 'rounded-md px-2.5 py-1.5 text-base',
65
+ { 'bg-gray-100': active },
66
+ ]"
67
+ >
68
+ {{ option.label }}
69
+ </li>
70
+ </ComboboxOption>
71
+ <li
72
+ v-if="filteredOptions.length == 0"
73
+ class="rounded-md px-2.5 py-1.5 text-base text-gray-600"
74
+ >
75
+ No results found
76
+ </li>
77
+ </ComboboxOptions>
78
+ </template>
79
+ </Popover>
80
+ </Combobox>
81
+ </template>
82
+ <script>
83
+ import {
84
+ Combobox,
85
+ ComboboxInput,
86
+ ComboboxOptions,
87
+ ComboboxOption,
88
+ ComboboxButton,
89
+ } from '@headlessui/vue'
90
+ import Popover from './Popover.vue'
91
+
92
+ export default {
93
+ name: 'Autocomplete',
94
+ props: ['modelValue', 'options', 'placeholder'],
95
+ emits: ['update:modelValue', 'change'],
96
+ components: {
97
+ Popover,
98
+ Combobox,
99
+ ComboboxInput,
100
+ ComboboxOptions,
101
+ ComboboxOption,
102
+ ComboboxButton,
103
+ },
104
+ data() {
105
+ return {
106
+ query: '',
107
+ }
108
+ },
109
+ computed: {
110
+ valuePropPassed() {
111
+ return 'value' in this.$attrs
112
+ },
113
+ selectedValue: {
114
+ get() {
115
+ return this.valuePropPassed ? this.$attrs.value : this.modelValue
116
+ },
117
+ set(val) {
118
+ this.query = ''
119
+ this.$emit(this.valuePropPassed ? 'change' : 'update:modelValue', val)
120
+ },
121
+ },
122
+ filteredOptions() {
123
+ if (!this.query) {
124
+ return this.options
125
+ }
126
+ return this.options.filter((option) => {
127
+ let searchTexts = [option.label, option.value]
128
+ return searchTexts.some((text) =>
129
+ (text || '')
130
+ .toString()
131
+ .toLowerCase()
132
+ .includes(this.query.toLowerCase())
133
+ )
134
+ })
135
+ },
136
+ },
137
+ methods: {
138
+ displayValue(option) {
139
+ if (typeof option === 'string') {
140
+ return option
141
+ }
142
+ return option?.label
143
+ },
144
+ },
145
+ }
146
+ </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="overflow-hidden" :class="styleClasses">
2
+ <div class="shrink-0 overflow-hidden" :class="styleClasses">
3
3
  <img
4
4
  v-if="imageURL"
5
5
  :src="imageURL"
@@ -8,7 +8,7 @@
8
8
  />
9
9
  <div
10
10
  v-else
11
- class="flex items-center justify-center w-full h-full text-gray-600 uppercase bg-gray-200"
11
+ class="flex h-full w-full items-center justify-center bg-gray-200 uppercase text-gray-600"
12
12
  :class="{ sm: 'text-xs', md: 'text-base', lg: 'text-lg' }[size]"
13
13
  >
14
14
  {{ label && label[0] }}
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <span
3
- class="inline-block px-3 py-1 text-xs font-medium rounded-md cursor-default"
3
+ class="inline-block cursor-default rounded-md px-3 py-1 text-xs font-medium"
4
4
  :class="classes"
5
5
  >
6
6
  <slot>{{ status }}</slot>
@@ -9,31 +9,50 @@
9
9
  <script>
10
10
  export default {
11
11
  name: 'Badge',
12
- props: ['color', 'status'],
12
+ props: ['color', 'status', 'colorMap'],
13
+ data: {
14
+ defaultColorMap: {
15
+ Pending: 'yellow',
16
+ Running: 'yellow',
17
+ Success: 'green',
18
+ Failure: 'red',
19
+ Active: 'green',
20
+ Broken: 'red',
21
+ Updating: 'blue',
22
+ Rejected: 'red',
23
+ Published: 'green',
24
+ Approved: 'green',
25
+ },
26
+ },
13
27
  computed: {
14
28
  classes() {
15
- let color = this.color
16
- if (!color && this.status) {
17
- color = {
18
- Pending: 'yellow',
19
- Running: 'yellow',
20
- Success: 'green',
21
- Failure: 'red',
22
- Active: 'green',
23
- Broken: 'red',
24
- Updating: 'blue',
25
- Rejected: 'red',
26
- Published: 'green',
27
- Approved: 'green',
28
- }[this.status]
29
- }
30
- return {
29
+ let color = this.getBadgeColor()
30
+
31
+ let cssClasses = {
31
32
  gray: 'text-gray-700 bg-gray-50',
32
33
  green: 'text-green-700 bg-green-50',
33
34
  red: 'text-red-700 bg-red-50',
34
35
  yellow: 'text-yellow-700 bg-yellow-50',
35
36
  blue: 'text-blue-700 bg-blue-50',
36
- }[color || 'gray']
37
+ }[color]
38
+
39
+ return cssClasses
40
+ },
41
+ },
42
+ methods: {
43
+ getBadgeColor() {
44
+ let color = this.color
45
+ if (color) {
46
+ return color
47
+ }
48
+
49
+ let statusColorMap = Object.assign(
50
+ this.defaultColorMap,
51
+ this.colorMap || {}
52
+ )
53
+ color = statusColorMap[this.status] || 'gray'
54
+
55
+ return color
37
56
  },
38
57
  },
39
58
  }
@@ -7,23 +7,26 @@
7
7
  >
8
8
  <LoadingIndicator
9
9
  v-if="loading"
10
+ class="mr-2 -ml-1 h-3 w-3"
10
11
  :class="{
11
12
  'text-white': appearance == 'primary',
12
13
  'text-gray-600': appearance == 'secondary',
13
14
  'text-red-200': appearance == 'danger',
15
+ 'text-green-200': appearance == 'success',
16
+ 'text-yellow-200': appearance == 'warning',
14
17
  }"
15
18
  />
16
19
  <FeatherIcon
17
20
  v-else-if="iconLeft"
18
21
  :name="iconLeft"
19
- class="w-4 h-4 mr-1.5"
22
+ class="mr-1.5 h-4 w-4"
20
23
  aria-hidden="true"
21
24
  />
22
25
  <template v-if="loading && loadingText">{{ loadingText }}</template>
23
26
  <template v-else-if="icon">
24
- <FeatherIcon :name="icon" class="w-4 h-4" :aria-label="label" />
27
+ <FeatherIcon :name="icon" class="h-4 w-4" :aria-label="label" />
25
28
  </template>
26
- <span :class="icon ? 'sr-only' : ''">
29
+ <span v-else :class="icon ? 'sr-only' : ''">
27
30
  <slot>
28
31
  {{ label }}
29
32
  </slot>
@@ -31,7 +34,7 @@
31
34
  <FeatherIcon
32
35
  v-if="iconRight"
33
36
  :name="iconRight"
34
- class="w-4 h-4 ml-2"
37
+ class="ml-2 h-4 w-4"
35
38
  aria-hidden="true"
36
39
  />
37
40
  </button>
@@ -40,7 +43,15 @@
40
43
  import FeatherIcon from './FeatherIcon.vue'
41
44
  import LoadingIndicator from './LoadingIndicator.vue'
42
45
 
43
- const ValidAppearances = ['primary', 'secondary', 'danger', 'white', 'minimal']
46
+ const ValidAppearances = [
47
+ 'primary',
48
+ 'secondary',
49
+ 'danger',
50
+ 'success',
51
+ 'warning',
52
+ 'white',
53
+ 'minimal',
54
+ ]
44
55
 
45
56
  export default {
46
57
  name: 'Button',
@@ -98,19 +109,23 @@ export default {
98
109
  buttonClasses() {
99
110
  let appearanceClasses = {
100
111
  primary:
101
- 'bg-blue-500 hover:bg-blue-600 text-white focus:ring-2 focus:ring-offset-2 focus:ring-blue-500',
112
+ 'bg-blue-500 hover:bg-blue-600 border-transparent text-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500',
102
113
  secondary:
103
- 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-2 focus:ring-offset-2 focus:ring-gray-500',
114
+ 'bg-gray-100 hover:bg-gray-200 border-transparent text-gray-900 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500',
104
115
  danger:
105
- 'bg-red-500 hover:bg-red-400 text-white focus:ring-2 focus:ring-offset-2 focus:ring-red-500',
116
+ 'bg-red-500 hover:bg-red-400 border-transparent text-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-red-500',
117
+ success:
118
+ 'bg-green-500 hover:bg-green-400 border-transparent text-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-green-500',
119
+ warning:
120
+ 'bg-yellow-500 hover:bg-yellow-400 border-transparent text-white focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-yellow-500',
106
121
  white:
107
- 'bg-white text-gray-900 border hover:bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-gray-400',
108
- minimal: `active:bg-gray-200 focus:bg-gray-200 text-gray-900 ${
122
+ 'bg-white text-gray-900 border-gray-200 hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-400',
123
+ minimal: `active:bg-gray-200 border-transparent focus:bg-gray-200 text-gray-900 ${
109
124
  this.active ? 'bg-gray-200' : 'bg-transparent hover:bg-gray-200'
110
125
  }`,
111
126
  }
112
127
  return [
113
- 'inline-flex items-center justify-center text-base leading-5 rounded-md transition-colors focus:outline-none',
128
+ 'inline-flex items-center justify-center text-base leading-5 rounded-md border transition-colors focus:outline-none',
114
129
  this.icon ? 'p-1.5' : 'px-3 py-1',
115
130
  this.isDisabled
116
131
  ? 'opacity-50 cursor-not-allowed pointer-events-none'
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex flex-col px-6 py-5 bg-white border rounded-lg shadow">
2
+ <div class="flex flex-col rounded-lg border bg-white px-6 py-5 shadow">
3
3
  <div class="flex items-baseline justify-between">
4
4
  <div class="flex items-baseline space-x-2">
5
5
  <div class="flex items-center space-x-2" v-if="$slots['actions-left']">
@@ -11,16 +11,16 @@
11
11
  <slot name="actions"></slot>
12
12
  </div>
13
13
  </div>
14
- <p class="text-base text-gray-600 mt-1.5" v-if="subtitle">
14
+ <p class="mt-1.5 text-base text-gray-600" v-if="subtitle">
15
15
  {{ subtitle }}
16
16
  </p>
17
17
  <div
18
18
  v-if="loading"
19
- class="flex flex-col items-center justify-center flex-auto mt-4 rounded-md"
19
+ class="mt-4 flex flex-auto flex-col items-center justify-center rounded-md"
20
20
  >
21
21
  <LoadingText />
22
22
  </div>
23
- <div class="flex-auto mt-4 overflow-auto" v-else-if="$slots['default']">
23
+ <div class="mt-4 flex-auto overflow-auto" v-else-if="$slots['default']">
24
24
  <slot></slot>
25
25
  </div>
26
26
  </div>
@@ -0,0 +1,247 @@
1
+ <template>
2
+ <Popover @open="selectCurrentMonthYear" transition="default">
3
+ <template #target="{ togglePopover }">
4
+ <Input
5
+ type="text"
6
+ :class="inputClass"
7
+ :value="
8
+ modelValue && formatValue ? formatValue(modelValue) : modelValue || ''
9
+ "
10
+ :placeholder="placeholder"
11
+ @focus="!readonly ? togglePopover() : null"
12
+ readonly
13
+ />
14
+ </template>
15
+ <template #body-main="{ togglePopover }">
16
+ <div class="mt-1 select-none p-3 text-left">
17
+ <div class="flex items-center justify-between">
18
+ <span class="text-base font-medium text-blue-500">
19
+ {{ formatMonth }}
20
+ </span>
21
+ <span class="flex">
22
+ <div
23
+ class="grid h-5 w-5 cursor-pointer place-items-center rounded-md hover:bg-gray-100"
24
+ >
25
+ <FeatherIcon
26
+ @click="prevMonth"
27
+ name="chevron-left"
28
+ class="h-4 w-4"
29
+ />
30
+ </div>
31
+ <div
32
+ class="ml-2 grid h-5 w-5 cursor-pointer place-items-center rounded-md hover:bg-gray-100"
33
+ >
34
+ <FeatherIcon
35
+ @click="nextMonth"
36
+ name="chevron-right"
37
+ class="h-4 w-4"
38
+ />
39
+ </div>
40
+ </span>
41
+ </div>
42
+ <div class="mt-2 text-sm">
43
+ <div class="grid w-full grid-cols-7 place-items-center text-gray-600">
44
+ <div
45
+ class="grid h-6 w-6 place-items-center gap-1 text-center"
46
+ v-for="(d, i) in ['S', 'M', 'T', 'W', 'T', 'F', 'S']"
47
+ :key="i"
48
+ >
49
+ {{ d }}
50
+ </div>
51
+ </div>
52
+ <div v-for="(week, i) in datesAsWeeks" :key="i" class="mt-1">
53
+ <div class="grid w-full grid-cols-7 place-items-center gap-1">
54
+ <div
55
+ v-for="date in week"
56
+ :key="toValue(date)"
57
+ class="grid h-6 w-6 cursor-pointer place-items-center rounded-md hover:bg-blue-100 hover:text-blue-700"
58
+ :class="{
59
+ 'text-gray-600': date.getMonth() !== currentMonth - 1,
60
+ 'text-blue-500': toValue(date) === toValue(today),
61
+ 'bg-blue-100 font-semibold text-blue-500':
62
+ toValue(date) === modelValue,
63
+ }"
64
+ @click="
65
+ () => {
66
+ selectDate(date)
67
+ togglePopover()
68
+ }
69
+ "
70
+ >
71
+ {{ date.getDate() }}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ <div class="mt-2 flex w-full justify-end">
77
+ <div
78
+ class="cursor-pointer rounded-md px-2 py-1 text-sm hover:bg-gray-100"
79
+ @click="
80
+ () => {
81
+ selectDate('')
82
+ togglePopover()
83
+ }
84
+ "
85
+ >
86
+ Clear
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </template>
91
+ </Popover>
92
+ </template>
93
+
94
+ <script>
95
+ import Popover from './Popover.vue'
96
+
97
+ export default {
98
+ name: 'DatePicker',
99
+ props: ['modelValue', 'placeholder', 'readonly', 'formatValue', 'inputClass'],
100
+ emits: ['update:modelValue'],
101
+ components: {
102
+ Popover,
103
+ },
104
+ data() {
105
+ return {
106
+ currentYear: null,
107
+ currentMonth: null,
108
+ }
109
+ },
110
+ created() {
111
+ this.selectCurrentMonthYear()
112
+ },
113
+ computed: {
114
+ today() {
115
+ return this.getDate()
116
+ },
117
+ datesAsWeeks() {
118
+ let datesAsWeeks = []
119
+ let dates = this.dates.slice()
120
+ while (dates.length) {
121
+ let week = dates.splice(0, 7)
122
+ datesAsWeeks.push(week)
123
+ }
124
+ return datesAsWeeks
125
+ },
126
+ dates() {
127
+ if (!(this.currentYear && this.currentMonth)) {
128
+ return []
129
+ }
130
+ let monthIndex = this.currentMonth - 1
131
+ let year = this.currentYear
132
+
133
+ let firstDayOfMonth = this.getDate(year, monthIndex, 1)
134
+ let lastDayOfMonth = this.getDate(year, monthIndex + 1, 0)
135
+ let leftPaddingCount = firstDayOfMonth.getDay()
136
+ let rightPaddingCount = 6 - lastDayOfMonth.getDay()
137
+
138
+ let leftPadding = this.getDatesAfter(firstDayOfMonth, -leftPaddingCount)
139
+ let rightPadding = this.getDatesAfter(lastDayOfMonth, rightPaddingCount)
140
+ let daysInMonth = this.getDaysInMonth(monthIndex, year)
141
+ let datesInMonth = this.getDatesAfter(firstDayOfMonth, daysInMonth - 1)
142
+
143
+ let dates = [
144
+ ...leftPadding,
145
+ firstDayOfMonth,
146
+ ...datesInMonth,
147
+ ...rightPadding,
148
+ ]
149
+ if (dates.length < 42) {
150
+ const finalPadding = this.getDatesAfter(dates.at(-1), 42 - dates.length)
151
+ dates = dates.concat(...finalPadding)
152
+ }
153
+ return dates
154
+ },
155
+ formatMonth() {
156
+ let date = this.getDate(this.currentYear, this.currentMonth - 1, 1)
157
+ return date.toLocaleString('en-US', { month: 'short', year: 'numeric' })
158
+ },
159
+ },
160
+ methods: {
161
+ selectDate(date) {
162
+ this.$emit('update:modelValue', this.toValue(date))
163
+ },
164
+ selectCurrentMonthYear() {
165
+ let date = this.modelValue
166
+ ? this.getDate(this.modelValue)
167
+ : this.getDate()
168
+ this.currentYear = date.getFullYear()
169
+ this.currentMonth = date.getMonth() + 1
170
+ },
171
+ prevMonth() {
172
+ this.changeMonth(-1)
173
+ },
174
+ nextMonth() {
175
+ this.changeMonth(1)
176
+ },
177
+ changeMonth(adder) {
178
+ this.currentMonth = this.currentMonth + adder
179
+ if (this.currentMonth < 1) {
180
+ this.currentMonth = 12
181
+ this.currentYear = this.currentYear - 1
182
+ }
183
+ if (this.currentMonth > 12) {
184
+ this.currentMonth = 1
185
+ this.currentYear = this.currentYear + 1
186
+ }
187
+ },
188
+ getDatesAfter(date, count) {
189
+ let incrementer = 1
190
+ if (count < 0) {
191
+ incrementer = -1
192
+ count = Math.abs(count)
193
+ }
194
+ let dates = []
195
+ while (count) {
196
+ date = this.getDate(
197
+ date.getFullYear(),
198
+ date.getMonth(),
199
+ date.getDate() + incrementer
200
+ )
201
+ dates.push(date)
202
+ count--
203
+ }
204
+ if (incrementer === -1) {
205
+ return dates.reverse()
206
+ }
207
+ return dates
208
+ },
209
+
210
+ getDaysInMonth(monthIndex, year) {
211
+ let daysInMonthMap = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
212
+ let daysInMonth = daysInMonthMap[monthIndex]
213
+ if (monthIndex === 1 && this.isLeapYear(year)) {
214
+ return 29
215
+ }
216
+ return daysInMonth
217
+ },
218
+
219
+ isLeapYear(year) {
220
+ if (year % 400 === 0) return true
221
+ if (year % 100 === 0) return false
222
+ if (year % 4 === 0) return true
223
+ return false
224
+ },
225
+
226
+ toValue(date) {
227
+ if (!date) {
228
+ return ''
229
+ }
230
+
231
+ // toISOString is buggy and reduces the day by one
232
+ // this is because it considers the UTC timestamp
233
+ // in order to circumvent that we need to use luxon/moment
234
+ // but that refactor could take some time, so fixing the time difference
235
+ // as suggested in this answer.
236
+ // https://stackoverflow.com/a/16084846/3541205
237
+ date.setHours(0, -date.getTimezoneOffset(), 0, 0)
238
+ return date.toISOString().slice(0, 10)
239
+ },
240
+
241
+ getDate(...args) {
242
+ let d = new Date(...args)
243
+ return d
244
+ },
245
+ },
246
+ }
247
+ </script>