@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,88 @@
1
+ /**
2
+ * @adminuiux/calculator - Formatter Module
3
+ * Zero-dependency numerical formatting, parsing, and symbol conversion utilities.
4
+ *
5
+ * @license MIT
6
+ * @copyright AdminUIUX
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ export const Formatter = {
12
+ /**
13
+ * Round a numeric value to a specified decimal precision, avoiding IEEE 754 floating point artifacts.
14
+ * @param {number} value
15
+ * @param {number} [precision=8]
16
+ * @returns {number}
17
+ */
18
+ round(value, precision = 8) {
19
+ if (!isFinite(value) || isNaN(value)) return 0;
20
+ const p = Math.pow(10, precision);
21
+ return Math.round((value + Number.EPSILON) * p) / p;
22
+ },
23
+
24
+ /**
25
+ * Format a number or numeric string with thousand separators and optional currency symbol.
26
+ * @param {number|string} num
27
+ * @param {Object} [options={}]
28
+ * @param {string} [options.currency=''] - Currency prefix (e.g., '$', '€', 'USD')
29
+ * @param {number} [options.precision] - Optional fixed decimal places to round to
30
+ * @returns {string}
31
+ */
32
+ format(num, options = {}) {
33
+ if (num === null || num === undefined || num === '') return '';
34
+ const parsed = typeof num === 'number' ? num : parseFloat(num);
35
+ if (isNaN(parsed)) return 'Error';
36
+
37
+ let targetNum = num;
38
+ if (typeof options.precision === 'number') {
39
+ targetNum = this.round(parsed, options.precision);
40
+ }
41
+
42
+ const str = String(targetNum);
43
+ if (str.includes('e') || str.includes('E')) return str;
44
+
45
+ const parts = str.split('.');
46
+ const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
47
+ const formatted = parts.length > 1 ? `${intPart}.${parts[1]}` : intPart;
48
+
49
+ if (options.currency) {
50
+ return `${options.currency} ${formatted}`;
51
+ }
52
+ return formatted;
53
+ },
54
+
55
+ /**
56
+ * Parse formatted string (e.g. "$ 1,234.56") into a standard JavaScript number.
57
+ * @param {string|number} str
58
+ * @returns {number}
59
+ */
60
+ parse(str) {
61
+ if (typeof str === 'number') return isNaN(str) ? 0 : str;
62
+ if (!str || typeof str !== 'string') return 0;
63
+ const clean = str.replace(/[^0-9.-]/g, '');
64
+ const val = parseFloat(clean);
65
+ return isNaN(val) ? 0 : val;
66
+ },
67
+
68
+ /**
69
+ * Convert standard code operators to human-readable math symbols.
70
+ * @param {string} op
71
+ * @returns {string}
72
+ */
73
+ getOperatorSymbol(op) {
74
+ switch (op) {
75
+ case '*':
76
+ case 'x':
77
+ return '×';
78
+ case '/':
79
+ return '÷';
80
+ case '-':
81
+ return '−';
82
+ default:
83
+ return op;
84
+ }
85
+ }
86
+ };
87
+
88
+ export default Formatter;
package/src/index.js ADDED
@@ -0,0 +1,434 @@
1
+ /**
2
+ * @adminuiux/calculator - Entry Point Module
3
+ * Universal export of Calculator, Formatter, StreamCalculator, and AdminUIUXCalculator.
4
+ *
5
+ * @license MIT
6
+ * @copyright AdminUIUX
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ import { Formatter } from './formatter.js';
12
+ import { Calculator, StreamCalculator } from './calculator.js';
13
+
14
+ export class AdminUIUXCalculator {
15
+ constructor(element, config = {}) {
16
+ if (typeof element === 'string') {
17
+ this.wrapper = document.querySelector(element);
18
+ } else {
19
+ this.wrapper = element;
20
+ }
21
+
22
+ if (!this.wrapper) return;
23
+ if (this.wrapper._calculatorInstance) return;
24
+ this.wrapper._calculatorInstance = this;
25
+
26
+ this.config = Object.assign({
27
+ type: this.wrapper.getAttribute('data-calculator-type') || this.wrapper.getAttribute('data-calendar-type') || this.wrapper.getAttribute('data-calc-mode') || 'basic',
28
+ precision: parseInt(this.wrapper.getAttribute('data-calc-precision'), 10) || (config.precision ?? 8),
29
+ angleMode: 'deg',
30
+ currency: this.wrapper.getAttribute('data-calc-currency') || config.currency || '',
31
+ taxRate: config.taxRate ?? 0.18,
32
+ tipRates: config.tipRates ?? [10, 15, 20],
33
+ quickPresets: config.quickPresets ?? [50, 100, 500, 1000],
34
+ enableKeyboard: config.enableKeyboard ?? true,
35
+ showTape: config.showTape ?? false,
36
+ onCalculate: config.onCalculate || null
37
+ }, config);
38
+
39
+ this.engine = new StreamCalculator(this.config);
40
+ this.hasInteracted = false;
41
+ this.memory = 0;
42
+
43
+ // Cache DOM Elements
44
+ this.displayEl = this.wrapper.querySelector('.calculator-display, [data-calc-display]');
45
+ this.historyEl = this.wrapper.querySelector('.calculator-history, [data-calc-history]');
46
+ this.memoryBadge = this.wrapper.querySelector('[data-calc-memory-badge]');
47
+ this.angleModeBtn = this.wrapper.querySelector('[data-calc-angle-toggle]');
48
+ this.tapeListEl = this.wrapper.querySelector('.calculator-tape, [data-calc-tape]');
49
+
50
+ this.initEventListeners();
51
+ this.updateDisplay();
52
+ }
53
+
54
+ initEventListeners() {
55
+ this.wrapper.querySelectorAll('[data-calc-action], [data-calculator-perfom], [data-calculator-perform]').forEach(btn => {
56
+ btn.addEventListener('click', (e) => {
57
+ e.preventDefault();
58
+ const performExpr = btn.getAttribute('data-calculator-perfom') || btn.getAttribute('data-calculator-perform');
59
+ if (performExpr) {
60
+ this.executePerform(performExpr);
61
+ return;
62
+ }
63
+ const action = btn.getAttribute('data-calc-action');
64
+ const val = btn.getAttribute('data-val');
65
+ this.handleAction(action, val);
66
+ });
67
+ });
68
+
69
+ if (this.config.enableKeyboard) {
70
+ this.wrapper.addEventListener('keydown', (e) => {
71
+ this.handleKeyDown(e);
72
+ });
73
+ if (!this.wrapper.hasAttribute('tabindex')) {
74
+ this.wrapper.setAttribute('tabindex', '0');
75
+ }
76
+ }
77
+ }
78
+
79
+ handleKeyDown(e) {
80
+ if (['input', 'textarea'].includes(document.activeElement?.tagName?.toLowerCase())) return;
81
+
82
+ const key = e.key;
83
+ if (!isNaN(key)) {
84
+ e.preventDefault();
85
+ this.handleAction('num', key);
86
+ } else if (key === '.') {
87
+ e.preventDefault();
88
+ this.handleAction('num', '.');
89
+ } else if (key === '+' || key === '-' || key === '*' || key === '/') {
90
+ e.preventDefault();
91
+ this.handleAction('operator', key);
92
+ } else if (key === 'Enter' || key === '=') {
93
+ e.preventDefault();
94
+ this.handleAction('equals');
95
+ } else if (key === 'Backspace') {
96
+ e.preventDefault();
97
+ this.handleAction('backspace');
98
+ } else if (key === 'Escape') {
99
+ e.preventDefault();
100
+ this.handleAction('clear');
101
+ } else if (key === '%') {
102
+ e.preventDefault();
103
+ this.handleAction('percent');
104
+ }
105
+ }
106
+
107
+ handleAction(action, val) {
108
+ this.hasInteracted = true;
109
+ switch (action) {
110
+ case 'num':
111
+ this.engine.inputDigit(val);
112
+ break;
113
+ case 'operator':
114
+ this.engine.inputOperator(val);
115
+ break;
116
+ case 'equals':
117
+ this.engine.inputEquals();
118
+ this.syncTape();
119
+ break;
120
+ case 'clear':
121
+ this.engine.clearAll();
122
+ break;
123
+ case 'clear-entry':
124
+ case 'backspace':
125
+ this.engine.backspace();
126
+ break;
127
+ case 'negate':
128
+ this.engine.toggleSign();
129
+ break;
130
+ case 'percent':
131
+ this.engine.applyPercent();
132
+ break;
133
+ case 'scientific':
134
+ this.applyScientific(val);
135
+ break;
136
+ case 'memory':
137
+ this.handleMemory(val);
138
+ break;
139
+ case 'angle-toggle':
140
+ this.toggleAngleMode();
141
+ break;
142
+ case 'preset':
143
+ this.applyPreset(parseFloat(val));
144
+ break;
145
+ case 'tax-add':
146
+ this.applyTax(true);
147
+ break;
148
+ case 'tax-sub':
149
+ this.applyTax(false);
150
+ break;
151
+ case 'tip':
152
+ this.applyTip(parseFloat(val));
153
+ break;
154
+ case 'discount':
155
+ this.applyDiscount(parseFloat(val));
156
+ break;
157
+ }
158
+ this.updateDisplay();
159
+ }
160
+
161
+ applyScientific(func) {
162
+ const val = this.engine.parseCurrent();
163
+ let res = val;
164
+ switch (func) {
165
+ case 'sin': res = Calculator.sin(val, this.config.angleMode, this.config.precision); break;
166
+ case 'cos': res = Calculator.cos(val, this.config.angleMode, this.config.precision); break;
167
+ case 'tan': res = Calculator.tan(val, this.config.angleMode, this.config.precision); break;
168
+ case 'asin': res = Calculator.asin(val, this.config.angleMode, this.config.precision); break;
169
+ case 'acos': res = Calculator.acos(val, this.config.angleMode, this.config.precision); break;
170
+ case 'atan': res = Calculator.atan(val, this.config.angleMode, this.config.precision); break;
171
+ case 'sqrt': res = Calculator.sqrt(val, this.config.precision); break;
172
+ case 'sqr': res = Calculator.sqr(val, this.config.precision); break;
173
+ case 'cube': res = Calculator.cube(val, this.config.precision); break;
174
+ case 'log': res = Calculator.log(val, this.config.precision); break;
175
+ case 'ln': res = Calculator.ln(val, this.config.precision); break;
176
+ case 'exp': res = Calculator.exp(val, this.config.precision); break;
177
+ case '1/x': res = Calculator.reciprocal(val, this.config.precision); break;
178
+ case 'abs': res = Calculator.abs(val); break;
179
+ case 'pi': res = Formatter.round(Math.PI, this.config.precision); break;
180
+ case 'e': res = Formatter.round(Math.E, this.config.precision); break;
181
+ case 'fact': res = Calculator.factorial(val); break;
182
+ }
183
+
184
+ this.engine.currentInput = String(res);
185
+ if (this.engine.hasJustCalculated) {
186
+ this.engine.historyTokens = [`${func}(${Formatter.format(val)}) =`];
187
+ }
188
+ }
189
+
190
+ handleMemory(action) {
191
+ const val = this.engine.parseCurrent();
192
+ switch (action) {
193
+ case 'mc': this.memory = 0; break;
194
+ case 'mr':
195
+ this.engine.currentInput = String(this.memory);
196
+ this.engine.hasJustCalculated = false;
197
+ break;
198
+ case 'm+': this.memory += val; break;
199
+ case 'm-': this.memory -= val; break;
200
+ case 'ms': this.memory = val; break;
201
+ }
202
+ if (this.memoryBadge) {
203
+ this.memoryBadge.style.display = this.memory !== 0 ? 'inline-block' : 'none';
204
+ }
205
+ }
206
+
207
+ toggleAngleMode() {
208
+ this.config.angleMode = this.config.angleMode === 'deg' ? 'rad' : 'deg';
209
+ if (this.angleModeBtn) {
210
+ this.angleModeBtn.textContent = this.config.angleMode.toUpperCase();
211
+ }
212
+ }
213
+
214
+ applyPreset(amount) {
215
+ const current = this.engine.parseCurrent();
216
+ this.engine.currentInput = String(current + amount);
217
+ this.engine.hasJustCalculated = false;
218
+ }
219
+
220
+ applyTax(isAdd) {
221
+ const current = this.engine.parseCurrent();
222
+ const rate = this.config.taxRate;
223
+ const result = Calculator.tax(current, rate, isAdd, this.config.precision);
224
+ this.engine.currentInput = String(result);
225
+ this.engine.historyTokens = [`${Formatter.format(current)} ${isAdd ? '+' : '-'} ${Math.round(rate * 100)}% Tax =`];
226
+ this.engine.hasJustCalculated = true;
227
+ }
228
+
229
+ applyTip(percent) {
230
+ const current = this.engine.parseCurrent();
231
+ const result = Calculator.tip(current, percent, this.config.precision);
232
+ this.engine.currentInput = String(result);
233
+ this.engine.historyTokens = [`${Formatter.format(current)} + ${percent}% Tip =`];
234
+ this.engine.hasJustCalculated = true;
235
+ }
236
+
237
+ applyDiscount(percent) {
238
+ const current = this.engine.parseCurrent();
239
+ const result = Calculator.discount(current, percent, this.config.precision);
240
+ this.engine.currentInput = String(result);
241
+ this.engine.historyTokens = [`${Formatter.format(current)} - ${percent}% Off =`];
242
+ this.engine.hasJustCalculated = true;
243
+ }
244
+
245
+ executePerform(expr) {
246
+ if (!expr) return;
247
+ this.hasInteracted = true;
248
+ const current = this.engine.parseCurrent();
249
+ const res = Calculator.executePerform(current, expr);
250
+ this.engine.currentInput = String(res.value);
251
+ if (res.history) {
252
+ this.engine.historyTokens = [res.history];
253
+ }
254
+ this.engine.hasJustCalculated = true;
255
+ this.updateDisplay();
256
+ }
257
+
258
+ getOutputTargets() {
259
+ const selector = this.wrapper.getAttribute('data-calculator-output') ||
260
+ this.wrapper.getAttribute('data-output-target') ||
261
+ this.config.outputTarget ||
262
+ this.config.output;
263
+ if (!selector) return [];
264
+ try {
265
+ return Array.from(document.querySelectorAll(selector));
266
+ } catch (e) {
267
+ return [];
268
+ }
269
+ }
270
+
271
+ getCalculationTargets() {
272
+ const selector = this.wrapper.getAttribute('data-calculator-calculation') ||
273
+ this.wrapper.getAttribute('data-calculation-target') ||
274
+ this.config.calculationTarget ||
275
+ this.config.calculation;
276
+ if (!selector) return [];
277
+ try {
278
+ return Array.from(document.querySelectorAll(selector));
279
+ } catch (e) {
280
+ return [];
281
+ }
282
+ }
283
+
284
+ updateOutputTargets(displayStr, rawVal) {
285
+ const targets = this.getOutputTargets();
286
+ if (targets.length === 0) return;
287
+
288
+ targets.forEach(target => {
289
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
290
+ target.value = displayStr;
291
+ target.dispatchEvent(new Event('input', { bubbles: true }));
292
+ target.dispatchEvent(new Event('change', { bubbles: true }));
293
+ } else {
294
+ target.textContent = displayStr;
295
+ }
296
+ });
297
+
298
+ this.wrapper.dispatchEvent(new CustomEvent('calculator:change', {
299
+ bubbles: true,
300
+ detail: {
301
+ value: rawVal,
302
+ formatted: displayStr,
303
+ history: this.engine.getHistoryString(),
304
+ calculator: this
305
+ }
306
+ }));
307
+ }
308
+
309
+ updateCalculationTargets(calculationStr) {
310
+ const targets = this.getCalculationTargets();
311
+ if (targets.length === 0) return;
312
+
313
+ targets.forEach(target => {
314
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
315
+ target.value = calculationStr;
316
+ target.dispatchEvent(new Event('input', { bubbles: true }));
317
+ target.dispatchEvent(new Event('change', { bubbles: true }));
318
+ } else {
319
+ target.textContent = calculationStr;
320
+ }
321
+ });
322
+ }
323
+
324
+ updateDisplay() {
325
+ const currency = this.config.currency ? `${this.config.currency} ` : '';
326
+ const rawVal = this.engine.parseCurrent();
327
+ let finalDisplay = this.engine.getDisplayValue();
328
+
329
+ const parts = finalDisplay.split('.');
330
+ const formattedInt = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
331
+ finalDisplay = parts.length > 1 ? `${formattedInt}.${parts[1]}` : formattedInt;
332
+
333
+ if (this.displayEl) {
334
+ this.displayEl.textContent = `${currency}${finalDisplay}`;
335
+ }
336
+
337
+ const historyStr = this.engine.getHistoryString();
338
+ if (this.historyEl) {
339
+ if (historyStr) {
340
+ this.historyEl.textContent = historyStr;
341
+ } else {
342
+ this.historyEl.innerHTML = ' ';
343
+ }
344
+ }
345
+
346
+ this.scrollToEnd(this.historyEl);
347
+ this.scrollToEnd(this.displayEl);
348
+
349
+ if (this.hasInteracted) {
350
+ this.updateOutputTargets(`${currency}${finalDisplay}`, rawVal);
351
+ this.updateCalculationTargets(historyStr || 'Result:');
352
+ }
353
+ }
354
+
355
+ isRTL() {
356
+ if (typeof document === 'undefined') return false;
357
+ return (
358
+ document.documentElement.getAttribute('dir') === 'rtl' ||
359
+ document.documentElement.getAttribute('data-bs-direction') === 'rtl' ||
360
+ document.body?.getAttribute('dir') === 'rtl' ||
361
+ this.wrapper?.getAttribute('dir') === 'rtl' ||
362
+ getComputedStyle(this.wrapper || document.documentElement).direction === 'rtl'
363
+ );
364
+ }
365
+
366
+ scrollToEnd(element) {
367
+ if (!element) return;
368
+ const rtl = this.isRTL();
369
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
370
+ if (typeof requestAnimationFrame === 'function') {
371
+ requestAnimationFrame(() => {
372
+ if (element) {
373
+ element.scrollLeft = rtl ? -element.scrollWidth : element.scrollWidth;
374
+ }
375
+ });
376
+ }
377
+ }
378
+
379
+ syncTape() {
380
+ if (!this.tapeListEl) return;
381
+ this.tapeListEl.innerHTML = '';
382
+ this.engine.tapeHistory.forEach((entry, idx) => {
383
+ const item = document.createElement('div');
384
+ item.className = 'd-flex justify-content-between py-1 border-bottom border-light-subtle';
385
+ item.innerHTML = `<span class="text-secondary">${this.engine.tapeHistory.length - idx}.</span> <span>${entry}</span>`;
386
+ this.tapeListEl.appendChild(item);
387
+ });
388
+ }
389
+
390
+ static create(target, options = {}) {
391
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
392
+ if (!el) return null;
393
+ return new AdminUIUXCalculator(el, options);
394
+ }
395
+
396
+ static init(target, options = {}) {
397
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
398
+ if (!el) return null;
399
+ return new AdminUIUXCalculator(el, options);
400
+ }
401
+
402
+ static initAll() {
403
+ if (typeof document === 'undefined') return;
404
+ document.querySelectorAll('.calculator, [data-calculator-type], [data-calendar-type]').forEach(el => {
405
+ if (!el._calculatorInstance) {
406
+ new AdminUIUXCalculator(el);
407
+ }
408
+ });
409
+ }
410
+ }
411
+
412
+ // Attach sub-modules
413
+ AdminUIUXCalculator.Calculator = Calculator;
414
+ AdminUIUXCalculator.Formatter = Formatter;
415
+ AdminUIUXCalculator.StreamCalculator = StreamCalculator;
416
+
417
+ // Browser global exposure & auto initialization
418
+ if (typeof window !== 'undefined') {
419
+ window.AdminUIUXCalculator = AdminUIUXCalculator;
420
+ window.Calculator = Calculator;
421
+ window.Formatter = Formatter;
422
+ window.StreamCalculator = StreamCalculator;
423
+
424
+ if (typeof document !== 'undefined') {
425
+ if (document.readyState === 'loading') {
426
+ document.addEventListener('DOMContentLoaded', () => AdminUIUXCalculator.initAll());
427
+ } else {
428
+ AdminUIUXCalculator.initAll();
429
+ }
430
+ }
431
+ }
432
+
433
+ export { Formatter, Calculator, StreamCalculator };
434
+ export default AdminUIUXCalculator;