@ditojs/admin 2.100.0 → 3.1.0
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/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3555 -2719
- package/dist/dito-admin.umd.js +5 -6
- package/package.json +84 -46
- package/src/DitoAdmin.js +1 -65
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +23 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoButtons.vue +14 -1
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +152 -84
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +34 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +6 -6
- package/src/components/DitoRouterView.vue +27 -0
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +21 -3
- package/src/components/index.js +1 -0
- package/src/mixins/DitoMixin.js +53 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +132 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +155 -96
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +37 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +20 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +5 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +154 -0
- package/src/utils/FormModel.js +615 -0
- package/src/utils/data.js +20 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/request.js +70 -0
- package/src/utils/schema/data.js +659 -0
- package/src/utils/schema/lookup.js +250 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/units.js +70 -3
- package/types/index.d.ts +174 -59
- package/src/mixins/DataMixin.js +0 -131
- package/src/utils/schema.js +0 -1121
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { isObject, isString } from '@ditojs/utils'
|
|
2
|
+
import { getTypeOptions, getSourceType } from './types.js'
|
|
3
|
+
|
|
4
|
+
export function iterateSchemaComponents(schemas, callback) {
|
|
5
|
+
for (const schema of schemas) {
|
|
6
|
+
if (isSingleComponentView(schema)) {
|
|
7
|
+
const res = callback(schema.component, schema.name, 0)
|
|
8
|
+
if (res !== undefined) {
|
|
9
|
+
return res
|
|
10
|
+
}
|
|
11
|
+
} else if (isSchema(schema)) {
|
|
12
|
+
for (const [name, component] of Object.entries(schema.components || {})) {
|
|
13
|
+
const res = callback(component, name, 1)
|
|
14
|
+
if (res !== undefined) {
|
|
15
|
+
return res
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function iterateNestedSchemaComponents(schema, callback) {
|
|
23
|
+
return schema
|
|
24
|
+
? iterateSchemaComponents([schema, ...getTabSchemas(schema)], callback)
|
|
25
|
+
: undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function findNestedSchemaComponent(schema, callback) {
|
|
29
|
+
return (
|
|
30
|
+
iterateNestedSchemaComponents(
|
|
31
|
+
schema,
|
|
32
|
+
component => (callback(component) ? component : undefined)
|
|
33
|
+
) ?? null
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function someNestedSchemaComponent(schema, callback) {
|
|
38
|
+
return (
|
|
39
|
+
iterateNestedSchemaComponents(
|
|
40
|
+
schema,
|
|
41
|
+
(component, name) => (callback(component, name) ? true : undefined)
|
|
42
|
+
) ?? false
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function everyNestedSchemaComponent(schema, callback) {
|
|
47
|
+
return (
|
|
48
|
+
iterateNestedSchemaComponents(
|
|
49
|
+
schema,
|
|
50
|
+
component => (callback(component) ? undefined : false)
|
|
51
|
+
) ?? true
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasNestedSchemaComponents(schema) {
|
|
56
|
+
return someNestedSchemaComponent(schema, () => true) ?? false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns whether `schema` has a component named `name` in its components or
|
|
61
|
+
* tabs, or in those of its unnested components, e.g. sections without
|
|
62
|
+
* `nested: true`, which share the data of `schema` without adding their own
|
|
63
|
+
* name to the data path.
|
|
64
|
+
*/
|
|
65
|
+
export function hasComponentNamed(schema, name) {
|
|
66
|
+
const found = iterateNestedSchemaComponents(
|
|
67
|
+
schema,
|
|
68
|
+
(component, componentName) => {
|
|
69
|
+
if (
|
|
70
|
+
isNested(component)
|
|
71
|
+
? componentName === name
|
|
72
|
+
: hasComponentNamed(component, name)
|
|
73
|
+
) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
return !!found
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isSchema(schema) {
|
|
82
|
+
return isObject(schema) && isString(schema.type)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isForm(schema) {
|
|
86
|
+
return isSchema(schema) && schema.type === 'form'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isView(schema) {
|
|
90
|
+
return isSchema(schema) && schema.type === 'view'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isTab(schema) {
|
|
94
|
+
return isSchema(schema) && schema.type === 'tab'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isPanel(schema) {
|
|
98
|
+
return isSchema(schema) && schema.type === 'panel'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Returns whether `schema` is a panel that provides its own data through
|
|
102
|
+
// `schema.data`, e.g. a filters panel, instead of sharing the data of its
|
|
103
|
+
// schema, see `DitoPanel`.
|
|
104
|
+
export function isPanelWithOwnData(schema) {
|
|
105
|
+
return isPanel(schema) && !!schema.data
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isMenu(schema) {
|
|
109
|
+
return isSchema(schema) && schema.type === 'menu'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getSchemaIdentifier(schema) {
|
|
113
|
+
return JSON.stringify(schema)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isSingleComponentView(schema) {
|
|
117
|
+
return (
|
|
118
|
+
isView(schema) &&
|
|
119
|
+
isObject(schema.component)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isCompact(schema) {
|
|
124
|
+
return !!schema.compact
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isInlined(schema) {
|
|
128
|
+
return !!(schema.inlined || schema.components)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isNested(schema) {
|
|
132
|
+
return !!(schema.nested || getTypeOptions(schema)?.defaultNested === true)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function hasLabel(schema, generateLabels) {
|
|
136
|
+
const { label } = schema
|
|
137
|
+
return (
|
|
138
|
+
label !== false && (
|
|
139
|
+
!!label ||
|
|
140
|
+
generateLabels && getTypeOptions(schema)?.generateLabel
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function omitSpacing(schema) {
|
|
146
|
+
return !!getTypeOptions(schema)?.omitSpacing
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getTabSchemas(schema) {
|
|
150
|
+
return schema?.tabs ? Object.values(schema.tabs) : []
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getPanelSchemas(schema) {
|
|
154
|
+
return schema?.panels ? Object.values(schema.panels) : []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isObjectSource(schemaOrType) {
|
|
158
|
+
return getSourceType(schemaOrType) === 'object'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function isListSource(schemaOrType) {
|
|
162
|
+
return getSourceType(schemaOrType) === 'list'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns whether `schema` is a source with its own resource, whose items are
|
|
167
|
+
* loaded, validated and submitted through their own forms, separately from
|
|
168
|
+
* the data that contains them.
|
|
169
|
+
*/
|
|
170
|
+
export function isSourceWithResource(schema) {
|
|
171
|
+
return !!getSourceType(schema) && !!schema.resource
|
|
172
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isObject } from '@ditojs/utils'
|
|
2
|
+
|
|
3
|
+
const typeComponents = {}
|
|
4
|
+
|
|
5
|
+
const unknownTypeReported = {}
|
|
6
|
+
|
|
7
|
+
export function registerTypeComponent(type, component) {
|
|
8
|
+
typeComponents[type] = component
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTypeComponent(type, allowNull = false) {
|
|
12
|
+
const component = typeComponents[type] || null
|
|
13
|
+
if (!component && !allowNull && !unknownTypeReported[type]) {
|
|
14
|
+
// Report each missing type only once, to avoid flooding the console:
|
|
15
|
+
unknownTypeReported[type] = true
|
|
16
|
+
throw new Error(`Unknown Dito.js component type: '${type}'`)
|
|
17
|
+
}
|
|
18
|
+
return component
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getType(schemaOrType) {
|
|
22
|
+
return isObject(schemaOrType) ? schemaOrType.type : schemaOrType
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getTypeOptions(schemaOrType) {
|
|
26
|
+
return getTypeComponent(getType(schemaOrType), true) ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getSourceType(schemaOrType) {
|
|
30
|
+
return (
|
|
31
|
+
getTypeOptions(schemaOrType)?.getSourceType?.(getType(schemaOrType)) ??
|
|
32
|
+
null
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import DitoContext from '../../DitoContext.js'
|
|
2
|
+
import * as validators from '../../validators/index.js'
|
|
3
|
+
import { isFunction, isRegExp, asArray, normalizeDataPath } from '@ditojs/utils'
|
|
4
|
+
import { getTypeOptions } from './types.js'
|
|
5
|
+
import { isSourceWithResource } from './structure.js'
|
|
6
|
+
import {
|
|
7
|
+
getSchemaValue,
|
|
8
|
+
shouldRenderSchema,
|
|
9
|
+
processSchemaData
|
|
10
|
+
} from './data.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the validations of a component schema, without requiring a
|
|
14
|
+
* component: the validations of the type's static `getTypeValidations()`
|
|
15
|
+
* option, `required`, and finally the `schema.rules` overrides, where
|
|
16
|
+
* `undefined` removes a validation. Schema values are evaluated with `context`.
|
|
17
|
+
*/
|
|
18
|
+
export function getValidations(schema, context) {
|
|
19
|
+
const validations = {
|
|
20
|
+
...getTypeOptions(schema)?.getTypeValidations?.(schema, context)
|
|
21
|
+
}
|
|
22
|
+
if (getSchemaValue('required', { type: Boolean, schema, context })) {
|
|
23
|
+
validations.required = true
|
|
24
|
+
}
|
|
25
|
+
// Allow schema to override default rules and add any new ones:
|
|
26
|
+
for (const [name, setting] of Object.entries(schema.rules || {})) {
|
|
27
|
+
if (setting === undefined) {
|
|
28
|
+
delete validations[name]
|
|
29
|
+
} else {
|
|
30
|
+
validations[name] = setting
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return validations
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the errors of validating `value` against `validations`, in the
|
|
38
|
+
* format of server errors, `[{ message }]`, with messages without the label
|
|
39
|
+
* prefix, or an empty array if the value is valid.
|
|
40
|
+
*/
|
|
41
|
+
export function getValueValidationErrors(value, validations) {
|
|
42
|
+
const errors = []
|
|
43
|
+
for (const [name, setting] of Object.entries(validations)) {
|
|
44
|
+
// eslint-disable-next-line import/namespace
|
|
45
|
+
const validator = validators[name]
|
|
46
|
+
if (
|
|
47
|
+
validator &&
|
|
48
|
+
// Only apply 'required' validator to empty values.
|
|
49
|
+
// Apply all other validators only to non-empty values.
|
|
50
|
+
(name === 'required' || value != null && value !== '')
|
|
51
|
+
) {
|
|
52
|
+
const { validate, message } = validator
|
|
53
|
+
if (!validate(value, setting, validations)) {
|
|
54
|
+
errors.push({
|
|
55
|
+
message: isFunction(message)
|
|
56
|
+
? message(value, setting, validations)
|
|
57
|
+
: message
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return errors
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates `data` along the components of `schema`, including tabs, panels
|
|
67
|
+
* and the nested forms of list and object items, skipping components whose
|
|
68
|
+
* `if` evaluates to `false` and sources with their own resource. Returns the
|
|
69
|
+
* errors in the format of server errors, `{ [dataPath]: [{ message }] }`, or
|
|
70
|
+
* `null` if the data is valid.
|
|
71
|
+
*/
|
|
72
|
+
export function getDataValidationErrors(schema, data, {
|
|
73
|
+
dataPath = '',
|
|
74
|
+
component = null,
|
|
75
|
+
rootData = data
|
|
76
|
+
} = {}) {
|
|
77
|
+
let errors = null
|
|
78
|
+
|
|
79
|
+
const createContext = ({ schema, data, name, dataPath }) =>
|
|
80
|
+
new DitoContext(component, {
|
|
81
|
+
schema,
|
|
82
|
+
value: name != null ? data[name] : undefined,
|
|
83
|
+
name,
|
|
84
|
+
data,
|
|
85
|
+
dataPath,
|
|
86
|
+
rootData
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const shouldProcess = entry => (
|
|
90
|
+
!isSourceWithResource(entry.schema) &&
|
|
91
|
+
shouldRenderSchema(entry.schema, createContext(entry))
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const before = entry => {
|
|
95
|
+
const { schema, data, name, dataPath } = entry
|
|
96
|
+
const context = createContext(entry)
|
|
97
|
+
let value = data[name]
|
|
98
|
+
// Validate the value as displayed, like the components do, see
|
|
99
|
+
// `ValueMixin.value`:
|
|
100
|
+
const { format } = schema
|
|
101
|
+
if (isFunction(format)) {
|
|
102
|
+
value = format(context)
|
|
103
|
+
}
|
|
104
|
+
const validations = getValidations(schema, context)
|
|
105
|
+
const valueErrors = getValueValidationErrors(value, validations)
|
|
106
|
+
if (valueErrors.length > 0) {
|
|
107
|
+
errors ||= {}
|
|
108
|
+
errors[dataPath] = valueErrors
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
processSchemaData(schema, data, {
|
|
113
|
+
dataPath,
|
|
114
|
+
shouldProcess,
|
|
115
|
+
before,
|
|
116
|
+
options: { component, rootData }
|
|
117
|
+
})
|
|
118
|
+
return errors
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns the entries of `errors` whose data paths match `match`, or `null` if
|
|
123
|
+
* none match. `match` can be a function receiving the data path, a regular
|
|
124
|
+
* expression, a data path, or an array of data paths.
|
|
125
|
+
*/
|
|
126
|
+
export function getMatchingValidationErrors(errors, match) {
|
|
127
|
+
let isMatchingDataPath
|
|
128
|
+
if (isFunction(match)) {
|
|
129
|
+
isMatchingDataPath = match
|
|
130
|
+
} else if (isRegExp(match)) {
|
|
131
|
+
isMatchingDataPath = dataPath => match.test(dataPath)
|
|
132
|
+
} else {
|
|
133
|
+
const dataPaths = asArray(match).map(normalizeDataPath)
|
|
134
|
+
isMatchingDataPath = dataPath => dataPaths.includes(dataPath)
|
|
135
|
+
}
|
|
136
|
+
const matchingEntries = Object.entries(errors || {}).filter(
|
|
137
|
+
([dataPath]) => isMatchingDataPath(dataPath)
|
|
138
|
+
)
|
|
139
|
+
return matchingEntries.length > 0
|
|
140
|
+
? Object.fromEntries(matchingEntries)
|
|
141
|
+
: null
|
|
142
|
+
}
|
package/src/utils/units.js
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
|
-
|
|
1
|
+
// Unit prefixes in order of their exponent, shared by parsing and formatting.
|
|
2
|
+
const prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Formats a number of bytes like `'1.5 MB'`, using base 10 (1 kB = 1000 bytes)
|
|
6
|
+
* unless specified. Base 2 uses IEC units (1 KiB = 1024 bytes).
|
|
7
|
+
*/
|
|
8
|
+
export function formatFileSize(size, { base = 10 } = {}) {
|
|
9
|
+
const multiplier = getMultiplier(base)
|
|
10
|
+
const absolute = Math.abs(size)
|
|
11
|
+
let exponent = Math.min(
|
|
12
|
+
absolute >= 1 ? Math.floor(Math.log(absolute) / Math.log(multiplier)) : 0,
|
|
13
|
+
prefixes.length - 1
|
|
14
|
+
)
|
|
15
|
+
let amount = roundFileSize(absolute, multiplier, exponent)
|
|
16
|
+
// Rounding can reach the next unit, e.g. 999999 bytes to 1000 kB = 1 MB.
|
|
17
|
+
if (amount >= multiplier && exponent < prefixes.length - 1) {
|
|
18
|
+
amount = roundFileSize(absolute, multiplier, ++exponent)
|
|
19
|
+
}
|
|
20
|
+
const prefix = prefixes[exponent]
|
|
21
|
+
const unit = !prefix
|
|
22
|
+
? 'B'
|
|
23
|
+
: base === 10
|
|
24
|
+
? `${prefix}B`
|
|
25
|
+
: `${prefix.toUpperCase()}iB`
|
|
26
|
+
return `${size < 0 ? '-' : ''}${amount} ${unit}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses file sizes like `'10 MB'`, `'1.5G'`, `'512kb'` or `'100 bytes'` into
|
|
31
|
+
* a number of bytes, using base 10 (1 kB = 1000 bytes) unless specified.
|
|
32
|
+
* IEC units like `'10 MiB'` always use base 2 (1 KiB = 1024 bytes).
|
|
33
|
+
* Bit units are case-sensitive (`Kb` = kilobit), the byte units aren't.
|
|
34
|
+
*/
|
|
35
|
+
export function parseFileSize(input, { base = 10 } = {}) {
|
|
36
|
+
const [, amountString, unit] = (
|
|
37
|
+
String(input).match(/^([\d.,]*)\s*(\D*)$/) || []
|
|
38
|
+
)
|
|
39
|
+
const amount = parseFloat(amountString?.replace(',', '.'))
|
|
40
|
+
if (!isFinite(amount)) {
|
|
41
|
+
throw new Error(`Can't interpret file size: ${input}`)
|
|
42
|
+
}
|
|
43
|
+
const multiplier = getMultiplier(/^[kmgtpezy]ib?$/i.test(unit) ? 2 : base)
|
|
44
|
+
const bitMatch = unit.replace(/^bits?$/, 'b').match(/^([KMGTPEZY]?)b$/)
|
|
45
|
+
if (bitMatch) {
|
|
46
|
+
const exponent = prefixes.indexOf(bitMatch[1].replace('K', 'k'))
|
|
47
|
+
return Math.round(amount * multiplier ** exponent / 8)
|
|
48
|
+
}
|
|
49
|
+
const exponent = prefixes.findIndex(
|
|
50
|
+
prefix => (
|
|
51
|
+
prefix.toLowerCase() ===
|
|
52
|
+
unit
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/^(bytes?|b)$/, '')
|
|
55
|
+
.replace(/^(\w)(i?b|i)$/, '$1')
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if (exponent >= 0) {
|
|
59
|
+
return Math.round(amount * multiplier ** exponent)
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Invalid file size unit: ${unit}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getMultiplier(base) {
|
|
65
|
+
return base === 10 ? 1000 : 1024
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function roundFileSize(size, multiplier, exponent) {
|
|
69
|
+
// Round bytes to integers, larger units to two decimals.
|
|
70
|
+
const factor = exponent > 0 ? 100 : 1
|
|
71
|
+
return Math.round(size / multiplier ** exponent * factor) / factor
|
|
5
72
|
}
|