@goadminuiux/calculator 0.0.0-stage → 1.0.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.
@@ -0,0 +1,1234 @@
1
+ /**
2
+ * @adminuiux/calculator
3
+ * AdminUIUX - Production-Ready Universal Calculator Package
4
+ *
5
+ * Lightweight, zero-dependency vanilla JavaScript calculator engine & DOM component.
6
+ * Works seamlessly in Browser (direct <script> / CDN), CommonJS (Node.js), and ES Modules.
7
+ *
8
+ * @version 1.0.0
9
+ * @author AdminUIUX (adminuiux.com)
10
+ * @license MIT
11
+ *
12
+ * Public APIs:
13
+ * - Calculator: Pure mathematical calculation & evaluation engine (framework-independent)
14
+ * - Formatter: High-precision number rounding, currency, and thousand-separator formatting
15
+ * - StreamCalculator: Decoupled desk/stream calculator state machine (digits, chained ops, memory, tape)
16
+ * - AdminUIUXCalculator: DOM-binding component connecting HTML buttons, displays, outputs & keyboard
17
+ */
18
+
19
+ (function (global, factory) {
20
+ 'use strict';
21
+
22
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
23
+ // CommonJS / Node.js
24
+ module.exports = factory();
25
+ } else if (typeof define === 'function' && define.amd) {
26
+ // AMD
27
+ define([], factory);
28
+ } else {
29
+ // Browser Global / CDN script
30
+ const exportsObj = factory();
31
+ global.AdminUIUXCalculator = exportsObj.AdminUIUXCalculator;
32
+ global.Calculator = exportsObj.Calculator;
33
+ global.Formatter = exportsObj.Formatter;
34
+ global.StreamCalculator = exportsObj.StreamCalculator;
35
+ }
36
+ })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this, function () {
37
+ 'use strict';
38
+
39
+ /* ==========================================================================
40
+ 1. FORMATTER UTILITIES
41
+ ========================================================================== */
42
+ const Formatter = {
43
+ /**
44
+ * Round a number to a fixed decimal precision avoiding IEEE 754 floating point artifacts.
45
+ * @param {number} value
46
+ * @param {number} [precision=8]
47
+ * @returns {number}
48
+ */
49
+ round(value, precision = 8) {
50
+ if (!isFinite(value) || isNaN(value)) return 0;
51
+ const p = Math.pow(10, precision);
52
+ return Math.round((value + Number.EPSILON) * p) / p;
53
+ },
54
+
55
+ /**
56
+ * Format a number with thousand commas and optional currency prefix.
57
+ * @param {number|string} num
58
+ * @param {Object} [options={}]
59
+ * @param {string} [options.currency=''] - Currency prefix (e.g. '$', '€')
60
+ * @param {number} [options.precision] - Optional fixed decimal precision to round to
61
+ * @returns {string}
62
+ */
63
+ format(num, options = {}) {
64
+ if (num === null || num === undefined || num === '') return '';
65
+ const parsed = typeof num === 'number' ? num : parseFloat(num);
66
+ if (isNaN(parsed)) return 'Error';
67
+
68
+ let targetNum = num;
69
+ if (typeof options.precision === 'number') {
70
+ targetNum = this.round(parsed, options.precision);
71
+ }
72
+
73
+ const str = String(targetNum);
74
+ if (str.includes('e') || str.includes('E')) return str;
75
+
76
+ const parts = str.split('.');
77
+ const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
78
+ const formatted = parts.length > 1 ? `${intPart}.${parts[1]}` : intPart;
79
+
80
+ if (options.currency) {
81
+ return `${options.currency} ${formatted}`;
82
+ }
83
+ return formatted;
84
+ },
85
+
86
+ /**
87
+ * Parse formatted numeric string into clean number.
88
+ * @param {string|number} str
89
+ * @returns {number}
90
+ */
91
+ parse(str) {
92
+ if (typeof str === 'number') return isNaN(str) ? 0 : str;
93
+ if (!str || typeof str !== 'string') return 0;
94
+ const clean = str.replace(/[^0-9.-]/g, '');
95
+ const val = parseFloat(clean);
96
+ return isNaN(val) ? 0 : val;
97
+ },
98
+
99
+ /**
100
+ * Convert standard math code operators into display symbols.
101
+ * @param {string} op
102
+ * @returns {string}
103
+ */
104
+ getOperatorSymbol(op) {
105
+ switch (op) {
106
+ case '*':
107
+ case 'x':
108
+ return '×';
109
+ case '/':
110
+ return '÷';
111
+ case '-':
112
+ return '−';
113
+ default:
114
+ return op;
115
+ }
116
+ }
117
+ };
118
+
119
+ /* ==========================================================================
120
+ 2. CORE PURE CALCULATOR ENGINE (Framework & DOM Independent)
121
+ ========================================================================== */
122
+ const Calculator = {
123
+ precision: 8,
124
+
125
+ /**
126
+ * Basic Arithmetic Operations
127
+ */
128
+ add(a, b, precision = this.precision) {
129
+ return Formatter.round(Number(a) + Number(b), precision);
130
+ },
131
+
132
+ subtract(a, b, precision = this.precision) {
133
+ return Formatter.round(Number(a) - Number(b), precision);
134
+ },
135
+
136
+ multiply(a, b, precision = this.precision) {
137
+ return Formatter.round(Number(a) * Number(b), precision);
138
+ },
139
+
140
+ divide(a, b, precision = this.precision) {
141
+ const divisor = Number(b);
142
+ if (divisor === 0) return 0;
143
+ return Formatter.round(Number(a) / divisor, precision);
144
+ },
145
+
146
+ modulo(a, b, precision = this.precision) {
147
+ const divisor = Number(b);
148
+ if (divisor === 0) return 0;
149
+ return Formatter.round(Number(a) % divisor, precision);
150
+ },
151
+
152
+ power(a, b, precision = this.precision) {
153
+ return Formatter.round(Math.pow(Number(a), Number(b)), precision);
154
+ },
155
+
156
+ /**
157
+ * Percentage Calculations
158
+ */
159
+ percent(value, percentage, precision = this.precision) {
160
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
161
+ },
162
+
163
+ addPercent(value, percentage, precision = this.precision) {
164
+ const delta = (Number(value) * Number(percentage)) / 100;
165
+ return Formatter.round(Number(value) + delta, precision);
166
+ },
167
+
168
+ subtractPercent(value, percentage, precision = this.precision) {
169
+ const delta = (Number(value) * Number(percentage)) / 100;
170
+ return Formatter.round(Number(value) - delta, precision);
171
+ },
172
+
173
+ multiplyPercent(value, percentage, precision = this.precision) {
174
+ return Formatter.round((Number(value) * Number(percentage)) / 100, precision);
175
+ },
176
+
177
+ dividePercent(value, percentage, precision = this.precision) {
178
+ const frac = Number(percentage) / 100;
179
+ if (frac === 0) return 0;
180
+ return Formatter.round(Number(value) / frac, precision);
181
+ },
182
+
183
+ /**
184
+ * Scientific & Math Functions
185
+ */
186
+ sin(val, angleMode = 'deg', precision = this.precision) {
187
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
188
+ return Formatter.round(Math.sin(rad), precision);
189
+ },
190
+
191
+ cos(val, angleMode = 'deg', precision = this.precision) {
192
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
193
+ return Formatter.round(Math.cos(rad), precision);
194
+ },
195
+
196
+ tan(val, angleMode = 'deg', precision = this.precision) {
197
+ const rad = angleMode === 'deg' ? (Number(val) * Math.PI) / 180 : Number(val);
198
+ return Formatter.round(Math.tan(rad), precision);
199
+ },
200
+
201
+ asin(val, angleMode = 'deg', precision = this.precision) {
202
+ const rad = Math.asin(Number(val));
203
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
204
+ return Formatter.round(res, precision);
205
+ },
206
+
207
+ acos(val, angleMode = 'deg', precision = this.precision) {
208
+ const rad = Math.acos(Number(val));
209
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
210
+ return Formatter.round(res, precision);
211
+ },
212
+
213
+ atan(val, angleMode = 'deg', precision = this.precision) {
214
+ const rad = Math.atan(Number(val));
215
+ const res = angleMode === 'deg' ? (rad * 180) / Math.PI : rad;
216
+ return Formatter.round(res, precision);
217
+ },
218
+
219
+ sqrt(val, precision = this.precision) {
220
+ const n = Number(val);
221
+ return n >= 0 ? Formatter.round(Math.sqrt(n), precision) : 0;
222
+ },
223
+
224
+ sqr(val, precision = this.precision) {
225
+ return Formatter.round(Number(val) * Number(val), precision);
226
+ },
227
+
228
+ cube(val, precision = this.precision) {
229
+ return Formatter.round(Number(val) * Number(val) * Number(val), precision);
230
+ },
231
+
232
+ log(val, precision = this.precision) {
233
+ const n = Number(val);
234
+ return n > 0 ? Formatter.round(Math.log10(n), precision) : 0;
235
+ },
236
+
237
+ ln(val, precision = this.precision) {
238
+ const n = Number(val);
239
+ return n > 0 ? Formatter.round(Math.log(n), precision) : 0;
240
+ },
241
+
242
+ exp(val, precision = this.precision) {
243
+ return Formatter.round(Math.exp(Number(val)), precision);
244
+ },
245
+
246
+ reciprocal(val, precision = this.precision) {
247
+ const n = Number(val);
248
+ return n !== 0 ? Formatter.round(1 / n, precision) : 0;
249
+ },
250
+
251
+ abs(val) {
252
+ return Math.abs(Number(val));
253
+ },
254
+
255
+ factorial(n) {
256
+ const num = Math.floor(Number(n));
257
+ if (num < 0) return 0;
258
+ if (num === 0 || num === 1) return 1;
259
+ let r = 1;
260
+ for (let i = 2; i <= Math.min(num, 170); i++) r *= i;
261
+ return r;
262
+ },
263
+
264
+ /**
265
+ * Financial Helpers
266
+ */
267
+ tax(amount, rate = 0.18, isAdd = true, precision = this.precision) {
268
+ const amt = Number(amount);
269
+ const res = isAdd ? amt * (1 + rate) : amt / (1 + rate);
270
+ return Formatter.round(res, precision);
271
+ },
272
+
273
+ tip(amount, percent = 15, precision = this.precision) {
274
+ const amt = Number(amount);
275
+ const tipVal = (amt * percent) / 100;
276
+ return Formatter.round(amt + tipVal, precision);
277
+ },
278
+
279
+ discount(amount, percent = 10, precision = this.precision) {
280
+ const amt = Number(amount);
281
+ const discVal = (amt * percent) / 100;
282
+ return Formatter.round(amt - discVal, precision);
283
+ },
284
+
285
+ /**
286
+ * Format shorthand helper
287
+ */
288
+ format(value, options) {
289
+ return Formatter.format(value, options);
290
+ },
291
+
292
+ /**
293
+ * Execute action expression string (e.g. "+10%", "-15%", "+500", "*2")
294
+ * @param {number} currentVal
295
+ * @param {string} expr
296
+ * @returns {{ value: number, history: string }}
297
+ */
298
+ executePerform(currentVal, expr) {
299
+ if (!expr) return { value: currentVal, history: '' };
300
+ const str = String(expr).trim();
301
+ const val = Number(currentVal) || 0;
302
+
303
+ // Percentage modifier (e.g. "+15%", "-10%", "20%")
304
+ const pctMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)\s*%$/);
305
+ if (pctMatch) {
306
+ const sign = pctMatch[1] || '+';
307
+ const pct = parseFloat(pctMatch[2]);
308
+ let result = val;
309
+ if (sign === '+' || !pctMatch[1]) {
310
+ result = this.addPercent(val, pct);
311
+ } else if (sign === '-') {
312
+ result = this.subtractPercent(val, pct);
313
+ } else if (sign === '*') {
314
+ result = this.multiplyPercent(val, pct);
315
+ } else if (sign === '/') {
316
+ result = this.dividePercent(val, pct);
317
+ }
318
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
319
+ return {
320
+ value: result,
321
+ history: `${Formatter.format(val)} ${opDisplay} ${pct}% =`
322
+ };
323
+ }
324
+
325
+ // Numeric offset/multiplier (e.g. "+100", "-50", "*2", "/4")
326
+ const numMatch = str.match(/^([+\-*/]?)\s*(\d+(?:\.\d+)?)$/);
327
+ if (numMatch) {
328
+ const sign = numMatch[1] || '+';
329
+ const num = parseFloat(numMatch[2]);
330
+ let result = val;
331
+ if (sign === '+') result = this.add(val, num);
332
+ else if (sign === '-') result = this.subtract(val, num);
333
+ else if (sign === '*') result = this.multiply(val, num);
334
+ else if (sign === '/') result = this.divide(val, num);
335
+ const opDisplay = sign === '-' ? '−' : (sign === '*' ? '×' : (sign === '/' ? '÷' : '+'));
336
+ return {
337
+ value: result,
338
+ history: `${Formatter.format(val)} ${opDisplay} ${Formatter.format(num)} =`
339
+ };
340
+ }
341
+
342
+ return { value: val, history: '' };
343
+ },
344
+
345
+ /**
346
+ * Mathematical Expression Parser & Evaluator (BODMAS / Operator Precedence)
347
+ * Evaluates strings like "100 + 25 * 2", "(10 + 20) * 3", "100 - 10%", etc.
348
+ * Pure tokenizer and Shunting-Yard evaluator with zero eval() or Function() calls.
349
+ *
350
+ * @param {string} expression
351
+ * @param {Object} [options={}]
352
+ * @returns {number}
353
+ */
354
+ evaluate(expression, options = {}) {
355
+ if (!expression || typeof expression !== 'string') return 0;
356
+ const precision = options.precision ?? this.precision;
357
+
358
+ // Normalize symbols: '×' -> '*', '÷' -> '/', '−' -> '-'
359
+ let clean = expression
360
+ .replace(/×/g, '*')
361
+ .replace(/÷/g, '/')
362
+ .replace(/−/g, '-')
363
+ .replace(/,/g, '')
364
+ .trim();
365
+
366
+ if (!clean) return 0;
367
+
368
+ // Tokenize expression
369
+ const tokens = [];
370
+ let i = 0;
371
+ let lastToken = null;
372
+
373
+ while (i < clean.length) {
374
+ const char = clean[i];
375
+
376
+ if (/\s/.test(char)) {
377
+ i++;
378
+ continue;
379
+ }
380
+
381
+ // Numbers (including decimals)
382
+ if (/\d/.test(char) || (char === '.' && i + 1 < clean.length && /\d/.test(clean[i + 1]))) {
383
+ let numStr = '';
384
+ while (i < clean.length && (/[\d.]/.test(clean[i]))) {
385
+ numStr += clean[i];
386
+ i++;
387
+ }
388
+ // Check if followed by %
389
+ let isPct = false;
390
+ while (i < clean.length && /\s/.test(clean[i])) i++;
391
+ if (i < clean.length && clean[i] === '%') {
392
+ isPct = true;
393
+ i++;
394
+ }
395
+ const numVal = parseFloat(numStr);
396
+ tokens.push({ type: 'number', value: numVal, isPercent: isPct });
397
+ lastToken = 'number';
398
+ continue;
399
+ }
400
+
401
+ // Operators & Parentheses
402
+ if (char === '+' || char === '-' || char === '*' || char === '/' || char === '^' || char === '(' || char === ')') {
403
+ // Check for unary plus/minus
404
+ const isUnary = (char === '+' || char === '-') &&
405
+ (lastToken === null || lastToken === 'operator' || lastToken === 'lparen');
406
+
407
+ if (isUnary) {
408
+ tokens.push({ type: 'unary', value: char });
409
+ lastToken = 'unary';
410
+ } else if (char === '(') {
411
+ tokens.push({ type: 'lparen', value: '(' });
412
+ lastToken = 'lparen';
413
+ } else if (char === ')') {
414
+ tokens.push({ type: 'rparen', value: ')' });
415
+ lastToken = 'rparen';
416
+ } else {
417
+ tokens.push({ type: 'operator', value: char });
418
+ lastToken = 'operator';
419
+ }
420
+ i++;
421
+ continue;
422
+ }
423
+
424
+ // Skip any unrecognized characters
425
+ i++;
426
+ }
427
+
428
+ if (tokens.length === 0) return 0;
429
+
430
+ // Operator Precedence Map
431
+ const precedence = {
432
+ '+': 1,
433
+ '-': 1,
434
+ '*': 2,
435
+ '/': 2,
436
+ '^': 3,
437
+ 'u+': 4,
438
+ 'u-': 4
439
+ };
440
+
441
+ const outputQueue = [];
442
+ const opStack = [];
443
+
444
+ // Shunting-Yard Algorithm: infix to postfix (RPN)
445
+ for (let t = 0; t < tokens.length; t++) {
446
+ const token = tokens[t];
447
+
448
+ if (token.type === 'number') {
449
+ outputQueue.push(token);
450
+ } else if (token.type === 'unary') {
451
+ opStack.push({ type: 'unary', op: 'u' + token.value });
452
+ } else if (token.type === 'operator') {
453
+ const op = token.value;
454
+ while (
455
+ opStack.length > 0 &&
456
+ opStack[opStack.length - 1].type !== 'lparen' &&
457
+ ((precedence[opStack[opStack.length - 1].op] > precedence[op]) ||
458
+ (precedence[opStack[opStack.length - 1].op] === precedence[op] && op !== '^'))
459
+ ) {
460
+ outputQueue.push(opStack.pop());
461
+ }
462
+ opStack.push({ type: 'operator', op: op });
463
+ } else if (token.type === 'lparen') {
464
+ opStack.push(token);
465
+ } else if (token.type === 'rparen') {
466
+ while (opStack.length > 0 && opStack[opStack.length - 1].type !== 'lparen') {
467
+ outputQueue.push(opStack.pop());
468
+ }
469
+ if (opStack.length > 0 && opStack[opStack.length - 1].type === 'lparen') {
470
+ opStack.pop();
471
+ }
472
+ }
473
+ }
474
+
475
+ while (opStack.length > 0) {
476
+ const op = opStack.pop();
477
+ if (op.type !== 'lparen') {
478
+ outputQueue.push(op);
479
+ }
480
+ }
481
+
482
+ // Postfix Evaluator
483
+ const evalStack = [];
484
+ for (let k = 0; k < outputQueue.length; k++) {
485
+ const item = outputQueue[k];
486
+
487
+ if (item.type === 'number') {
488
+ evalStack.push(item);
489
+ } else if (item.type === 'unary') {
490
+ const operand = evalStack.pop();
491
+ const val = typeof operand === 'object' ? operand.value : operand;
492
+ const res = item.op === 'u-' ? -val : val;
493
+ evalStack.push({ type: 'number', value: res });
494
+ } else if (item.type === 'operator') {
495
+ const rightToken = evalStack.pop();
496
+ const leftToken = evalStack.pop();
497
+
498
+ const left = typeof leftToken === 'object' ? leftToken.value : Number(leftToken);
499
+ let right = typeof rightToken === 'object' ? rightToken.value : Number(rightToken);
500
+
501
+ // Smart percentage handling
502
+ if (rightToken && rightToken.isPercent) {
503
+ if (item.op === '+' || item.op === '-') {
504
+ right = (left * right) / 100;
505
+ } else if (item.op === '*' || item.op === '/') {
506
+ right = right / 100;
507
+ }
508
+ }
509
+
510
+ let res = 0;
511
+ switch (item.op) {
512
+ case '+': res = left + right; break;
513
+ case '-': res = left - right; break;
514
+ case '*': res = left * right; break;
515
+ case '/': res = right === 0 ? 0 : left / right; break;
516
+ case '^': res = Math.pow(left, right); break;
517
+ }
518
+ evalStack.push({ type: 'number', value: res });
519
+ }
520
+ }
521
+
522
+ if (evalStack.length === 0) return 0;
523
+ const finalResult = evalStack[0].value ?? evalStack[0];
524
+ return Formatter.round(Number(finalResult), precision);
525
+ },
526
+
527
+ /**
528
+ * Factory to instantiate a decoupled Stream Calculator state machine
529
+ * @param {Object} [config={}]
530
+ * @returns {StreamCalculator}
531
+ */
532
+ createStream(config = {}) {
533
+ return new StreamCalculator(config);
534
+ }
535
+ };
536
+
537
+ /* ==========================================================================
538
+ 3. STREAM CALCULATOR STATE MACHINE (Desk / Chained Calculator)
539
+ ========================================================================== */
540
+ class StreamCalculator {
541
+ constructor(config = {}) {
542
+ this.config = Object.assign({
543
+ precision: 8,
544
+ currency: '',
545
+ taxRate: 0.18,
546
+ tipRates: [10, 15, 20],
547
+ angleMode: 'deg',
548
+ onCalculate: null
549
+ }, config);
550
+
551
+ this.reset();
552
+ }
553
+
554
+ reset() {
555
+ this.currentInput = '0';
556
+ this.previousValue = null;
557
+ this.pendingOperator = null;
558
+ this.historyTokens = [];
559
+ this.hasJustCalculated = false;
560
+ this.hasPercentApplied = false;
561
+ this.rawPercent = null;
562
+ this.memory = 0;
563
+ this.tapeHistory = [];
564
+ }
565
+
566
+ inputDigit(digit) {
567
+ if (this.hasJustCalculated) {
568
+ this.currentInput = digit === '.' ? '0.' : digit;
569
+ this.previousValue = null;
570
+ this.pendingOperator = null;
571
+ this.historyTokens = [];
572
+ this.hasJustCalculated = false;
573
+ this.hasPercentApplied = false;
574
+ this.rawPercent = null;
575
+ return;
576
+ }
577
+
578
+ this.hasPercentApplied = false;
579
+ this.rawPercent = null;
580
+
581
+ if (digit === '.') {
582
+ if (!this.currentInput.includes('.')) {
583
+ this.currentInput = this.currentInput === '' || this.currentInput === '0' ? '0.' : this.currentInput + '.';
584
+ }
585
+ } else if (digit === '00') {
586
+ if (this.currentInput === '0' || this.currentInput === '') {
587
+ this.currentInput = '0';
588
+ } else {
589
+ this.currentInput += '00';
590
+ }
591
+ } else {
592
+ if (this.currentInput === '0' || this.currentInput === '') {
593
+ this.currentInput = String(digit);
594
+ } else {
595
+ this.currentInput += String(digit);
596
+ }
597
+ }
598
+ }
599
+
600
+ inputOperator(op) {
601
+ const symbol = Formatter.getOperatorSymbol(op);
602
+
603
+ if (this.hasJustCalculated) {
604
+ const currentNum = this.parseCurrent();
605
+ this.previousValue = currentNum;
606
+ this.pendingOperator = op;
607
+ this.historyTokens = [Formatter.format(currentNum), symbol];
608
+ this.currentInput = '';
609
+ this.hasJustCalculated = false;
610
+ return;
611
+ }
612
+
613
+ if (this.previousValue === null) {
614
+ const currentNum = this.parseCurrent();
615
+ this.previousValue = currentNum;
616
+ this.pendingOperator = op;
617
+ this.historyTokens = [Formatter.format(currentNum), symbol];
618
+ this.currentInput = '';
619
+ return;
620
+ }
621
+
622
+ if (this.currentInput !== '') {
623
+ const nextNum = parseFloat(this.currentInput);
624
+ let calculated = 0;
625
+
626
+ if (this.hasPercentApplied) {
627
+ const prevOp = this.pendingOperator;
628
+ if (prevOp === '+') calculated = this.previousValue + nextNum;
629
+ else if (prevOp === '-') calculated = this.previousValue - nextNum;
630
+ else if (prevOp === '*' || prevOp === 'x' || prevOp === '×') calculated = nextNum;
631
+ else if (prevOp === '/' || prevOp === '÷') {
632
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
633
+ calculated = frac === 0 ? 0 : this.previousValue / frac;
634
+ } else {
635
+ calculated = this.calculate(this.previousValue, prevOp, nextNum);
636
+ }
637
+ calculated = Formatter.round(calculated, this.config.precision);
638
+ this.hasPercentApplied = false;
639
+ this.rawPercent = null;
640
+ } else {
641
+ calculated = this.calculate(this.previousValue, this.pendingOperator, nextNum);
642
+ }
643
+
644
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
645
+ this.historyTokens.push(symbol);
646
+ } else {
647
+ this.historyTokens.push(Formatter.format(nextNum), symbol);
648
+ }
649
+ this.previousValue = calculated;
650
+ this.pendingOperator = op;
651
+ this.currentInput = '';
652
+ return;
653
+ }
654
+
655
+ // Change pending operator
656
+ this.pendingOperator = op;
657
+ if (this.historyTokens.length > 0) {
658
+ this.historyTokens[this.historyTokens.length - 1] = symbol;
659
+ }
660
+ }
661
+
662
+ inputEquals() {
663
+ if (this.pendingOperator === null || this.previousValue === null) return this.parseCurrent();
664
+
665
+ const nextNum = this.currentInput !== '' ? parseFloat(this.currentInput) : this.previousValue;
666
+ let result = 0;
667
+
668
+ if (this.hasPercentApplied) {
669
+ const op = this.pendingOperator;
670
+ if (op === '+') result = this.previousValue + nextNum;
671
+ else if (op === '-') result = this.previousValue - nextNum;
672
+ else if (op === '*' || op === 'x' || op === '×') result = nextNum;
673
+ else if (op === '/' || op === '÷') {
674
+ const frac = this.rawPercent !== null ? (this.rawPercent / 100) : (nextNum / (this.previousValue || 1));
675
+ result = frac === 0 ? 0 : this.previousValue / frac;
676
+ } else {
677
+ result = this.calculate(this.previousValue, op, nextNum);
678
+ }
679
+ result = Formatter.round(result, this.config.precision);
680
+ this.hasPercentApplied = false;
681
+ this.rawPercent = null;
682
+ } else {
683
+ result = this.calculate(this.previousValue, this.pendingOperator, nextNum);
684
+ }
685
+
686
+ if (this.historyTokens.length > 0 && String(this.historyTokens[this.historyTokens.length - 1]).endsWith('%')) {
687
+ this.historyTokens.push('=');
688
+ } else {
689
+ this.historyTokens.push(Formatter.format(nextNum), '=');
690
+ }
691
+
692
+ const historyStr = this.historyTokens.join(' ');
693
+ this.tapeHistory.unshift(`${historyStr} ${Formatter.format(result)}`);
694
+
695
+ if (typeof this.config.onCalculate === 'function') {
696
+ this.config.onCalculate(result, historyStr);
697
+ }
698
+
699
+ this.previousValue = result;
700
+ this.currentInput = String(result);
701
+ this.pendingOperator = null;
702
+ this.hasJustCalculated = true;
703
+ return result;
704
+ }
705
+
706
+ calculate(a, op, b) {
707
+ let res = 0;
708
+ switch (op) {
709
+ case '+': res = a + b; break;
710
+ case '-': res = a - b; break;
711
+ case '*':
712
+ case 'x':
713
+ case '×':
714
+ res = a * b; break;
715
+ case '/':
716
+ case '÷':
717
+ res = b === 0 ? 0 : a / b; break;
718
+ case '%':
719
+ case 'mod':
720
+ res = a % b; break;
721
+ case '^':
722
+ case 'pow':
723
+ res = Math.pow(a, b); break;
724
+ default: res = b;
725
+ }
726
+ return Formatter.round(res, this.config.precision);
727
+ }
728
+
729
+ toggleSign() {
730
+ if (this.currentInput !== '' && this.currentInput !== '0') {
731
+ if (this.currentInput.startsWith('-')) {
732
+ this.currentInput = this.currentInput.slice(1);
733
+ } else {
734
+ this.currentInput = '-' + this.currentInput;
735
+ }
736
+ } else if (this.previousValue !== null) {
737
+ this.previousValue = -this.previousValue;
738
+ this.currentInput = String(this.previousValue);
739
+ }
740
+ }
741
+
742
+ applyPercent() {
743
+ const current = this.parseCurrent();
744
+ if (this.previousValue !== null && this.pendingOperator !== null) {
745
+ const percentVal = (this.previousValue * current) / 100;
746
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
747
+ this.hasPercentApplied = true;
748
+ this.rawPercent = current;
749
+ this.historyTokens = [
750
+ Formatter.format(this.previousValue),
751
+ Formatter.getOperatorSymbol(this.pendingOperator),
752
+ `${Formatter.format(current)}%`
753
+ ];
754
+ } else {
755
+ const percentVal = current / 100;
756
+ this.currentInput = String(Formatter.round(percentVal, this.config.precision));
757
+ this.hasPercentApplied = false;
758
+ this.rawPercent = null;
759
+ this.historyTokens = [`${Formatter.format(current)}%`];
760
+ }
761
+ }
762
+
763
+ backspace() {
764
+ if (this.hasJustCalculated) {
765
+ this.clearAll();
766
+ return;
767
+ }
768
+ if (this.currentInput.length > 1) {
769
+ this.currentInput = this.currentInput.slice(0, -1);
770
+ } else {
771
+ this.currentInput = '0';
772
+ }
773
+ }
774
+
775
+ clearAll() {
776
+ this.currentInput = '0';
777
+ this.previousValue = null;
778
+ this.pendingOperator = null;
779
+ this.historyTokens = [];
780
+ this.hasJustCalculated = false;
781
+ this.hasPercentApplied = false;
782
+ this.rawPercent = null;
783
+ }
784
+
785
+ parseCurrent() {
786
+ return parseFloat(this.currentInput) || 0;
787
+ }
788
+
789
+ getDisplayValue() {
790
+ let displayVal = this.currentInput;
791
+ if (displayVal === '' && this.previousValue !== null) {
792
+ displayVal = String(this.previousValue);
793
+ }
794
+ if (displayVal === '') displayVal = '0';
795
+ return displayVal;
796
+ }
797
+
798
+ getFormattedDisplay() {
799
+ return Formatter.format(this.getDisplayValue(), { currency: this.config.currency });
800
+ }
801
+
802
+ getHistoryString() {
803
+ return this.historyTokens.join(' ');
804
+ }
805
+ }
806
+
807
+ /* ==========================================================================
808
+ 4. ADMINUIUX CALCULATOR (DOM Binding Layer)
809
+ ========================================================================== */
810
+ class AdminUIUXCalculator {
811
+ constructor(element, config = {}) {
812
+ if (typeof element === 'string') {
813
+ this.wrapper = document.querySelector(element);
814
+ } else {
815
+ this.wrapper = element;
816
+ }
817
+
818
+ if (!this.wrapper) return;
819
+ if (this.wrapper._calculatorInstance) return;
820
+ this.wrapper._calculatorInstance = this;
821
+
822
+ // Extract configuration from data attributes or arguments
823
+ this.config = Object.assign({
824
+ type: this.wrapper.getAttribute('data-calculator-type') || this.wrapper.getAttribute('data-calendar-type') || this.wrapper.getAttribute('data-calc-mode') || 'basic',
825
+ precision: parseInt(this.wrapper.getAttribute('data-calc-precision'), 10) || (config.precision ?? 8),
826
+ angleMode: 'deg',
827
+ currency: this.wrapper.getAttribute('data-calc-currency') || config.currency || '',
828
+ taxRate: config.taxRate ?? 0.18,
829
+ tipRates: config.tipRates ?? [10, 15, 20],
830
+ quickPresets: config.quickPresets ?? [50, 100, 500, 1000],
831
+ enableKeyboard: config.enableKeyboard ?? true,
832
+ showTape: config.showTape ?? false,
833
+ onCalculate: config.onCalculate || null
834
+ }, config);
835
+
836
+ // Instantiate Decoupled Stream Calculator Engine
837
+ this.engine = new StreamCalculator(this.config);
838
+
839
+ this.hasInteracted = false;
840
+ this.memory = 0;
841
+
842
+ // Cache DOM Elements
843
+ this.displayEl = this.wrapper.querySelector('.calculator-display, [data-calc-display]');
844
+ this.historyEl = this.wrapper.querySelector('.calculator-history, [data-calc-history]');
845
+ this.memoryBadge = this.wrapper.querySelector('[data-calc-memory-badge]');
846
+ this.angleModeBtn = this.wrapper.querySelector('[data-calc-angle-toggle]');
847
+ this.tapeListEl = this.wrapper.querySelector('.calculator-tape, [data-calc-tape]');
848
+
849
+ this.initEventListeners();
850
+ this.updateDisplay();
851
+ }
852
+
853
+ initEventListeners() {
854
+ this.wrapper.querySelectorAll('[data-calc-action], [data-calculator-perfom], [data-calculator-perform]').forEach(btn => {
855
+ btn.addEventListener('click', (e) => {
856
+ e.preventDefault();
857
+ const performExpr = btn.getAttribute('data-calculator-perfom') || btn.getAttribute('data-calculator-perform');
858
+ if (performExpr) {
859
+ this.executePerform(performExpr);
860
+ return;
861
+ }
862
+ const action = btn.getAttribute('data-calc-action');
863
+ const val = btn.getAttribute('data-val');
864
+ this.handleAction(action, val);
865
+ });
866
+ });
867
+
868
+ if (this.config.enableKeyboard) {
869
+ this.wrapper.addEventListener('keydown', (e) => {
870
+ this.handleKeyDown(e);
871
+ });
872
+ if (!this.wrapper.hasAttribute('tabindex')) {
873
+ this.wrapper.setAttribute('tabindex', '0');
874
+ }
875
+ }
876
+ }
877
+
878
+ handleKeyDown(e) {
879
+ if (['input', 'textarea'].includes(document.activeElement?.tagName?.toLowerCase())) return;
880
+
881
+ const key = e.key;
882
+ if (!isNaN(key)) {
883
+ e.preventDefault();
884
+ this.handleAction('num', key);
885
+ } else if (key === '.') {
886
+ e.preventDefault();
887
+ this.handleAction('num', '.');
888
+ } else if (key === '+' || key === '-' || key === '*' || key === '/') {
889
+ e.preventDefault();
890
+ this.handleAction('operator', key);
891
+ } else if (key === 'Enter' || key === '=') {
892
+ e.preventDefault();
893
+ this.handleAction('equals');
894
+ } else if (key === 'Backspace') {
895
+ e.preventDefault();
896
+ this.handleAction('backspace');
897
+ } else if (key === 'Escape') {
898
+ e.preventDefault();
899
+ this.handleAction('clear');
900
+ } else if (key === '%') {
901
+ e.preventDefault();
902
+ this.handleAction('percent');
903
+ }
904
+ }
905
+
906
+ handleAction(action, val) {
907
+ this.hasInteracted = true;
908
+ switch (action) {
909
+ case 'num':
910
+ this.engine.inputDigit(val);
911
+ break;
912
+ case 'operator':
913
+ this.engine.inputOperator(val);
914
+ break;
915
+ case 'equals':
916
+ this.engine.inputEquals();
917
+ this.syncTape();
918
+ break;
919
+ case 'clear':
920
+ this.engine.clearAll();
921
+ break;
922
+ case 'clear-entry':
923
+ case 'backspace':
924
+ this.engine.backspace();
925
+ break;
926
+ case 'negate':
927
+ this.engine.toggleSign();
928
+ break;
929
+ case 'percent':
930
+ this.engine.applyPercent();
931
+ break;
932
+ case 'scientific':
933
+ this.applyScientific(val);
934
+ break;
935
+ case 'memory':
936
+ this.handleMemory(val);
937
+ break;
938
+ case 'angle-toggle':
939
+ this.toggleAngleMode();
940
+ break;
941
+ case 'preset':
942
+ this.applyPreset(parseFloat(val));
943
+ break;
944
+ case 'tax-add':
945
+ this.applyTax(true);
946
+ break;
947
+ case 'tax-sub':
948
+ this.applyTax(false);
949
+ break;
950
+ case 'tip':
951
+ this.applyTip(parseFloat(val));
952
+ break;
953
+ case 'discount':
954
+ this.applyDiscount(parseFloat(val));
955
+ break;
956
+ }
957
+ this.updateDisplay();
958
+ }
959
+
960
+ applyScientific(func) {
961
+ const val = this.engine.parseCurrent();
962
+ let res = val;
963
+ switch (func) {
964
+ case 'sin': res = Calculator.sin(val, this.config.angleMode, this.config.precision); break;
965
+ case 'cos': res = Calculator.cos(val, this.config.angleMode, this.config.precision); break;
966
+ case 'tan': res = Calculator.tan(val, this.config.angleMode, this.config.precision); break;
967
+ case 'asin': res = Calculator.asin(val, this.config.angleMode, this.config.precision); break;
968
+ case 'acos': res = Calculator.acos(val, this.config.angleMode, this.config.precision); break;
969
+ case 'atan': res = Calculator.atan(val, this.config.angleMode, this.config.precision); break;
970
+ case 'sqrt': res = Calculator.sqrt(val, this.config.precision); break;
971
+ case 'sqr': res = Calculator.sqr(val, this.config.precision); break;
972
+ case 'cube': res = Calculator.cube(val, this.config.precision); break;
973
+ case 'log': res = Calculator.log(val, this.config.precision); break;
974
+ case 'ln': res = Calculator.ln(val, this.config.precision); break;
975
+ case 'exp': res = Calculator.exp(val, this.config.precision); break;
976
+ case '1/x': res = Calculator.reciprocal(val, this.config.precision); break;
977
+ case 'abs': res = Calculator.abs(val); break;
978
+ case 'pi': res = Formatter.round(Math.PI, this.config.precision); break;
979
+ case 'e': res = Formatter.round(Math.E, this.config.precision); break;
980
+ case 'fact': res = Calculator.factorial(val); break;
981
+ }
982
+
983
+ this.engine.currentInput = String(res);
984
+ if (this.engine.hasJustCalculated) {
985
+ this.engine.historyTokens = [`${func}(${Formatter.format(val)}) =`];
986
+ }
987
+ }
988
+
989
+ handleMemory(action) {
990
+ const val = this.engine.parseCurrent();
991
+ switch (action) {
992
+ case 'mc': this.memory = 0; break;
993
+ case 'mr':
994
+ this.engine.currentInput = String(this.memory);
995
+ this.engine.hasJustCalculated = false;
996
+ break;
997
+ case 'm+': this.memory += val; break;
998
+ case 'm-': this.memory -= val; break;
999
+ case 'ms': this.memory = val; break;
1000
+ }
1001
+ if (this.memoryBadge) {
1002
+ this.memoryBadge.style.display = this.memory !== 0 ? 'inline-block' : 'none';
1003
+ }
1004
+ }
1005
+
1006
+ toggleAngleMode() {
1007
+ this.config.angleMode = this.config.angleMode === 'deg' ? 'rad' : 'deg';
1008
+ if (this.angleModeBtn) {
1009
+ this.angleModeBtn.textContent = this.config.angleMode.toUpperCase();
1010
+ }
1011
+ }
1012
+
1013
+ applyPreset(amount) {
1014
+ const current = this.engine.parseCurrent();
1015
+ this.engine.currentInput = String(current + amount);
1016
+ this.engine.hasJustCalculated = false;
1017
+ }
1018
+
1019
+ applyTax(isAdd) {
1020
+ const current = this.engine.parseCurrent();
1021
+ const rate = this.config.taxRate;
1022
+ const result = Calculator.tax(current, rate, isAdd, this.config.precision);
1023
+ this.engine.currentInput = String(result);
1024
+ this.engine.historyTokens = [`${Formatter.format(current)} ${isAdd ? '+' : '-'} ${Math.round(rate * 100)}% Tax =`];
1025
+ this.engine.hasJustCalculated = true;
1026
+ }
1027
+
1028
+ applyTip(percent) {
1029
+ const current = this.engine.parseCurrent();
1030
+ const result = Calculator.tip(current, percent, this.config.precision);
1031
+ this.engine.currentInput = String(result);
1032
+ this.engine.historyTokens = [`${Formatter.format(current)} + ${percent}% Tip =`];
1033
+ this.engine.hasJustCalculated = true;
1034
+ }
1035
+
1036
+ applyDiscount(percent) {
1037
+ const current = this.engine.parseCurrent();
1038
+ const result = Calculator.discount(current, percent, this.config.precision);
1039
+ this.engine.currentInput = String(result);
1040
+ this.engine.historyTokens = [`${Formatter.format(current)} - ${percent}% Off =`];
1041
+ this.engine.hasJustCalculated = true;
1042
+ }
1043
+
1044
+ executePerform(expr) {
1045
+ if (!expr) return;
1046
+ this.hasInteracted = true;
1047
+ const current = this.engine.parseCurrent();
1048
+ const res = Calculator.executePerform(current, expr);
1049
+ this.engine.currentInput = String(res.value);
1050
+ if (res.history) {
1051
+ this.engine.historyTokens = [res.history];
1052
+ }
1053
+ this.engine.hasJustCalculated = true;
1054
+ this.updateDisplay();
1055
+ }
1056
+
1057
+ getOutputTargets() {
1058
+ const selector = this.wrapper.getAttribute('data-calculator-output') ||
1059
+ this.wrapper.getAttribute('data-output-target') ||
1060
+ this.config.outputTarget ||
1061
+ this.config.output;
1062
+ if (!selector) return [];
1063
+ try {
1064
+ return Array.from(document.querySelectorAll(selector));
1065
+ } catch (e) {
1066
+ return [];
1067
+ }
1068
+ }
1069
+
1070
+ getCalculationTargets() {
1071
+ const selector = this.wrapper.getAttribute('data-calculator-calculation') ||
1072
+ this.wrapper.getAttribute('data-calculation-target') ||
1073
+ this.config.calculationTarget ||
1074
+ this.config.calculation;
1075
+ if (!selector) return [];
1076
+ try {
1077
+ return Array.from(document.querySelectorAll(selector));
1078
+ } catch (e) {
1079
+ return [];
1080
+ }
1081
+ }
1082
+
1083
+ updateOutputTargets(displayStr, rawVal) {
1084
+ const targets = this.getOutputTargets();
1085
+ if (targets.length === 0) return;
1086
+
1087
+ targets.forEach(target => {
1088
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
1089
+ target.value = displayStr;
1090
+ target.dispatchEvent(new Event('input', { bubbles: true }));
1091
+ target.dispatchEvent(new Event('change', { bubbles: true }));
1092
+ } else {
1093
+ target.textContent = displayStr;
1094
+ }
1095
+ });
1096
+
1097
+ this.wrapper.dispatchEvent(new CustomEvent('calculator:change', {
1098
+ bubbles: true,
1099
+ detail: {
1100
+ value: rawVal,
1101
+ formatted: displayStr,
1102
+ history: this.engine.getHistoryString(),
1103
+ calculator: this
1104
+ }
1105
+ }));
1106
+ }
1107
+
1108
+ updateCalculationTargets(calculationStr) {
1109
+ const targets = this.getCalculationTargets();
1110
+ if (targets.length === 0) return;
1111
+
1112
+ targets.forEach(target => {
1113
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
1114
+ target.value = calculationStr;
1115
+ target.dispatchEvent(new Event('input', { bubbles: true }));
1116
+ target.dispatchEvent(new Event('change', { bubbles: true }));
1117
+ } else {
1118
+ target.textContent = calculationStr;
1119
+ }
1120
+ });
1121
+ }
1122
+
1123
+ updateDisplay() {
1124
+ const currency = this.config.currency ? `${this.config.currency} ` : '';
1125
+ const rawVal = this.engine.parseCurrent();
1126
+ let finalDisplay = this.engine.getDisplayValue();
1127
+
1128
+ // Format integer with commas
1129
+ const parts = finalDisplay.split('.');
1130
+ const formattedInt = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1131
+ finalDisplay = parts.length > 1 ? `${formattedInt}.${parts[1]}` : formattedInt;
1132
+
1133
+ if (this.displayEl) {
1134
+ this.displayEl.textContent = `${currency}${finalDisplay}`;
1135
+ }
1136
+
1137
+ const historyStr = this.engine.getHistoryString();
1138
+ if (this.historyEl) {
1139
+ if (historyStr) {
1140
+ this.historyEl.textContent = historyStr;
1141
+ } else {
1142
+ this.historyEl.innerHTML = '&nbsp;';
1143
+ }
1144
+ }
1145
+
1146
+ this.scrollToEnd(this.historyEl);
1147
+ this.scrollToEnd(this.displayEl);
1148
+
1149
+ if (this.hasInteracted) {
1150
+ this.updateOutputTargets(`${currency}${finalDisplay}`, rawVal);
1151
+ this.updateCalculationTargets(historyStr || 'Result:');
1152
+ }
1153
+ }
1154
+
1155
+ isRTL() {
1156
+ if (typeof document === 'undefined') return false;
1157
+ return (
1158
+ document.documentElement.getAttribute('dir') === 'rtl' ||
1159
+ document.documentElement.getAttribute('data-bs-direction') === 'rtl' ||
1160
+ document.body?.getAttribute('dir') === 'rtl' ||
1161
+ this.wrapper?.getAttribute('dir') === 'rtl' ||
1162
+ getComputedStyle(this.wrapper || document.documentElement).direction === 'rtl'
1163
+ );
1164
+ }
1165
+
1166
+ scrollToEnd(element) {
1167
+ if (!element) return;
1168
+ const rtl = this.isRTL();
1169
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
1170
+ if (typeof requestAnimationFrame === 'function') {
1171
+ requestAnimationFrame(() => {
1172
+ if (element) {
1173
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
1174
+ }
1175
+ });
1176
+ }
1177
+ }
1178
+
1179
+ syncTape() {
1180
+ if (!this.tapeListEl) return;
1181
+ this.tapeListEl.innerHTML = '';
1182
+ this.engine.tapeHistory.forEach((entry, idx) => {
1183
+ const item = document.createElement('div');
1184
+ item.className = 'd-flex justify-content-between py-1 border-bottom border-light-subtle';
1185
+ item.innerHTML = `<span class="text-secondary">${this.engine.tapeHistory.length - idx}.</span> <span>${entry}</span>`;
1186
+ this.tapeListEl.appendChild(item);
1187
+ });
1188
+ }
1189
+
1190
+ // Static Factory Methods
1191
+ static create(target, options = {}) {
1192
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1193
+ if (!el) return null;
1194
+ return new AdminUIUXCalculator(el, options);
1195
+ }
1196
+
1197
+ static init(target, options = {}) {
1198
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1199
+ if (!el) return null;
1200
+ return new AdminUIUXCalculator(el, options);
1201
+ }
1202
+
1203
+ static initAll() {
1204
+ if (typeof document === 'undefined') return;
1205
+ document.querySelectorAll('.calculator, [data-calculator-type], [data-calendar-type]').forEach(el => {
1206
+ if (!el._calculatorInstance) {
1207
+ new AdminUIUXCalculator(el);
1208
+ }
1209
+ });
1210
+ }
1211
+ }
1212
+
1213
+ // Attach static namespaces to AdminUIUXCalculator for convenience
1214
+ AdminUIUXCalculator.Calculator = Calculator;
1215
+ AdminUIUXCalculator.Formatter = Formatter;
1216
+ AdminUIUXCalculator.StreamCalculator = StreamCalculator;
1217
+
1218
+ // Automatic initialization in browser DOM
1219
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1220
+ if (document.readyState === 'loading') {
1221
+ document.addEventListener('DOMContentLoaded', () => AdminUIUXCalculator.initAll());
1222
+ } else {
1223
+ AdminUIUXCalculator.initAll();
1224
+ }
1225
+ }
1226
+
1227
+ return {
1228
+ AdminUIUXCalculator: AdminUIUXCalculator,
1229
+ Calculator: Calculator,
1230
+ Formatter: Formatter,
1231
+ StreamCalculator: StreamCalculator,
1232
+ default: AdminUIUXCalculator
1233
+ };
1234
+ });