quasar-ui-danx 0.4.9 → 0.4.10

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": "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;