frappe-ui 0.0.27 → 0.0.30

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": "frappe-ui",
3
- "version": "0.0.27",
3
+ "version": "0.0.30",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -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="p-3 mt-1 text-left select-none">
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 w-5 h-5 rounded-md cursor-pointer hover:bg-gray-100 place-items-center"
24
+ >
25
+ <FeatherIcon
26
+ @click="prevMonth"
27
+ name="chevron-left"
28
+ class="w-4 h-4"
29
+ />
30
+ </div>
31
+ <div
32
+ class="grid w-5 h-5 ml-2 rounded-md cursor-pointer hover:bg-gray-100 place-items-center"
33
+ >
34
+ <FeatherIcon
35
+ @click="nextMonth"
36
+ name="chevron-right"
37
+ class="w-4 h-4"
38
+ />
39
+ </div>
40
+ </span>
41
+ </div>
42
+ <div class="mt-2 text-sm">
43
+ <div class="grid w-full grid-cols-7 text-gray-600 place-items-center">
44
+ <div
45
+ class="grid w-6 h-6 gap-1 text-center place-items-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 gap-1 place-items-center">
54
+ <div
55
+ v-for="date in week"
56
+ :key="toValue(date)"
57
+ class="grid w-6 h-6 rounded-md cursor-pointer place-items-center 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="flex justify-end w-full mt-2">
77
+ <div
78
+ class="px-2 py-1 text-sm rounded-md cursor-pointer 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>
@@ -1,16 +1,18 @@
1
1
  <template>
2
2
  <div ref="reference">
3
3
  <div
4
- class="h-full"
5
4
  ref="target"
5
+ class="inline-block"
6
6
  @click="updatePosition"
7
7
  @focusin="updatePosition"
8
8
  @keydown="updatePosition"
9
+ @mouseover="onMouseover"
10
+ @mouseleave="onMouseleave"
9
11
  >
10
12
  <slot
11
13
  name="target"
12
14
  v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
13
- ></slot>
15
+ />
14
16
  </div>
15
17
  <teleport to="#frappeui-popper-root">
16
18
  <div
@@ -20,17 +22,25 @@
20
22
  :style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
21
23
  v-show="isOpen"
22
24
  >
23
- <transition v-bind="transition">
25
+ <transition v-bind="popupTransition">
24
26
  <div v-show="isOpen">
25
- <div
26
- v-if="!hideArrow"
27
- class="popover-arrow"
28
- ref="popover-arrow"
29
- ></div>
30
27
  <slot
31
- name="content"
28
+ name="body"
32
29
  v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
33
- ></slot>
30
+ >
31
+ <div class="bg-white border border-gray-100 rounded-lg shadow-xl">
32
+ <slot
33
+ name="body-main"
34
+ v-bind="{
35
+ togglePopover,
36
+ updatePosition,
37
+ open,
38
+ close,
39
+ isOpen,
40
+ }"
41
+ />
42
+ </div>
43
+ </slot>
34
44
  </div>
35
45
  </transition>
36
46
  </div>
@@ -44,12 +54,16 @@ import { createPopper } from '@popperjs/core'
44
54
  export default {
45
55
  name: 'Popover',
46
56
  props: {
47
- hideArrow: {
48
- type: Boolean,
49
- default: true,
50
- },
51
57
  show: {
52
- default: null,
58
+ default: undefined,
59
+ },
60
+ trigger: {
61
+ type: String,
62
+ default: 'click', // click, hover
63
+ },
64
+ hoverDelay: {
65
+ type: Number,
66
+ default: 0,
53
67
  },
54
68
  right: Boolean,
55
69
  placement: {
@@ -58,29 +72,25 @@ export default {
58
72
  },
59
73
  popoverClass: [String, Object, Array],
60
74
  transition: {
61
- type: Object,
62
75
  default: null,
63
76
  },
64
77
  },
65
- emits: ['init', 'open', 'close'],
66
- watch: {
67
- show: {
68
- immediate: true,
69
- handler(val) {
70
- if (val) {
71
- this.open()
72
- } else {
73
- this.close()
74
- }
75
- },
76
- },
77
- },
78
+ emits: ['open', 'close', 'update:show'],
78
79
  data() {
79
80
  return {
80
- isOpen: false,
81
+ showPopup: false,
81
82
  targetWidth: null,
82
83
  }
83
84
  },
85
+ watch: {
86
+ show(val) {
87
+ if (val) {
88
+ this.open()
89
+ } else {
90
+ this.close()
91
+ }
92
+ },
93
+ },
84
94
  created() {
85
95
  if (!document.getElementById('frappeui-popper-root')) {
86
96
  const root = document.createElement('div')
@@ -99,7 +109,7 @@ export default {
99
109
  }
100
110
  this.close()
101
111
  }
102
- if (this.show == null) {
112
+ if (!this.showPropPassed) {
103
113
  document.addEventListener('click', this.listener)
104
114
  }
105
115
  this.$nextTick(() => {
@@ -110,37 +120,65 @@ export default {
110
120
  this.popper && this.popper.destroy()
111
121
  document.removeEventListener('click', this.listener)
112
122
  },
123
+ computed: {
124
+ showPropPassed() {
125
+ return this.show != null
126
+ },
127
+ isOpen: {
128
+ get() {
129
+ if (this.showPropPassed) {
130
+ return this.show
131
+ }
132
+ return this.showPopup
133
+ },
134
+ set(val) {
135
+ val = Boolean(val)
136
+ if (this.showPropPassed) {
137
+ this.$emit('update:show', val)
138
+ } else {
139
+ this.showPopup = val
140
+ }
141
+ if (val === false) {
142
+ this.$emit('close')
143
+ } else if (val === true) {
144
+ this.$emit('open')
145
+ }
146
+ },
147
+ },
148
+ popupTransition() {
149
+ let templates = {
150
+ default: {
151
+ enterActiveClass: 'transition duration-200 ease-out',
152
+ enterFromClass: 'translate-y-1 opacity-0',
153
+ enterToClass: 'translate-y-0 opacity-100',
154
+ leaveActiveClass: 'transition duration-150 ease-in',
155
+ leaveFromClass: 'translate-y-0 opacity-100',
156
+ leaveToClass: 'translate-y-1 opacity-0',
157
+ },
158
+ }
159
+ if (typeof this.transition === 'string') {
160
+ return templates[this.transition]
161
+ }
162
+ return this.transition
163
+ },
164
+ },
113
165
  methods: {
114
166
  setupPopper() {
115
167
  if (!this.popper) {
116
168
  this.popper = createPopper(this.$refs.reference, this.$refs.popover, {
117
169
  placement: this.placement,
118
- modifiers: !this.hideArrow
119
- ? [
120
- {
121
- name: 'arrow',
122
- options: {
123
- element: this.$refs['popover-arrow'],
124
- },
125
- },
126
- {
127
- name: 'offset',
128
- options: {
129
- offset: [0, 10],
130
- },
131
- },
132
- ]
133
- : [],
134
170
  })
135
171
  } else {
136
172
  this.updatePosition()
137
173
  }
138
- this.$emit('init')
139
174
  },
140
175
  updatePosition() {
141
176
  this.popper && this.popper.update()
142
177
  },
143
178
  togglePopover(flag) {
179
+ if (flag instanceof Event) {
180
+ flag = null
181
+ }
144
182
  if (flag == null) {
145
183
  flag = !this.isOpen
146
184
  }
@@ -157,49 +195,32 @@ export default {
157
195
  }
158
196
  this.isOpen = true
159
197
  this.$nextTick(() => this.setupPopper())
160
- this.$emit('open')
161
198
  },
162
199
  close() {
163
200
  if (!this.isOpen) {
164
201
  return
165
202
  }
166
203
  this.isOpen = false
167
- this.$emit('close')
204
+ },
205
+ onMouseover() {
206
+ if (this.trigger === 'hover') {
207
+ if (this.hoverDelay) {
208
+ this.hoverTimer = setTimeout(() => {
209
+ this.open()
210
+ }, Number(this.hoverDelay) * 1000)
211
+ } else {
212
+ this.open()
213
+ }
214
+ }
215
+ },
216
+ onMouseleave() {
217
+ if (this.hoverTimer) {
218
+ clearTimeout(this.hoverTimer)
219
+ }
220
+ if (this.trigger === 'hover') {
221
+ this.close()
222
+ }
168
223
  },
169
224
  },
170
225
  }
171
226
  </script>
172
- <style scoped>
173
- .popover-arrow,
174
- .popover-arrow::after {
175
- position: absolute;
176
- width: theme('spacing.4');
177
- height: theme('spacing.4');
178
- z-index: -1;
179
- }
180
-
181
- .popover-arrow::after {
182
- content: '';
183
- background: white;
184
- transform: rotate(45deg);
185
- border-top: 1px solid theme('borderColor.gray.400');
186
- border-left: 1px solid theme('borderColor.gray.400');
187
- border-top-left-radius: 6px;
188
- }
189
-
190
- .popover-container[data-popper-placement^='top'] > .popover-arrow {
191
- bottom: calc(theme('spacing.2') * -1);
192
- }
193
-
194
- .popover-container[data-popper-placement^='bottom'] > .popover-arrow {
195
- top: calc(theme('spacing.2') * -1);
196
- }
197
-
198
- .popover-container[data-popper-placement^='left'] > .popover-arrow {
199
- right: calc(theme('spacing.2') * -1);
200
- }
201
-
202
- .popover-container[data-popper-placement^='right'] > .popover-arrow {
203
- left: calc(theme('spacing.2') * -1);
204
- }
205
- </style>
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ export { default as Avatar } from './components/Avatar.vue'
4
4
  export { default as Badge } from './components/Badge.vue'
5
5
  export { default as Button } from './components/Button.vue'
6
6
  export { default as Card } from './components/Card.vue'
7
+ export { default as DatePicker } from './components/DatePicker.vue'
7
8
  export { default as Dialog } from './components/Dialog.vue'
8
9
  export { default as Dropdown } from './components/Dropdown.vue'
9
10
  export { default as ErrorMessage } from './components/ErrorMessage.vue'
@@ -29,6 +30,7 @@ export { default as onOutsideClickDirective } from './directives/onOutsideClick.
29
30
  export { default as call, createCall } from './utils/call.js'
30
31
  export { default as debounce } from './utils/debounce.js'
31
32
  export { createResource } from './utils/resources.js'
33
+ export { default as pageMeta } from './utils/pageMeta.js'
32
34
 
33
35
  // plugin
34
36
  export { default as FrappeUI } from './utils/plugin.js'
@@ -0,0 +1,47 @@
1
+ import { watch } from 'vue'
2
+
3
+ export default {
4
+ install(app) {
5
+ app.mixin(createMixin())
6
+ },
7
+ }
8
+
9
+ function createMixin() {
10
+ let faviconRef = document.querySelector('link[rel="icon"]')
11
+ let defaultFavIcon = faviconRef.href
12
+
13
+ return {
14
+ created() {
15
+ if (this.$options.pageMeta) {
16
+ watch(
17
+ () => {
18
+ try {
19
+ return this.$options.pageMeta.call(this)
20
+ } catch (error) {
21
+ console.warn('Failed to parse pageMeta\n\n', error)
22
+ return null
23
+ }
24
+ },
25
+ (pageMeta) => {
26
+ if (!pageMeta) return
27
+ if (pageMeta.title) {
28
+ document.title = pageMeta.title
29
+ }
30
+ if (pageMeta.emoji) {
31
+ let href = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${pageMeta.emoji}</text></svg>`
32
+ faviconRef.href = href
33
+ } else if (pageMeta.icon) {
34
+ faviconRef.href = pageMeta.icon
35
+ } else {
36
+ faviconRef.href = defaultFavIcon
37
+ }
38
+ },
39
+ {
40
+ immediate: true,
41
+ deep: true,
42
+ }
43
+ )
44
+ }
45
+ },
46
+ }
47
+ }