quasar-ui-danx 0.4.9 → 0.4.10

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -1,27 +1,30 @@
1
1
  <template>
2
- <span class="dx-field-label">
2
+ <div class="dx-field-label">
3
3
  <slot>
4
- {{ labelText }}
5
- <template v-if="showName">({{ field?.name }})</template>
4
+ <div class="dx-field-label-text">
5
+ <div class="dx-field-label-label">
6
+ {{ label }}
7
+ </div>
8
+ <div
9
+ v-if="name"
10
+ class="dx-field-label-name"
11
+ >
12
+ ({{ name }})
13
+ </div>
14
+ </div>
6
15
  </slot>
7
16
  <span
8
- v-if="requiredLabel"
17
+ v-if="required"
9
18
  class="dx-field-required"
10
- >{{ requiredLabel }}</span>
11
- </span>
19
+ >{{ requiredLabel || "*" }}</span>
20
+ </div>
12
21
  </template>
13
22
 
14
23
  <script setup lang="ts">
15
- import { computed } from "vue";
16
- import { FormField } from "../../../../types";
17
-
18
- const props = defineProps<{
19
- field?: FormField;
24
+ defineProps<{
20
25
  label?: string;
21
- showName?: boolean;
26
+ name?: string;
22
27
  required?: boolean;
28
+ requiredLabel?: string;
23
29
  }>();
24
-
25
- const labelText = computed(() => props.label || props.field?.label);
26
- const requiredLabel = computed(() => props.field?.required_group || (props.required || props.field?.required ? "*" : ""));
27
30
  </script>
@@ -1,22 +1,51 @@
1
1
  <template>
2
2
  <div>
3
- <div class="text-xs font-bold">{{ label }}</div>
4
- <div :class="{'mt-2': !dense, 'mt-1': dense, 'text-no-wrap': nowrap}">
5
- <slot>{{ value || "-" }}</slot>
3
+ <div class="text-xs font-bold">
4
+ {{ label }}
5
+ </div>
6
+ <div :class="valueClass">
7
+ <a
8
+ v-if="url"
9
+ target="_blank"
10
+ :href="url"
11
+ :class="valueClass"
12
+ >
13
+ <slot>{{ formattedValue }}</slot>
14
+ </a>
15
+ <template v-else>
16
+ <slot>{{ formattedValue }}</slot>
17
+ </template>
6
18
  </div>
7
19
  </div>
8
20
  </template>
9
- <script setup>
10
- defineProps({
11
- label: {
12
- type: String,
13
- required: true
14
- },
15
- value: {
16
- type: [String, Number],
17
- default: "-"
18
- },
19
- dense: Boolean,
20
- nowrap: Boolean
21
+ <script setup lang="ts">
22
+ import { computed } from "vue";
23
+ import { fBoolean, fNumber } from "../../../../helpers";
24
+
25
+ export interface LabelValueBlockProps {
26
+ label: string;
27
+ value: string | number | boolean;
28
+ url?: string;
29
+ dense?: boolean;
30
+ nowrap?: boolean;
31
+ }
32
+
33
+ const props = withDefaults(defineProps<LabelValueBlockProps>(), {
34
+ value: "",
35
+ url: ""
36
+ });
37
+
38
+ const valueClass = computed(() => ({ "mt-2": !props.dense, "mt-1": props.dense, "text-no-wrap": props.nowrap }));
39
+ const formattedValue = computed(() => {
40
+ switch (typeof props.value) {
41
+ case "boolean":
42
+ return fBoolean(props.value);
43
+
44
+ case "number":
45
+ return fNumber(props.value);
46
+
47
+ default:
48
+ return props.value || "-";
49
+ }
21
50
  });
22
51
  </script>
@@ -12,7 +12,6 @@
12
12
  :field="field"
13
13
  :no-label="!field.label"
14
14
  label-class="text-xs font-bold text-zinc-800"
15
- parent-class="tight-label"
16
15
  input-class="!py-0"
17
16
  dense
18
17
  type="textarea"
@@ -29,29 +28,29 @@ import TextField from "./TextField";
29
28
 
30
29
  const emit = defineEmits(["update:model-value"]);
31
30
  const props = defineProps({
32
- modelValue: {
33
- type: [String, Number, Object],
34
- default: ""
35
- },
36
- field: {
37
- type: Object,
38
- default: null
39
- }
31
+ modelValue: {
32
+ type: [String, Number, Object],
33
+ default: ""
34
+ },
35
+ field: {
36
+ type: Object,
37
+ default: null
38
+ }
40
39
  });
41
40
 
42
41
  const selectedFieldName = ref(props.field.defaultOption);
43
42
  const searchList = computed(() => props.modelValue && props.modelValue[selectedFieldName.value]);
44
43
  const textInput = ref(formatModelValue());
45
44
  function onChange() {
46
- textInput.value = textInput.value?.replace(/\n/g, ",").replace(/,{2,}/g, ",") || "";
47
- emit("update:model-value", textInput.value ? { [selectedFieldName.value]: textInput.value.split(",") } : undefined);
45
+ textInput.value = textInput.value?.replace(/\n/g, ",").replace(/,{2,}/g, ",") || "";
46
+ emit("update:model-value", textInput.value ? { [selectedFieldName.value]: textInput.value.split(",") } : undefined);
48
47
  }
49
48
 
50
49
  function formatModelValue() {
51
- return Array.isArray(searchList.value) ? searchList.value?.join(",") : "";
50
+ return Array.isArray(searchList.value) ? searchList.value?.join(",") : "";
52
51
  }
53
52
 
54
53
  watch(() => props.modelValue, () => {
55
- textInput.value = formatModelValue();
54
+ textInput.value = formatModelValue();
56
55
  });
57
56
  </script>
@@ -1,72 +1,49 @@
1
1
  <template>
2
- <QInput
3
- class="dx-number-field max-w-full"
4
- :class="{'dx-no-prepend-label': hidePrependLabel, 'dx-prepend-label': !hidePrependLabel}"
2
+ <TextField
3
+ class="dx-number-field"
4
+ v-bind="$props"
5
5
  :model-value="numberVal"
6
- :data-testid="'number-field-' + fieldOptions.id"
7
- :placeholder="fieldOptions.placeholder"
8
- outlined
9
- dense
10
- inputmode="numeric"
11
- :input-class="inputClass"
12
6
  @update:model-value="onInput"
13
- >
14
- <template #prepend>
15
- <FieldLabel
16
- :field="fieldOptions"
17
- :show-name="showName"
18
- />
19
- </template>
20
- </QInput>
7
+ />
21
8
  </template>
22
9
 
23
- <script setup>
10
+ <script setup lang="ts">
24
11
  import { useDebounceFn } from "@vueuse/core";
25
- import { computed, nextTick, ref, watch } from "vue";
12
+ import { nextTick, ref, watch } from "vue";
26
13
  import { fNumber } from "../../../../helpers";
27
- import FieldLabel from "./FieldLabel";
14
+ import { AnyObject, TextFieldProps } from "../../../../types";
15
+ import TextField from "./TextField";
28
16
 
29
17
  const emit = defineEmits(["update:model-value", "update"]);
30
- const props = defineProps({
31
- modelValue: {
32
- type: [String, Number],
33
- default: ""
34
- },
35
- precision: {
36
- type: Number,
37
- default: 2
38
- },
39
- label: {
40
- type: String,
41
- default: undefined
42
- },
43
- field: {
44
- type: Object,
45
- default: null
46
- },
47
- inputClass: {
48
- type: [String, Object],
49
- default: ""
50
- },
51
- delay: {
52
- type: Number,
53
- default: 1000
54
- },
55
- hidePrependLabel: Boolean,
56
- currency: Boolean,
57
- showName: Boolean
18
+
19
+ export interface NumberFieldProps extends TextFieldProps {
20
+ precision?: number;
21
+ delay?: number;
22
+ currency?: boolean;
23
+ min?: number;
24
+ max?: number;
25
+ }
26
+
27
+ const props = withDefaults(defineProps<NumberFieldProps>(), {
28
+ modelValue: "",
29
+ precision: 2,
30
+ label: undefined,
31
+ delay: 1000,
32
+ min: undefined,
33
+ max: undefined
58
34
  });
59
35
 
60
36
  const numberVal = ref(format(props.modelValue));
61
- watch(() => props.modelValue, () => numberVal.value = format(props.modelValue));
62
37
 
63
- const fieldOptions = computed(() => props.field || { label: props.label || "", placeholder: "", id: "" });
38
+ watch(() => props.modelValue, () => numberVal.value = format(props.modelValue));
64
39
 
65
40
  function format(number) {
66
41
  if (!number && number !== 0 && number !== "0") return number;
67
42
 
43
+ if (props.type === "number") return number;
44
+
68
45
  const minimumFractionDigits = Math.min(props.precision, ("" + number).split(".")[1]?.length || 0);
69
- let options = {
46
+ let options: AnyObject = {
70
47
  minimumFractionDigits
71
48
  };
72
49
 
@@ -80,14 +57,15 @@ function format(number) {
80
57
  return fNumber(number, options);
81
58
  }
82
59
 
83
- const onUpdateDebounced = useDebounceFn((val) => emit("update", val), props.delay);
60
+ const onUpdateDebounced = useDebounceFn((val: number | string | undefined) => emit("update", val), props.delay);
84
61
 
85
62
  function onInput(value) {
86
- let number = "";
63
+ let number: number | undefined = undefined;
87
64
 
88
65
  // Prevent invalid characters
89
66
  if (value.match(/[^\d.,$]/)) {
90
67
  const oldVal = numberVal.value;
68
+
91
69
  // XXX: To get QInput to show only the value we want
92
70
  numberVal.value += " ";
93
71
  return nextTick(() => numberVal.value = oldVal);
@@ -95,11 +73,19 @@ function onInput(value) {
95
73
 
96
74
  if (value !== "") {
97
75
  value = value.replace(/[^\d.]/g, "");
98
- number = Number(value);
76
+ number = +value;
77
+
78
+ if (props.min) {
79
+ number = Math.max(number, props.min);
80
+ }
81
+ if (props.max) {
82
+ number = Math.min(number, props.max);
83
+ }
84
+
85
+ console.log("formattinged", number, value);
99
86
  numberVal.value = format(number);
100
87
  }
101
88
 
102
- number = number === "" ? undefined : number;
103
89
  emit("update:model-value", number);
104
90
 
105
91
  // Delay the change event, so we only see the value after the user has finished
@@ -37,8 +37,8 @@
37
37
  <template #selected>
38
38
  <div
39
39
  v-if="$props.multiple"
40
- class="flex gap-y-1 overflow-hidden"
41
- :class="{'flex-nowrap gap-y-0': chipLimit === 1, [selectionClass]: true}"
40
+ class="flex gap-y-1 overflow-hidden dx-selected-label"
41
+ :class="{'flex-nowrap gap-y-0': chipLimit === 1, 'dx-selected-chips': chipOptions.length > 0, [selectionClass]: true}"
42
42
  >
43
43
  <template v-if="chipOptions.length > 0">
44
44
  <QChip
@@ -62,6 +62,7 @@
62
62
  <div
63
63
  v-else
64
64
  :class="selectionClass"
65
+ class="dx-selected-label"
65
66
  >
66
67
  {{ selectedLabel }}
67
68
  </div>
@@ -254,8 +255,8 @@ function onUpdate(value) {
254
255
 
255
256
  value = value === "__null__" ? null : value;
256
257
 
257
- emit("update", value);
258
258
  emit("update:model-value", value);
259
+ emit("update", value);
259
260
  }
260
261
 
261
262
  /** XXX: This tells us when we should apply the filter. QSelect likes to trigger a new filter everytime you open the dropdown
@@ -1,15 +1,21 @@
1
1
  <template>
2
2
  <div>
3
3
  <FieldLabel
4
- :field="field"
4
+ v-if="!prependLabel"
5
5
  :label="label"
6
- :show-name="showName"
6
+ :required="required"
7
+ :required-label="requiredLabel"
7
8
  :class="labelClass"
8
- :value="readonly ? modelValue : ''"
9
9
  />
10
- <template v-if="!readonly">
10
+ <div
11
+ v-if="readonly"
12
+ class="dx-text-field-readonly-value"
13
+ >
14
+ {{ modelValue }}
15
+ </div>
16
+ <template v-else>
11
17
  <QInput
12
- :placeholder="field?.placeholder"
18
+ :placeholder="placeholder || (placeholder === '' ? '' : `Enter ${label}`)"
13
19
  outlined
14
20
  dense
15
21
  :readonly="readonly"
@@ -17,18 +23,31 @@
17
23
  :disable="disabled"
18
24
  :label-slot="!noLabel"
19
25
  :input-class="inputClass"
20
- :class="parentClass"
26
+ :class="{'dx-input-prepend-label': prependLabel}"
21
27
  stack-label
22
28
  :type="type"
23
29
  :model-value="modelValue"
24
- :maxlength="allowOverMax ? undefined : field?.maxLength"
30
+ :maxlength="allowOverMax ? undefined : maxLength"
25
31
  :debounce="debounce"
26
32
  @keydown.enter="$emit('submit')"
27
33
  @update:model-value="$emit('update:model-value', $event)"
28
- />
34
+ >
35
+ <template
36
+ v-if="prependLabel"
37
+ #prepend
38
+ >
39
+ <FieldLabel
40
+ class="dx-prepended-label"
41
+ :label="label"
42
+ :required="required"
43
+ :required-label="requiredLabel"
44
+ :class="labelClass"
45
+ />
46
+ </template>
47
+ </QInput>
29
48
  <MaxLengthCounter
30
- :length="modelValue?.length || 0"
31
- :max-length="field?.maxLength"
49
+ :length="(modelValue + '').length || 0"
50
+ :max-length="maxLength"
32
51
  />
33
52
  </template>
34
53
  </div>
@@ -46,9 +65,9 @@ withDefaults(defineProps<TextFieldProps>(), {
46
65
  type: "text",
47
66
  label: "",
48
67
  labelClass: "",
49
- parentClass: "",
50
68
  inputClass: "",
51
69
  maxLength: null,
52
- debounce: 0
70
+ debounce: 0,
71
+ placeholder: null
53
72
  });
54
73
  </script>
@@ -29,10 +29,9 @@ export function remoteDateTime(dateTimeString: string) {
29
29
  }
30
30
 
31
31
  /**
32
- * @param {DateTime|String} dateTime
33
- * @returns {DateTime|*}
32
+ * Parses a date string into a Luxon DateTime object
34
33
  */
35
- export function parseDateTime(dateTime: string | DateTime) {
34
+ export function parseDateTime(dateTime: string | DateTime | null): DateTime {
36
35
  if (typeof dateTime === "string") {
37
36
  dateTime = dateTime.replace("T", " ").replace(/\//g, "-");
38
37
  return DateTime.fromSQL(dateTime);
@@ -91,8 +90,8 @@ export function fDateTime(
91
90
  dateTime: string | DateTime | null = null,
92
91
  { format = "M/d/yy h:mma", empty = "- -" }: fDateOptions = {}
93
92
  ) {
94
- const formatted = (dateTime ? parseDateTime(dateTime) : DateTime.now()).toFormat(format).toLowerCase();
95
- return formatted === "invalid datetime" ? empty : formatted;
93
+ const formatted = parseDateTime(dateTime).toFormat(format).toLowerCase();
94
+ return ["Invalid DateTime", "invalid datetime"].includes(formatted) ? empty : formatted;
96
95
  }
97
96
 
98
97
  /**
@@ -151,16 +150,87 @@ export function fCurrency(amount: number, options?: object) {
151
150
  }).format(amount);
152
151
  }
153
152
 
153
+ /**
154
+ * Formats an amount into USD currency format without cents
155
+ */
156
+ export function fCurrencyNoCents(amount: number, options?: object) {
157
+ return fCurrency(amount, {
158
+ maximumFractionDigits: 0,
159
+ ...options
160
+ });
161
+ }
162
+
154
163
  /**
155
164
  * Formats a number into a human-readable format
156
- * @param number
157
- * @param options
158
- * @returns {string}
159
165
  */
160
- export function fNumber(number: number, options = {}) {
166
+ export function fNumber(number: number, options?: object) {
161
167
  return new Intl.NumberFormat("en-US", options).format(number);
162
168
  }
163
169
 
170
+ /**
171
+ * Formats a currency into a shorthand human-readable format (ie: $1.2M or $5K)
172
+ */
173
+ export function fShortCurrency(value: string | number, options?: { round: boolean }) {
174
+ return "$" + fShortNumber(value, options);
175
+ }
176
+
177
+ /**
178
+ * Formats a number into a shorthand human-readable format (ie: 1.2M or 5K)
179
+ */
180
+ export function fShortNumber(value: string | number, options?: { round: boolean }) {
181
+ const shorts = [
182
+ { pow: 3, unit: "K" },
183
+ { pow: 6, unit: "M" },
184
+ { pow: 9, unit: "B" },
185
+ { pow: 12, unit: "T" },
186
+ { pow: 15, unit: "Q" }
187
+ ];
188
+
189
+ let n = Math.round(+value);
190
+
191
+ const short = shorts.find(({ pow }) => Math.pow(10, pow) < n && Math.pow(10, pow + 3) > n) || null;
192
+
193
+ if (short) {
194
+ n = n / Math.pow(10, short.pow);
195
+ return options?.round
196
+ ? n + short.unit
197
+ : n.toFixed(n > 100 ? 0 : 1) + short.unit;
198
+ }
199
+
200
+ return n;
201
+ }
202
+
203
+ /**
204
+ * Formats a number into a human-readable size format (ie: 1.2MB or 5KB)
205
+ */
206
+ export function fShortSize(value: string | number) {
207
+ const powers = [
208
+ { pow: 0, unit: "B" },
209
+ { pow: 10, unit: "KB" },
210
+ { pow: 20, unit: "MB" },
211
+ { pow: 30, unit: "GB" },
212
+ { pow: 40, unit: "TB" },
213
+ { pow: 50, unit: "PB" },
214
+ { pow: 60, unit: "EB" },
215
+ { pow: 70, unit: "ZB" },
216
+ { pow: 80, unit: "YB" }
217
+ ];
218
+
219
+ const n = Math.round(+value);
220
+ const power = powers.find((p, i) => {
221
+ const nextPower = powers[i + 1];
222
+ return !nextPower || n < Math.pow(2, nextPower.pow + 10);
223
+ }) || powers[powers.length - 1];
224
+
225
+ const div = Math.pow(2, power.pow);
226
+
227
+ return Math.round(n / div) + " " + power.unit;
228
+ }
229
+
230
+ export function fBoolean(value: boolean) {
231
+ return value ? "Yes" : "No";
232
+ }
233
+
164
234
  /**
165
235
  * Truncates the string by removing chars from the middle of the string
166
236
  * @param str
@@ -1,5 +1,7 @@
1
1
  import { Ref, watch } from "vue";
2
2
 
3
+ export { useDebounceFn } from "@vueuse/core";
4
+
3
5
  /**
4
6
  * Sleep function to be used in conjunction with async await:
5
7
  *
@@ -58,6 +58,8 @@
58
58
  .q-field__marginal, .q-field__input, .q-field__label {
59
59
  color: inherit;
60
60
  height: auto;
61
+ min-height: 0;
62
+ line-height: inherit;
61
63
  }
62
64
  }
63
65
  }
@@ -1,5 +1,5 @@
1
1
  .dx-field-label {
2
- @apply text-xs block mb-1.5;
2
+ @apply text-xs mb-1.5 flex items-center space-x-1;
3
3
 
4
4
  .dx-field-required {
5
5
  @apply text-red-900 ml-0.5 bottom-1 relative;
@@ -10,22 +10,4 @@
10
10
  .q-field__label {
11
11
  @apply text-sm text-gray-700;
12
12
  }
13
-
14
- &.dx-number-field {
15
- &.dx-prepend-label {
16
- @apply text-xs text-black font-normal;
17
- }
18
-
19
- &.dx-no-prepend-label {
20
- @apply w-32;
21
-
22
- .q-field__native {
23
- @apply bg-white text-right;
24
- }
25
-
26
- .q-field__prepend {
27
- padding: 0;
28
- }
29
- }
30
- }
31
13
  }
@@ -1,15 +1,16 @@
1
1
  import { QInputProps } from "quasar";
2
- import { FormField } from "./forms";
3
2
 
4
3
  export interface TextFieldProps {
5
- modelValue?: string,
6
- field?: FormField,
7
- type?: QInputProps["type"],
8
- label?: string,
9
- labelClass?: string,
10
- parentClass?: string,
11
- inputClass?: string,
12
- allowOverMax?: boolean,
4
+ modelValue?: string | number;
5
+ type?: QInputProps["type"];
6
+ label?: string;
7
+ required?: boolean;
8
+ requiredLabel?: string;
9
+ prependLabel?: boolean;
10
+ placeholder?: string;
11
+ labelClass?: string | object;
12
+ inputClass?: string | object;
13
+ allowOverMax?: boolean;
13
14
  maxLength?: number;
14
15
  autogrow?: boolean;
15
16
  noLabel?: boolean;