mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
Updates
This commit is contained in:
@@ -15,8 +15,14 @@ import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import {
|
||||
evalArithmetic,
|
||||
hasArithmeticOperator,
|
||||
lastIndexOfArithmeticOperator,
|
||||
} from 'loot-core/shared/arithmetic';
|
||||
import {
|
||||
amountToCurrency,
|
||||
amountToInteger,
|
||||
appendDecimals,
|
||||
currencyToAmount,
|
||||
reapplyThousandSeparators,
|
||||
@@ -76,6 +82,10 @@ const AmountInput = memo(function AmountInput({
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
keyboardRef.current?.setInput(text);
|
||||
}, [text]);
|
||||
|
||||
const onKeyUp: HTMLProps<HTMLInputElement>['onKeyUp'] = e => {
|
||||
if (e.key === 'Backspace' && text === '') {
|
||||
setEditing(true);
|
||||
@@ -88,7 +98,10 @@ const AmountInput = memo(function AmountInput({
|
||||
};
|
||||
|
||||
const applyText = () => {
|
||||
const parsed = currencyToAmount(text) || 0;
|
||||
const parsed = hasArithmeticOperator(text)
|
||||
? evalArithmetic(text)
|
||||
: currencyToAmount(text) || 0;
|
||||
|
||||
const newValue = editing ? parsed : value;
|
||||
|
||||
setValue(Math.abs(newValue));
|
||||
@@ -119,12 +132,46 @@ const AmountInput = memo(function AmountInput({
|
||||
};
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
console.log('text', text);
|
||||
|
||||
text = reapplyThousandSeparators(text);
|
||||
text = appendDecimals(text, String(hideFraction) === 'true');
|
||||
|
||||
const lastOperatorIndex = lastIndexOfArithmeticOperator(text);
|
||||
if (lastOperatorIndex > 0) {
|
||||
// This will evaluate the expression whenever an operator is added
|
||||
// so only one operation will be displayed at a given time
|
||||
const isOperatorAtEnd = lastOperatorIndex === text.length - 1;
|
||||
if (isOperatorAtEnd) {
|
||||
const lastOperator = text[lastOperatorIndex];
|
||||
const charIndexPriorToLastOperator = lastOperatorIndex - 1;
|
||||
const charPriorToLastOperator =
|
||||
text.length > 0 ? text[charIndexPriorToLastOperator] : '';
|
||||
|
||||
if (
|
||||
charPriorToLastOperator &&
|
||||
hasArithmeticOperator(charPriorToLastOperator)
|
||||
) {
|
||||
// Clicked on another operator while there is still an operator
|
||||
// Replace previous operator with the new one
|
||||
// TODO: Fix why clicking the same operator duplicates it
|
||||
text = `${text.slice(0, charIndexPriorToLastOperator)}${lastOperator}`;
|
||||
} else {
|
||||
// Evaluate the left side of the expression whenever an operator is added
|
||||
const left = text.slice(0, lastOperatorIndex);
|
||||
const leftEvaluated = evalArithmetic(left);
|
||||
const leftEvaluatedWithDecimal = appendDecimals(
|
||||
String(amountToInteger(leftEvaluated)),
|
||||
String(hideFraction) === 'true',
|
||||
);
|
||||
text = `${leftEvaluatedWithDecimal}${lastOperator}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text = appendDecimals(text, String(hideFraction) === 'true');
|
||||
}
|
||||
setEditing(true);
|
||||
setText(text);
|
||||
props.onChangeValue?.(text);
|
||||
keyboardRef.current?.setInput(text);
|
||||
};
|
||||
|
||||
const input = (
|
||||
@@ -177,6 +224,7 @@ const AmountInput = memo(function AmountInput({
|
||||
keyboardRef={(r: AmountKeyboardRef) => (keyboardRef.current = r)}
|
||||
onChange={onChangeText}
|
||||
onBlur={onBlur}
|
||||
onEnter={onUpdate}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -17,6 +17,7 @@ export type AmountKeyboardRef = SimpleKeyboard;
|
||||
|
||||
type AmountKeyboardProps = ComponentPropsWithoutRef<typeof Keyboard> & {
|
||||
onBlur?: FocusEventHandler<HTMLDivElement>;
|
||||
onEnter?: (text: string) => void;
|
||||
};
|
||||
|
||||
export function AmountKeyboard(props: AmountKeyboardProps) {
|
||||
@@ -50,10 +51,18 @@ export function AmountKeyboard(props: AmountKeyboardProps) {
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
'& [data-skbtn="+"], & [data-skbtn="-"], & [data-skbtn="×"], & [data-skbtn="÷"], & [data-skbtn="{bksp}"]':
|
||||
'& [data-skbtn="+"], & [data-skbtn="-"], & [data-skbtn="×"], & [data-skbtn="÷"]':
|
||||
{
|
||||
backgroundColor: theme.keyboardButtonSecondaryBackground,
|
||||
},
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
'& [data-skbtn="{bksp}"], & [data-skbtn="{clear}"]': {
|
||||
backgroundColor: theme.keyboardButtonSecondaryBackground,
|
||||
},
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
'& [data-skbtn="{enter}"]': {
|
||||
backgroundColor: theme.buttonPrimaryBackground,
|
||||
},
|
||||
}),
|
||||
props.theme,
|
||||
]);
|
||||
@@ -81,25 +90,38 @@ export function AmountKeyboard(props: AmountKeyboardProps) {
|
||||
}}
|
||||
>
|
||||
<Keyboard
|
||||
layoutName="default"
|
||||
layout={{
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
default: [
|
||||
'+ 1 2 3',
|
||||
'- 4 5 6',
|
||||
'× 7 8 9',
|
||||
'÷ . 0 {bksp}',
|
||||
'÷ {clear} 0 {bksp}',
|
||||
'{space} , . {enter}',
|
||||
],
|
||||
}}
|
||||
display={{
|
||||
'{bksp}': '⌫',
|
||||
'{enter}': '↵',
|
||||
'{space}': '␣',
|
||||
'{clear}': 'C',
|
||||
}}
|
||||
useButtonTag
|
||||
stopMouseUpPropagation
|
||||
stopMouseDownPropagation
|
||||
autoUseTouchEvents
|
||||
{...props}
|
||||
keyboardRef={r => {
|
||||
keyboardRef.current = r;
|
||||
props.keyboardRef?.(r);
|
||||
}}
|
||||
onKeyPress={key => {
|
||||
if (key === '{clear}') {
|
||||
props.onChange?.('');
|
||||
} else if (key === '{enter}') {
|
||||
props.onEnter?.(keyboardRef.current?.getInput() || '');
|
||||
}
|
||||
props.onKeyPress?.(key);
|
||||
}}
|
||||
theme={layoutClassName}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
// @ts-strict-ignore
|
||||
import { currencyToAmount } from './util';
|
||||
|
||||
function fail(state, msg) {
|
||||
// These operators go from high to low order of precedence
|
||||
const operators = ['^', '/', '÷', '*', '×', '-', '+'] as const;
|
||||
type ArithmeticOp = (typeof operators)[number];
|
||||
|
||||
type ArithmeticAst =
|
||||
| number
|
||||
| { op: ArithmeticOp; left: ArithmeticAst; right: ArithmeticAst };
|
||||
|
||||
type ArithmeticState = { str: string; index: number };
|
||||
|
||||
const parseOperator = makeOperatorParser(...operators);
|
||||
|
||||
function fail(state: ArithmeticState, msg: string) {
|
||||
throw new Error(
|
||||
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
|
||||
);
|
||||
}
|
||||
|
||||
function char(state) {
|
||||
function char(state: ArithmeticState): string {
|
||||
return state.str[state.index];
|
||||
}
|
||||
|
||||
function next(state) {
|
||||
function next(state: ArithmeticState): string {
|
||||
if (state.index >= state.str.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -21,7 +33,7 @@ function next(state) {
|
||||
return ch;
|
||||
}
|
||||
|
||||
function nextOperator(state, op) {
|
||||
function nextOperator(state: ArithmeticState, op: ArithmeticOp) {
|
||||
if (char(state) === op) {
|
||||
next(state);
|
||||
return true;
|
||||
@@ -30,7 +42,7 @@ function nextOperator(state, op) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function parsePrimary(state) {
|
||||
function parsePrimary(state: ArithmeticState): number {
|
||||
// We only support numbers
|
||||
const isNegative = char(state) === '-';
|
||||
if (isNegative) {
|
||||
@@ -50,7 +62,7 @@ function parsePrimary(state) {
|
||||
return isNegative ? -number : number;
|
||||
}
|
||||
|
||||
function parseParens(state) {
|
||||
function parseParens(state: ArithmeticState): ArithmeticAst {
|
||||
if (char(state) === '(') {
|
||||
next(state);
|
||||
const expr = parseOperator(state);
|
||||
@@ -66,7 +78,7 @@ function parseParens(state) {
|
||||
return parsePrimary(state);
|
||||
}
|
||||
|
||||
function makeOperatorParser(...ops) {
|
||||
function makeOperatorParser(...ops: ArithmeticOp[]) {
|
||||
return ops.reduce((prevParser, op) => {
|
||||
return state => {
|
||||
let node = prevParser(state);
|
||||
@@ -78,15 +90,15 @@ function makeOperatorParser(...ops) {
|
||||
}, parseParens);
|
||||
}
|
||||
|
||||
// These operators go from high to low order of precedence
|
||||
const parseOperator = makeOperatorParser('^', '/', '*', '-', '+');
|
||||
|
||||
function parse(expression: string) {
|
||||
const state = { str: expression.replace(/\s/g, ''), index: 0 };
|
||||
function parse(expression: string): ArithmeticAst {
|
||||
const state: ArithmeticState = {
|
||||
str: expression.replace(/\s/g, ''),
|
||||
index: 0,
|
||||
};
|
||||
return parseOperator(state);
|
||||
}
|
||||
|
||||
function evaluate(ast): number {
|
||||
function evaluate(ast: ArithmeticAst): number {
|
||||
if (typeof ast === 'number') {
|
||||
return ast;
|
||||
}
|
||||
@@ -99,8 +111,10 @@ function evaluate(ast): number {
|
||||
case '-':
|
||||
return evaluate(left) - evaluate(right);
|
||||
case '*':
|
||||
case '×':
|
||||
return evaluate(left) * evaluate(right);
|
||||
case '/':
|
||||
case '÷':
|
||||
return evaluate(left) / evaluate(right);
|
||||
case '^':
|
||||
return Math.pow(evaluate(left), evaluate(right));
|
||||
@@ -129,3 +143,14 @@ export function evalArithmetic(
|
||||
// Never return NaN
|
||||
return isNaN(result) ? defaultValue : result;
|
||||
}
|
||||
|
||||
export function hasArithmeticOperator(expression: string): boolean {
|
||||
return operators.some(op => expression.includes(op));
|
||||
}
|
||||
|
||||
export function lastIndexOfArithmeticOperator(expression: string): number {
|
||||
return operators.reduce((max, op) => {
|
||||
const index = expression.lastIndexOf(op);
|
||||
return index > max ? index : max;
|
||||
}, -1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user