Skip to content

Commit 1652e74

Browse files
authored
[6.x] Number formatter (#14373)
1 parent 948b3dd commit 1652e74

12 files changed

Lines changed: 404 additions & 8 deletions

File tree

.storybook/preview.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import './theme.css';
88
import {translate} from '@/translations/translator';
99
import registerUiComponents from '@/bootstrap/ui';
1010
import DateFormatter from '@/components/DateFormatter';
11+
import NumberFormatter from '@/components/NumberFormatter';
1112
import cleanCodeSnippet from './clean-code-snippet';
1213
import PortalVue from 'portal-vue';
1314
import FullscreenHeader from '@/components/publish/FullscreenHeader.vue';
@@ -63,6 +64,7 @@ setup(async (app) => {
6364

6465
app.config.globalProperties.__ = translate;
6566
app.config.globalProperties.$date = new DateFormatter;
67+
app.config.globalProperties.$number = new NumberFormatter;
6668
app.config.globalProperties.cp_url = (url) => url;
6769
app.config.globalProperties.$portals = portals;
6870
app.config.globalProperties.$stacks = stacks;

packages/cms/src/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const {
1414
hooks,
1515
inertia,
1616
keys,
17+
numberFormatter,
1718
permissions,
1819
portals,
1920
preferences,

packages/cms/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const {
88
IndexFieldtypeMixin,
99
InlineEditForm,
1010
DateFormatter,
11+
NumberFormatter,
1112
ItemActions,
1213
RelatedItem,
1314
RestoreRevision,

resources/js/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Reveal from './components/Reveal.js';
1414
import Echo from './components/Echo.js';
1515
import Permission from './components/Permission.js';
1616
import DateFormatter from './components/DateFormatter.js';
17+
import NumberFormatter from './components/NumberFormatter.js';
1718
import CommandPalette from './components/CommandPalette.js';
1819
import ColorMode from './components/ColorMode.js';
1920
import Contrast from './components/Contrast.js';
@@ -39,6 +40,7 @@ export const reveal = new Reveal();
3940
export const echo = new Echo();
4041
export const permissions = new Permission();
4142
export const dateFormatter = new DateFormatter();
43+
export const numberFormatter = new NumberFormatter();
4244
export const commandPalette = new CommandPalette();
4345
export const colorMode = new ColorMode();
4446
export const contrast = new Contrast();

resources/js/bootstrap/cms/core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { default as IndexFieldtype } from '../../components/fieldtypes/index-fie
33
export { default as FieldtypeMixin } from '../../components/fieldtypes/Fieldtype.vue';
44
export { default as IndexFieldtypeMixin } from '../../components/fieldtypes/IndexFieldtype.vue';
55
export { default as DateFormatter } from '../../components/DateFormatter.js';
6+
export { default as NumberFormatter } from '../../components/NumberFormatter.js';
67
export { default as ItemActions } from '../../components/actions/ItemActions.vue';
78
export { default as InlineEditForm } from '../../components/inputs/relationship/InlineEditForm.vue';
89
export { default as RelatedItem } from '../../components/inputs/relationship/Item.vue';

resources/js/bootstrap/statamic.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import VueComponentDebug from 'vue-component-debug';
1919
import { registerIconSetFromStrings } from '@ui';
2020
import Layout from '@/pages/layout/Layout.vue';
2121
import { setTranslations, setLocale } from '@/translations/translator.js';
22+
import { setDefaultLocale as setFormattingLocale } from '@/components/FormattingLocale.js';
2223
import {
2324
keys,
2425
components,
@@ -35,6 +36,7 @@ import {
3536
echo,
3637
permissions,
3738
dateFormatter,
39+
numberFormatter,
3840
commandPalette,
3941
colorMode,
4042
contrast,
@@ -123,6 +125,10 @@ export default {
123125
return dateFormatter;
124126
},
125127

128+
get $number() {
129+
return numberFormatter;
130+
},
131+
126132
get $progress() {
127133
return progress;
128134
},
@@ -178,9 +184,9 @@ export default {
178184

179185
const formattingLocale = this.initialConfig.user?.preferences?.formatting_locale;
180186
if (formattingLocale === 'language') {
181-
dateFormatter.setDefaultLocale(this.initialConfig.translationLocale);
187+
setFormattingLocale(this.initialConfig.translationLocale);
182188
} else if (formattingLocale) {
183-
dateFormatter.setDefaultLocale(formattingLocale);
189+
setFormattingLocale(formattingLocale);
184190
}
185191

186192
bootingCallbacks.forEach((callback) => callback(this));
@@ -281,6 +287,7 @@ export default {
281287
$echo: echo,
282288
$permissions: permissions,
283289
$date: dateFormatter,
290+
$number: numberFormatter,
284291
$commandPalette: commandPalette,
285292
$colorMode: colorMode,
286293
$contrast: contrast,

resources/js/components/DateFormatter.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getLocale, getDefaultLocale, setDefaultLocale } from './FormattingLocale.js';
2+
13
export default class DateFormatter {
24
#date;
35
#options;
@@ -102,23 +104,31 @@ export default class DateFormatter {
102104
return this.date(date).options(options).toString();
103105
}
104106

107+
static get defaultLocale() {
108+
return getDefaultLocale();
109+
}
110+
111+
static set defaultLocale(locale) {
112+
setDefaultLocale(locale);
113+
}
114+
105115
withLocale(locale, callback) {
106-
const previousLocale = DateFormatter.defaultLocale;
107-
this.setDefaultLocale(locale);
116+
const previousLocale = getDefaultLocale();
117+
setDefaultLocale(locale);
108118

109119
try {
110120
return callback(this);
111121
} finally {
112-
this.setDefaultLocale(previousLocale);
122+
setDefaultLocale(previousLocale);
113123
}
114124
}
115125

116126
setDefaultLocale(locale) {
117-
DateFormatter.defaultLocale = locale;
127+
setDefaultLocale(locale);
118128
}
119129

120130
get locale() {
121-
return DateFormatter.defaultLocale ?? Intl.DateTimeFormat().resolvedOptions().locale;
131+
return getLocale();
122132
}
123133

124134
#normalizeDate(date) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
let defaultLocale = null;
2+
3+
export function getLocale() {
4+
return defaultLocale ?? Intl.DateTimeFormat().resolvedOptions().locale;
5+
}
6+
7+
export function getDefaultLocale() {
8+
return defaultLocale;
9+
}
10+
11+
export function setDefaultLocale(locale) {
12+
defaultLocale = locale;
13+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { getLocale, getDefaultLocale, setDefaultLocale } from './FormattingLocale.js';
2+
3+
export default class NumberFormatter {
4+
#number;
5+
#rangeEnd;
6+
#options;
7+
#presets = {
8+
decimal: {
9+
style: 'decimal',
10+
},
11+
percent: {
12+
style: 'percent',
13+
},
14+
};
15+
16+
constructor(number, options) {
17+
if (Array.isArray(number)) {
18+
this.#number = this.#normalizeNumber(number[0]);
19+
this.#rangeEnd = this.#normalizeNumber(number[1]);
20+
} else {
21+
this.#number = this.#normalizeNumber(number);
22+
}
23+
24+
this.#options = this.#normalizeOptions(options);
25+
}
26+
27+
number(value) {
28+
return new NumberFormatter(value, this.#options);
29+
}
30+
31+
options(options) {
32+
const value = this.#rangeEnd !== undefined ? [this.#number, this.#rangeEnd] : this.#number;
33+
34+
return new NumberFormatter(value, options);
35+
}
36+
37+
toString() {
38+
try {
39+
if (this.#rangeEnd !== undefined) {
40+
return Intl.NumberFormat(this.locale, this.#options).formatRange(this.#number, this.#rangeEnd);
41+
}
42+
43+
return Intl.NumberFormat(this.locale, this.#options).format(this.#number);
44+
} catch (e) {
45+
return 'Invalid Number';
46+
}
47+
}
48+
49+
static format(number, options) {
50+
return new NumberFormatter(number, options).toString();
51+
}
52+
53+
format(number, options) {
54+
return this.number(number).options(options).toString();
55+
}
56+
57+
formatRange(start, end, options) {
58+
return this.number([start, end]).options(options ?? this.#options).toString();
59+
}
60+
61+
static formatRange(start, end, options) {
62+
return new NumberFormatter([start, end], options).toString();
63+
}
64+
65+
static get defaultLocale() {
66+
return getDefaultLocale();
67+
}
68+
69+
static set defaultLocale(locale) {
70+
setDefaultLocale(locale);
71+
}
72+
73+
withLocale(locale, callback) {
74+
const previousLocale = getDefaultLocale();
75+
setDefaultLocale(locale);
76+
77+
try {
78+
return callback(this);
79+
} finally {
80+
setDefaultLocale(previousLocale);
81+
}
82+
}
83+
84+
setDefaultLocale(locale) {
85+
setDefaultLocale(locale);
86+
}
87+
88+
get locale() {
89+
return getLocale();
90+
}
91+
92+
#normalizeNumber(number) {
93+
if (number === null || number === undefined) return 0;
94+
95+
const n = Number(number);
96+
97+
if (Number.isNaN(n)) throw new Error('Invalid Number');
98+
99+
return n;
100+
}
101+
102+
#normalizeOptions(options) {
103+
if (!options) options = 'decimal';
104+
105+
if (typeof options === 'string') {
106+
if (!this.#presets[options]) throw new Error(`Invalid number format: ${options}`);
107+
108+
return this.#presets[options];
109+
}
110+
111+
return options;
112+
}
113+
}

resources/js/components/ui/Pagination.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ function getRange(start, end) {
155155
<div class="flex">
156156
<div class="flex flex-1 items-center">
157157
<div class="text-sm text-gray-600 dark:text-gray-500" v-if="showTotals && totalItems > 0">
158-
{{ __(':start-:end of :total', { start: fromItem, end: toItem, total: totalItems }) }}
158+
{{ __(':range of :total', {
159+
range: $number.formatRange(fromItem, toItem),
160+
total: $number.format(totalItems)
161+
}) }}
159162
</div>
160163
</div>
161164

0 commit comments

Comments
 (0)