Skip to content

Commit

Permalink
Money Format
Browse files Browse the repository at this point in the history
  • Loading branch information
leandrocfe committed Jul 2, 2024
1 parent 9d5d498 commit 60033de
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 54 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
}
],
"require": {
"php": "^8.1 || ^8.2",
"php": "^8.1 || ^8.2 || ^8.3",
"archtechx/money": "^0.5.1",
"filament/filament": "^3.0",
"illuminate/contracts": "^10.0 || ^11.0",
"laravellegends/pt-br-validator": "^10.0 || ^11.0",
"moneyphp/money": "^4.5",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
Expand Down
1 change: 0 additions & 1 deletion dist/mask.min.js

This file was deleted.

129 changes: 129 additions & 0 deletions resources/js/money.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* All credit goes to
* https://mary-ui.com
* https://github.com/lagden/currency
*
* It works perfect with some tweaks.
*/
class Currency {
unmaskedValue = 0

constructor(input, opts = {}) {
this.opts = {
keyEvent: 'input',
triggerOnBlur: false,
init: false,
backspace: false,
maskOpts: {},
...opts,
}

if (input instanceof HTMLInputElement === false) {
throw new TypeError('The input should be a HTMLInputElement')
}

// Add fraction on initial value if missing
const parts = String(input.value).split('.')
input.value = parts.length === 1 ? `${parts.shift()}.00` : `${parts.shift()}.${parts.pop().padEnd(2, '0')}`

this.input = input
this.events = new Set()

// Initialize
if (this.opts.init) {
this.input.value = Currency.masking(this.input.value, this.opts.maskOpts)
}

// Listener
this.input.addEventListener(this.opts.keyEvent, this)
this.events.add(this.opts.keyEvent)

this.input.addEventListener('click', this)
this.events.add('click')

if (this.opts.triggerOnBlur) {
this.input.addEventListener('blur', this)
this.events.add('blur')
}
}

static getUnmasked() {
return this.unmaskedValue
}

static position(v) {
const nums = new Set(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'])
const len = v.length

let cc = 0
for (let i = len - 1; i >= 0; i--) {
if (nums.has(v[i])) {
break
}
cc++
}

return String(v).length - cc
}

static masking(v, opts = {}) {
const {
empty = false,
locales = 'pt-BR',
options = {
minimumFractionDigits: 2,
},
} = opts

if (typeof v === 'number') {
v = v.toFixed(2)
}

const n = String(v).replace(/\D/g, '').replace(/^0+/g, '')
const t = n.padStart(3, '0')
const d = t.slice(-2)
const i = t.slice(0, t.length - 2)

if (empty && i === '0' && d === '00') {
return ''
}

this.unmaskedValue = `${i}.${d}`

return new Intl.NumberFormat(locales, options).format(this.unmaskedValue)
}

onMasking(event) {
if (this.opts.backspace && event?.inputType === 'deleteContentBackward') {
return
}

this.input.value = Currency.masking(this.input.value, this.opts.maskOpts)
const pos = Currency.position(this.input.value)
this.input.setSelectionRange(pos, pos)
}

onClick() {
const pos = Currency.position(this.input.value)
this.input.focus()
this.input.setSelectionRange(pos, pos)
}

destroy() {
for (const _event of this.events) {
this.input.removeEventListener(_event, this)
}
}

handleEvent(event) {
if (event.type === 'click') {
this.onClick(event)
} else {
this.onMasking(event)
}
}
}

if (!window.Currency) {
window.Currency = Currency
}
21 changes: 21 additions & 0 deletions src/Currencies/BRL.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Leandrocfe\FilamentPtbrFormFields\Currencies;

use ArchTech\Money\Currency;

class BRL extends Currency
{
public string $code = 'BRL';
public string $name = 'Real Brasileiro';
public float $rate = 1.0;
public int $mathDecimals = 2;
public int $displayDecimals = 2;
public int $rounding = 2;
public string $prefix = '';
public string $locale = 'pt-BR';
public string $decimalSeparator = ',';
public string $thousandsSeparator = '.';
}
21 changes: 21 additions & 0 deletions src/Currencies/USD.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Leandrocfe\FilamentPtbrFormFields\Currencies;

use ArchTech\Money\Currency;

class USD extends Currency
{
public string $code = 'USD';
public string $name = 'United States Dollar';
public float $rate = 1.0;
public int $mathDecimals = 2;
public int $displayDecimals = 2;
public int $rounding = 2;
public string $prefix = '';
public string $locale = 'en';
public string $decimalSeparator = '.';
public string $thousandsSeparator = ',';
}
12 changes: 11 additions & 1 deletion src/FilamentPtbrFormFieldsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
namespace Leandrocfe\FilamentPtbrFormFields;

use Filament\FilamentServiceProvider;
use Filament\Support\Facades\FilamentAsset;
use Spatie\LaravelPackageTools\Package;

use Filament\Support\Assets\Js;
class FilamentPtbrFormFieldsServiceProvider extends FilamentServiceProvider
{
public function packageBooted(): void
{
parent::packageBooted();

FilamentAsset::register([
Js::make('money-script', __DIR__ . '/../resources/js/money.js'),
]);
}

public function configurePackage(Package $package): void
{
/*
Expand Down
142 changes: 91 additions & 51 deletions src/Money.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,134 @@

namespace Leandrocfe\FilamentPtbrFormFields;

use ArchTech\Money\Currency;
use Closure;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Leandrocfe\FilamentPtbrFormFields\Currencies\BRL;

class Money extends TextInput
{
protected string|int|float|null $initialValue = '0,00';

protected ?Currency $currency = null;

protected bool|Closure $intFormat = false;

protected bool|Closure $dehydrateMask = false;

protected function setUp(): void
{
$this
->currency()
->prefix('R$')
->maxLength(13)
->extraAlpineAttributes([

'x-on:keypress' => 'function() {
var charCode = event.keyCode || event.which;
if (charCode < 48 || charCode > 57) {
event.preventDefault();
return false;
}
return true;
}',

'x-on:keyup' => 'function() {
var money = $el.value;
money = money.replace(/\D/g, \'\');
money = (parseFloat(money) / 100).toLocaleString(\'pt-BR\', { minimumFractionDigits: 2 });
$el.value = money === \'NaN\' ? \'0,00\' : money;
}',
])
->dehydrateMask()
->default(0.00)
->formatStateUsing(fn ($state) => $state ? number_format(floatval($state), 2, ',', '.') : $this->initialValue);
->extraAlpineAttributes(fn () => $this->getOnKeyPress())
->extraAlpineAttributes(fn () => $this->getOnKeyUp())
->formatStateUsing(fn ($state) => $this->hydrateCurrency($state))
->dehydrateStateUsing(fn ($state) => $this->dehydrateCurrency($state));
}

public function dehydrateMask(bool|Closure $condition = true): static
public function currency(string|null|Closure $currency = BRL::class): static
{
if ($condition) {
$this->dehydrateStateUsing(fn (?string $state): ?float => $this->convertToFloat($state));
} else {
$this->dehydrateStateUsing(fn (?string $state): ?string => $this->convertToNumberFormat($state));
$this->currency = new ($currency);
currencies()->add($currency);

if ($currency !== 'BRL') {
$this->prefix(null);
}

return $this;
}

public function initialValue(null|string|int|float|Closure $value = '0,00'): static
public function intFormat(bool|Closure $intFormat = true): static
{
$this->initialValue = $value;
$this->intFormat = $intFormat;

return $this;
}

private function sanitizeState(?string $state): ?Stringable
public function getIntFormat(): bool
{
$state = Str::of($state)
->replace('.', '')
->replace(',', '');
return $this->intFormat;
}

return $state ?? null;
protected function getOnKeyPress(): array
{
return [
'x-on:keypress' => 'function() {
var charCode = event.keyCode || event.which;
if (charCode < 48 || charCode > 57) {
event.preventDefault();
return false;
}
return true;
}',
];
}

private function convertToFloat(Stringable|string|null $state): float
protected function getOnKeyUp(): array
{
$state = $this->sanitizeState($state);
$currency = new ($this->getCurrency());
$numberFormatter = $currency->locale;

return [
'x-on:keyup' => 'function() {
$el.value = Currency.masking($el.value, {locales:\''.$numberFormatter.'\'});
}',
];
}

if (! $state) {
return 0;
}
protected function hydrateCurrency($state): string
{
$sanitized = $this->sanitizeState($state);

$money = money(amount: $sanitized, currency: $this->getCurrency());

if ($state->length() > 2) {
$state = $state
->substr(0, $state->length() - 2)
->append('.')
->append($state->substr($state->length() - 2, 2));
} else {
$state = $state->prepend('0.');
return $money->formatted(prefix: '');
}

protected function dehydrateCurrency($state): int|float|string
{
$sanitized = $this->sanitizeState($state);
$money = money(amount: $sanitized, currency: $this->getCurrency());

if($this->getDehydrateMask()) {
return $money->formatted();
}

return floatval($state->toString()) ?? 0;
return $this->getIntFormat() ? $money->value() : $money->decimal();
}

private function convertToNumberFormat(string $state): string
public function dehydrateMask(bool $condition = true): static
{
$state = $this->convertToFloat($state);
$this->dehydrateMask = $condition;
return $this;
}

public function getDehydrateMask(): bool
{
return $this->dehydrateMask;
}

public function initialValue(null|string|int|float|Closure $value = '0,00'): static
{
$this->initialValue = $value;

return number_format($state, 2, ',', '.') ?? 0;
return $this;
}

public function getCurrency(): ?Currency
{
return $this->currency;
}

protected function sanitizeState(?string $state): ?int
{
$state = Str::of($state)
->replace('.', '')
->replace(',', '')
->toInteger();

return $state ?? null;
}
}

0 comments on commit 60033de

Please sign in to comment.