Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37e45a2041 |
@@ -67,6 +67,9 @@
|
||||
<button class="widget-toolbar-btn" data-action="new-checklist" title="Checkliste erstellen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
|
||||
</button>
|
||||
<button class="widget-toolbar-btn" data-action="calculator" title="Taschenrechner">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><circle cx="8" cy="10" r="0.5"/><circle cx="12" cy="10" r="0.5"/><circle cx="16" cy="10" r="0.5"/><circle cx="8" cy="14" r="0.5"/><circle cx="12" cy="14" r="0.5"/><circle cx="16" cy="14" r="0.5"/><circle cx="8" cy="18" r="0.5"/><circle cx="12" cy="18" r="0.5"/><circle cx="16" cy="18" r="0.5"/></svg>
|
||||
</button>
|
||||
<button class="widget-toolbar-btn" data-action="notebook" title="Alle Notes">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
|
||||
</button>
|
||||
@@ -475,6 +478,7 @@
|
||||
<script src="src/js/search.js"></script>
|
||||
<script src="src/js/widgets.js"></script>
|
||||
<script src="src/js/notes.js"></script>
|
||||
<script src="src/js/calculator.js"></script>
|
||||
<script src="src/js/data.js"></script>
|
||||
<!-- Onboarding -->
|
||||
<script src="src/js/onboarding.js"></script>
|
||||
|
||||
@@ -1056,6 +1056,106 @@ body.show-desc .bm-desc { display: block; }
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CALCULATOR WIDGET
|
||||
============================================ */
|
||||
.calc-display {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
text-align: right;
|
||||
min-height: 48px;
|
||||
}
|
||||
.calc-expression {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
min-height: 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.calc-result {
|
||||
font-size: 22px;
|
||||
font-family: 'Rajdhani', monospace;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
.calc-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calc-btn {
|
||||
height: 36px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
.calc-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
.calc-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.calc-btn.operator {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.calc-btn.equals {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.calc-btn.equals:hover {
|
||||
background: var(--accent);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
.calc-btn.clear {
|
||||
color: var(--danger);
|
||||
}
|
||||
.calc-history {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 6px;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.calc-history-title {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.calc-history-item {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
padding: 3px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.calc-history-item:hover {
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
.calc-history-item .calc-h-result {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
WIDGET TOOLBAR
|
||||
============================================ */
|
||||
@@ -1492,6 +1592,7 @@ body.show-desc .bm-desc { display: block; }
|
||||
.search-bar { max-width: 400px; }
|
||||
.widget-toolbar-btn { width: 32px; height: 32px; }
|
||||
.notebook-panel { width: 320px; }
|
||||
.calc-btn { height: 32px; font-size: 13px; }
|
||||
}
|
||||
|
||||
/* Smartphone (max 480px) */
|
||||
@@ -1528,6 +1629,7 @@ body.show-desc .bm-desc { display: block; }
|
||||
.toolbar-left .widget-toolbar { left: 50%; right: auto; transform: translateX(-50%); }
|
||||
.widget-toolbar-btn { width: 32px; height: 32px; }
|
||||
.notebook-panel { width: 100%; right: -100%; }
|
||||
.calc-btn { height: 30px; font-size: 12px; }
|
||||
.toolbar-left .notebook-panel { left: -100%; }
|
||||
|
||||
.modal { width: calc(100vw - 32px); }
|
||||
|
||||
+3
-1
@@ -18,6 +18,7 @@ async function init() {
|
||||
initSearch();
|
||||
await migrateSticky();
|
||||
await Notes.init();
|
||||
await Calculator.init();
|
||||
initDataButtons();
|
||||
Store.checkQuota();
|
||||
|
||||
@@ -98,7 +99,8 @@ async function checkBackupReminder() {
|
||||
// JSON-Export auslösen (gleiche Logik wie btnExportJSON)
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
const notesData = (widgetData && Array.isArray(widgetData.notes)) ? widgetData.notes : [];
|
||||
const data = { version: '1.6.0', exported: new Date().toISOString(), boards, settings, notes: notesData };
|
||||
const calcHistory = (widgetData && widgetData.calculator) ? widgetData.calculator.history || [] : [];
|
||||
const data = { version: '1.7.0', exported: new Date().toISOString(), boards, settings, notes: notesData, calculator: calcHistory };
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
@@ -0,0 +1,729 @@
|
||||
/* =============================================
|
||||
HELLION NEWTAB — calculator.js
|
||||
Taschenrechner Widget: Expression-Parsing,
|
||||
History, Tastatureingabe
|
||||
============================================= */
|
||||
|
||||
const Calculator = {
|
||||
WIDGET_ID: 'widget_calculator',
|
||||
STORAGE_KEY: 'widgetStates',
|
||||
MAX_HISTORY: 10,
|
||||
|
||||
/** @type {Array<{expr: string, result: string}>} */
|
||||
_history: [],
|
||||
_currentExpr: '',
|
||||
_lastResult: '',
|
||||
_isOpen: false,
|
||||
_displayExprEl: null,
|
||||
_displayResultEl: null,
|
||||
_keydownHandler: null,
|
||||
|
||||
// ---- STORAGE ----
|
||||
|
||||
/**
|
||||
* Calculator-State aus Storage laden
|
||||
*/
|
||||
async load() {
|
||||
const data = await Store.get(this.STORAGE_KEY);
|
||||
if (data && data.calculator) {
|
||||
this._history = Array.isArray(data.calculator.history) ? data.calculator.history : [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculator-State in Storage speichern
|
||||
* Bestehende Notes-Daten bleiben erhalten
|
||||
*/
|
||||
async save() {
|
||||
const data = await Store.get(this.STORAGE_KEY) || {};
|
||||
const notesState = Array.isArray(data.notes) ? data.notes : [];
|
||||
|
||||
// Widget-Position aus WidgetManager holen
|
||||
const widgetState = WidgetManager.getState(this.WIDGET_ID);
|
||||
const calcData = {
|
||||
x: widgetState ? widgetState.x : 400,
|
||||
y: widgetState ? widgetState.y : 120,
|
||||
width: widgetState ? widgetState.width : 280,
|
||||
height: widgetState ? widgetState.height : 400,
|
||||
open: this._isOpen,
|
||||
history: this._history.slice(0, this.MAX_HISTORY)
|
||||
};
|
||||
|
||||
await Store.set(this.STORAGE_KEY, { notes: notesState, calculator: calcData });
|
||||
},
|
||||
|
||||
// ---- WIDGET LIFECYCLE ----
|
||||
|
||||
/**
|
||||
* Calculator oeffnen oder in Vordergrund bringen
|
||||
*/
|
||||
async open() {
|
||||
if (this._isOpen) {
|
||||
WidgetManager.bringToFront(this.WIDGET_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gespeicherte Position laden
|
||||
const data = await Store.get(this.STORAGE_KEY);
|
||||
const saved = (data && data.calculator) ? data.calculator : {};
|
||||
|
||||
const widgetId = WidgetManager.create('calculator', {
|
||||
id: this.WIDGET_ID,
|
||||
title: 'Taschenrechner',
|
||||
x: saved.x || 400,
|
||||
y: saved.y || 120,
|
||||
width: saved.width || 280,
|
||||
height: saved.height || 400,
|
||||
open: true
|
||||
});
|
||||
|
||||
const body = WidgetManager.getBody(widgetId);
|
||||
if (body) this.renderBody(body);
|
||||
|
||||
this._isOpen = true;
|
||||
|
||||
// Keyboard-Events binden
|
||||
const entry = WidgetManager._widgets.get(this.WIDGET_ID);
|
||||
if (entry) this._bindKeyboard(entry.el);
|
||||
|
||||
await this.save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculator toggle: oeffnen oder minimieren
|
||||
*/
|
||||
async toggle() {
|
||||
if (this._isOpen) {
|
||||
const entry = WidgetManager._widgets.get(this.WIDGET_ID);
|
||||
if (entry && entry.state.open) {
|
||||
await WidgetManager.minimize(this.WIDGET_ID);
|
||||
this._isOpen = false;
|
||||
await this.save();
|
||||
} else if (entry) {
|
||||
await WidgetManager.openWidget(this.WIDGET_ID);
|
||||
this._isOpen = true;
|
||||
await this.save();
|
||||
}
|
||||
} else {
|
||||
await this.open();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn Widget geschlossen wird
|
||||
*/
|
||||
async onClose() {
|
||||
this._isOpen = false;
|
||||
this._unbindKeyboard();
|
||||
this._displayExprEl = null;
|
||||
this._displayResultEl = null;
|
||||
await this.save();
|
||||
},
|
||||
|
||||
// ---- UI RENDERING ----
|
||||
|
||||
/**
|
||||
* Calculator-Body rendern (in Widget-Body einfuegen)
|
||||
* @param {HTMLElement} bodyEl
|
||||
*/
|
||||
renderBody(bodyEl) {
|
||||
bodyEl.textContent = '';
|
||||
bodyEl.style.padding = '8px';
|
||||
bodyEl.style.display = 'flex';
|
||||
bodyEl.style.flexDirection = 'column';
|
||||
|
||||
// Display
|
||||
const display = document.createElement('div');
|
||||
display.className = 'calc-display';
|
||||
|
||||
const exprEl = document.createElement('div');
|
||||
exprEl.className = 'calc-expression';
|
||||
this._displayExprEl = exprEl;
|
||||
|
||||
const resultEl = document.createElement('div');
|
||||
resultEl.className = 'calc-result';
|
||||
resultEl.textContent = '0';
|
||||
this._displayResultEl = resultEl;
|
||||
|
||||
display.append(exprEl, resultEl);
|
||||
|
||||
// Buttons
|
||||
const buttonsEl = this._createButtons();
|
||||
|
||||
// History
|
||||
const historyEl = this._createHistoryPanel();
|
||||
|
||||
bodyEl.append(display, buttonsEl, historyEl);
|
||||
|
||||
// Aktuellen State anzeigen
|
||||
this._updateDisplay();
|
||||
},
|
||||
|
||||
/**
|
||||
* Button-Grid erstellen (4x5)
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
_createButtons() {
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'calc-buttons';
|
||||
|
||||
// Button-Layout: [label, value, cssClass]
|
||||
const buttons = [
|
||||
['C', 'clear', 'clear'],
|
||||
['()', 'paren', 'operator'],
|
||||
['%', '%', 'operator'],
|
||||
['\u00F7', '/', 'operator'],
|
||||
['7', '7', ''],
|
||||
['8', '8', ''],
|
||||
['9', '9', ''],
|
||||
['\u00D7', '*', 'operator'],
|
||||
['4', '4', ''],
|
||||
['5', '5', ''],
|
||||
['6', '6', ''],
|
||||
['\u2212', '-', 'operator'],
|
||||
['1', '1', ''],
|
||||
['2', '2', ''],
|
||||
['3', '3', ''],
|
||||
['+', '+', 'operator'],
|
||||
['0', '0', ''],
|
||||
['.', '.', ''],
|
||||
['\u232B', 'backspace', ''],
|
||||
['=', '=', 'equals']
|
||||
];
|
||||
|
||||
buttons.forEach(([label, value, cls]) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'calc-btn' + (cls ? ' ' + cls : '');
|
||||
btn.textContent = label;
|
||||
btn.type = 'button';
|
||||
btn.addEventListener('click', () => this._handleKey(value));
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
|
||||
return grid;
|
||||
},
|
||||
|
||||
/**
|
||||
* History-Panel erstellen
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
_createHistoryPanel() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'calc-history';
|
||||
container.id = 'calcHistoryPanel';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'calc-history-title';
|
||||
title.textContent = 'History';
|
||||
container.appendChild(title);
|
||||
|
||||
this._renderHistoryItems(container);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* History-Items rendern
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
_renderHistoryItems(container) {
|
||||
// Alte Items entfernen (nur die .calc-history-item Elemente)
|
||||
const oldItems = container.querySelectorAll('.calc-history-item');
|
||||
oldItems.forEach(item => item.remove());
|
||||
|
||||
if (this._history.length === 0) return;
|
||||
|
||||
// Neueste zuerst
|
||||
const reversed = [...this._history].reverse();
|
||||
reversed.forEach(entry => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'calc-history-item';
|
||||
|
||||
const exprSpan = document.createElement('span');
|
||||
exprSpan.textContent = entry.expr;
|
||||
|
||||
const resultSpan = document.createElement('span');
|
||||
resultSpan.className = 'calc-h-result';
|
||||
resultSpan.textContent = '= ' + entry.result;
|
||||
|
||||
item.append(exprSpan, resultSpan);
|
||||
|
||||
// Klick uebernimmt Ergebnis als neue Eingabe
|
||||
item.addEventListener('click', () => {
|
||||
this._currentExpr = entry.result;
|
||||
this._lastResult = '';
|
||||
this._updateDisplay();
|
||||
});
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
},
|
||||
|
||||
// ---- INPUT HANDLING ----
|
||||
|
||||
/**
|
||||
* Taste verarbeiten
|
||||
* @param {string} key
|
||||
*/
|
||||
_handleKey(key) {
|
||||
switch (key) {
|
||||
case 'clear':
|
||||
this._currentExpr = '';
|
||||
this._lastResult = '';
|
||||
break;
|
||||
|
||||
case 'backspace':
|
||||
this._currentExpr = this._currentExpr.slice(0, -1);
|
||||
break;
|
||||
|
||||
case '=':
|
||||
this._calculate();
|
||||
return;
|
||||
|
||||
case 'paren': {
|
||||
// Smarte Klammern: oeffnende wenn noetig, sonst schliessende
|
||||
const openCount = (this._currentExpr.match(/\(/g) || []).length;
|
||||
const closeCount = (this._currentExpr.match(/\)/g) || []).length;
|
||||
const lastChar = this._currentExpr.slice(-1);
|
||||
if (openCount <= closeCount || /[+\-*/%(]$/.test(lastChar) || this._currentExpr === '') {
|
||||
this._currentExpr += '(';
|
||||
} else {
|
||||
this._currentExpr += ')';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case '%':
|
||||
case '+':
|
||||
case '-':
|
||||
case '*':
|
||||
case '/': {
|
||||
// Wenn gerade ein Ergebnis angezeigt wird, damit weiterrechnen
|
||||
if (this._lastResult && this._currentExpr === '') {
|
||||
this._currentExpr = this._lastResult;
|
||||
this._lastResult = '';
|
||||
}
|
||||
// Doppelte Operatoren verhindern (letzten ersetzen)
|
||||
const last = this._currentExpr.slice(-1);
|
||||
if (/[+\-*/%]/.test(last)) {
|
||||
this._currentExpr = this._currentExpr.slice(0, -1) + key;
|
||||
} else {
|
||||
this._currentExpr += key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case '.': {
|
||||
// Doppelten Dezimalpunkt im letzten Zahlenblock verhindern
|
||||
const parts = this._currentExpr.split(/[+\-*/%()]/);
|
||||
const lastPart = parts[parts.length - 1];
|
||||
if (lastPart && lastPart.includes('.')) break;
|
||||
this._currentExpr += key;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Ziffern 0-9
|
||||
if (/^[0-9]$/.test(key)) {
|
||||
// Wenn ein Ergebnis da ist und User eine Zahl tippt, neue Berechnung starten
|
||||
if (this._lastResult && this._currentExpr === '') {
|
||||
this._lastResult = '';
|
||||
}
|
||||
this._currentExpr += key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._updateDisplay();
|
||||
},
|
||||
|
||||
/**
|
||||
* Berechnung ausfuehren
|
||||
*/
|
||||
async _calculate() {
|
||||
if (!this._currentExpr) return;
|
||||
|
||||
const result = this._evaluate(this._currentExpr);
|
||||
if (result === null) {
|
||||
this._lastResult = 'Fehler';
|
||||
this._updateDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
const resultStr = this._formatResult(result);
|
||||
this._addHistory(this._currentExpr, resultStr);
|
||||
this._lastResult = resultStr;
|
||||
|
||||
// Display aktualisieren
|
||||
if (this._displayExprEl) {
|
||||
this._displayExprEl.textContent = this._formatExpression(this._currentExpr) + ' =';
|
||||
}
|
||||
if (this._displayResultEl) {
|
||||
this._displayResultEl.textContent = resultStr;
|
||||
}
|
||||
|
||||
this._currentExpr = '';
|
||||
|
||||
// History-Panel aktualisieren
|
||||
const historyPanel = document.getElementById('calcHistoryPanel');
|
||||
if (historyPanel) this._renderHistoryItems(historyPanel);
|
||||
|
||||
await this.save();
|
||||
},
|
||||
|
||||
// ---- EXPRESSION PARSER (Shunting-Yard, KEIN eval!) ----
|
||||
|
||||
/**
|
||||
* Expression sicher auswerten
|
||||
* @param {string} expr
|
||||
* @returns {number|null}
|
||||
*/
|
||||
_evaluate(expr) {
|
||||
try {
|
||||
// Nur erlaubte Zeichen
|
||||
const sanitized = expr.replace(/[^0-9+\-*/.%()]/g, '');
|
||||
if (!sanitized) return null;
|
||||
|
||||
const tokens = this._tokenize(sanitized);
|
||||
if (!tokens) return null;
|
||||
|
||||
return this._parseExpression(tokens);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expression in Tokens aufteilen
|
||||
* @param {string} expr
|
||||
* @returns {Array|null}
|
||||
*/
|
||||
_tokenize(expr) {
|
||||
const tokens = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < expr.length) {
|
||||
const ch = expr[i];
|
||||
|
||||
// Zahl (inkl. Dezimal)
|
||||
if (/[0-9.]/.test(ch)) {
|
||||
let num = '';
|
||||
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
||||
num += expr[i];
|
||||
i++;
|
||||
}
|
||||
const parsed = parseFloat(num);
|
||||
if (isNaN(parsed)) return null;
|
||||
tokens.push({ type: 'number', value: parsed });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Operator
|
||||
if (/[+\-*/%]/.test(ch)) {
|
||||
// Negativer Vorzeichen-Check: am Anfang oder nach Operator/oeffnender Klammer
|
||||
if (ch === '-') {
|
||||
const prev = tokens[tokens.length - 1];
|
||||
if (!prev || prev.type === 'op' || (prev.type === 'paren' && prev.value === '(')) {
|
||||
// Negatives Vorzeichen → als Teil der naechsten Zahl lesen
|
||||
let num = '-';
|
||||
i++;
|
||||
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
||||
num += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (num === '-') return null;
|
||||
const parsed = parseFloat(num);
|
||||
if (isNaN(parsed)) return null;
|
||||
tokens.push({ type: 'number', value: parsed });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
tokens.push({ type: 'op', value: ch });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Klammern
|
||||
if (ch === '(' || ch === ')') {
|
||||
tokens.push({ type: 'paren', value: ch });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unbekanntes Zeichen
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
},
|
||||
|
||||
/**
|
||||
* Rekursiver Descent Parser mit Operator-Precedence
|
||||
* @param {Array} tokens
|
||||
* @returns {number|null}
|
||||
*/
|
||||
_parseExpression(tokens) {
|
||||
let pos = 0;
|
||||
|
||||
function peek() { return tokens[pos]; }
|
||||
function consume() { return tokens[pos++]; }
|
||||
|
||||
// Expression: Term (('+' | '-') Term)*
|
||||
function parseExpr() {
|
||||
let left = parseTerm();
|
||||
if (left === null) return null;
|
||||
|
||||
while (pos < tokens.length) {
|
||||
const t = peek();
|
||||
if (!t || t.type !== 'op' || (t.value !== '+' && t.value !== '-')) break;
|
||||
consume();
|
||||
const right = parseTerm();
|
||||
if (right === null) return null;
|
||||
left = t.value === '+' ? left + right : left - right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// Term: Factor (('*' | '/' | '%') Factor)*
|
||||
function parseTerm() {
|
||||
let left = parseFactor();
|
||||
if (left === null) return null;
|
||||
|
||||
while (pos < tokens.length) {
|
||||
const t = peek();
|
||||
if (!t || t.type !== 'op' || (t.value !== '*' && t.value !== '/' && t.value !== '%')) break;
|
||||
consume();
|
||||
const right = parseFactor();
|
||||
if (right === null) return null;
|
||||
if (t.value === '*') {
|
||||
left = left * right;
|
||||
} else if (t.value === '/') {
|
||||
if (right === 0) return null;
|
||||
left = left / right;
|
||||
} else {
|
||||
left = left % right;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// Factor: Number | '(' Expression ')'
|
||||
function parseFactor() {
|
||||
const t = peek();
|
||||
if (!t) return null;
|
||||
|
||||
if (t.type === 'number') {
|
||||
consume();
|
||||
return t.value;
|
||||
}
|
||||
|
||||
if (t.type === 'paren' && t.value === '(') {
|
||||
consume();
|
||||
const val = parseExpr();
|
||||
if (val === null) return null;
|
||||
const closing = peek();
|
||||
if (closing && closing.type === 'paren' && closing.value === ')') {
|
||||
consume();
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = parseExpr();
|
||||
|
||||
// Alle Tokens muessen verbraucht sein
|
||||
if (pos < tokens.length) return null;
|
||||
|
||||
if (result === null || !isFinite(result)) return null;
|
||||
return result;
|
||||
},
|
||||
|
||||
// ---- FORMATTING ----
|
||||
|
||||
/**
|
||||
* Ergebnis formatieren (maximal 10 Dezimalstellen, trailing Nullen entfernen)
|
||||
* @param {number} num
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatResult(num) {
|
||||
if (Number.isInteger(num)) return num.toString();
|
||||
// Maximal 10 Dezimalstellen, trailing Nullen weg
|
||||
const str = num.toFixed(10).replace(/\.?0+$/, '');
|
||||
return str;
|
||||
},
|
||||
|
||||
/**
|
||||
* Expression fuer Anzeige formatieren (× statt *, ÷ statt /)
|
||||
* @param {string} expr
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatExpression(expr) {
|
||||
return expr
|
||||
.replace(/\*/g, '\u00D7')
|
||||
.replace(/\//g, '\u00F7');
|
||||
},
|
||||
|
||||
// ---- DISPLAY ----
|
||||
|
||||
/**
|
||||
* Display aktualisieren
|
||||
*/
|
||||
_updateDisplay() {
|
||||
if (this._displayExprEl) {
|
||||
if (this._lastResult) {
|
||||
// Ergebnis-Modus: Expression oben, Ergebnis gross
|
||||
// (wird von _calculate() direkt gesetzt)
|
||||
} else {
|
||||
this._displayExprEl.textContent = '';
|
||||
}
|
||||
}
|
||||
if (this._displayResultEl) {
|
||||
if (this._lastResult && this._currentExpr === '') {
|
||||
this._displayResultEl.textContent = this._lastResult;
|
||||
} else {
|
||||
this._displayResultEl.textContent = this._formatExpression(this._currentExpr) || '0';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ---- HISTORY ----
|
||||
|
||||
/**
|
||||
* History-Eintrag hinzufuegen
|
||||
* @param {string} expr
|
||||
* @param {string} result
|
||||
*/
|
||||
_addHistory(expr, result) {
|
||||
this._history.push({
|
||||
expr: this._formatExpression(expr),
|
||||
result: result
|
||||
});
|
||||
// Limit einhalten
|
||||
if (this._history.length > this.MAX_HISTORY) {
|
||||
this._history = this._history.slice(-this.MAX_HISTORY);
|
||||
}
|
||||
},
|
||||
|
||||
// ---- KEYBOARD ----
|
||||
|
||||
/**
|
||||
* Tastatur-Events binden
|
||||
* @param {HTMLElement} widgetEl
|
||||
*/
|
||||
_bindKeyboard(widgetEl) {
|
||||
this._unbindKeyboard();
|
||||
|
||||
this._keydownHandler = (e) => {
|
||||
// Nur reagieren wenn Calculator-Widget fokussiert ist
|
||||
// (d.h. nicht wenn User in Textarea/Input tippt)
|
||||
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
|
||||
if (e.target.contentEditable === 'true') return;
|
||||
|
||||
const key = e.key;
|
||||
let handled = false;
|
||||
|
||||
if (/^[0-9]$/.test(key)) {
|
||||
this._handleKey(key);
|
||||
handled = true;
|
||||
} else if (key === '+' || key === '-' || key === '*' || key === '/') {
|
||||
this._handleKey(key);
|
||||
handled = true;
|
||||
} else if (key === '.') {
|
||||
this._handleKey('.');
|
||||
handled = true;
|
||||
} else if (key === '%') {
|
||||
this._handleKey('%');
|
||||
handled = true;
|
||||
} else if (key === '(' || key === ')') {
|
||||
this._handleKey('paren');
|
||||
handled = true;
|
||||
} else if (key === 'Enter' || key === '=') {
|
||||
this._handleKey('=');
|
||||
handled = true;
|
||||
} else if (key === 'Backspace') {
|
||||
this._handleKey('backspace');
|
||||
handled = true;
|
||||
} else if (key === 'Escape' || key === 'c' || key === 'C') {
|
||||
this._handleKey('clear');
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
widgetEl.addEventListener('keydown', this._keydownHandler);
|
||||
// Widget fokussierbar machen
|
||||
widgetEl.tabIndex = 0;
|
||||
widgetEl.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Keyboard-Events entfernen
|
||||
*/
|
||||
_unbindKeyboard() {
|
||||
if (this._keydownHandler) {
|
||||
const entry = WidgetManager._widgets.get(this.WIDGET_ID);
|
||||
if (entry) {
|
||||
entry.el.removeEventListener('keydown', this._keydownHandler);
|
||||
}
|
||||
this._keydownHandler = null;
|
||||
}
|
||||
},
|
||||
|
||||
// ---- INIT ----
|
||||
|
||||
/**
|
||||
* Calculator initialisieren (aus app.js aufgerufen)
|
||||
*/
|
||||
async init() {
|
||||
await this.load();
|
||||
|
||||
// Wenn Calculator beim letzten Mal offen war, wiederherstellen
|
||||
const data = await Store.get(this.STORAGE_KEY);
|
||||
if (data && data.calculator && data.calculator.open) {
|
||||
await this.open();
|
||||
}
|
||||
|
||||
// Close-Event abfangen: WidgetManager.close() ueberschreiben
|
||||
const origClose = WidgetManager.close.bind(WidgetManager);
|
||||
const self = this;
|
||||
WidgetManager.close = function(id) {
|
||||
origClose(id);
|
||||
if (id === self.WIDGET_ID) {
|
||||
self.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Minimize-Event abfangen
|
||||
const origMinimize = WidgetManager.minimize.bind(WidgetManager);
|
||||
WidgetManager.minimize = async function(id) {
|
||||
await origMinimize(id);
|
||||
if (id === self.WIDGET_ID) {
|
||||
self._isOpen = false;
|
||||
await self.save();
|
||||
}
|
||||
};
|
||||
|
||||
// Open-Event abfangen
|
||||
const origOpen = WidgetManager.openWidget.bind(WidgetManager);
|
||||
WidgetManager.openWidget = async function(id) {
|
||||
await origOpen(id);
|
||||
if (id === self.WIDGET_ID) {
|
||||
self._isOpen = true;
|
||||
// Body neu rendern (war durch minimize entfernt)
|
||||
const body = WidgetManager.getBody(self.WIDGET_ID);
|
||||
if (body && body.children.length === 0) {
|
||||
self.renderBody(body);
|
||||
}
|
||||
const entry = WidgetManager._widgets.get(self.WIDGET_ID);
|
||||
if (entry) self._bindKeyboard(entry.el);
|
||||
await self.save();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+25
-6
@@ -13,11 +13,12 @@ function initDataButtons() {
|
||||
btnExport.addEventListener('click', async () => {
|
||||
const widgetData = await Store.get('widgetStates');
|
||||
const data = {
|
||||
version: '1.6.0',
|
||||
version: '1.7.0',
|
||||
exported: new Date().toISOString(),
|
||||
boards,
|
||||
settings,
|
||||
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : []
|
||||
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : [],
|
||||
calculator: widgetData && widgetData.calculator ? widgetData.calculator.history || [] : []
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -60,9 +61,9 @@ function initDataButtons() {
|
||||
|
||||
// Notes importieren (falls vorhanden)
|
||||
let notesImported = 0;
|
||||
const existingWidgets = await Store.get('widgetStates') || {};
|
||||
if (Array.isArray(data.notes) && data.notes.length > 0) {
|
||||
const existingWidgets = await Store.get('widgetStates');
|
||||
const existingNotes = (existingWidgets && Array.isArray(existingWidgets.notes)) ? existingWidgets.notes : [];
|
||||
const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : [];
|
||||
const importNotes = data.notes.filter(n => {
|
||||
if (!n || !n.id || !n.template) return false;
|
||||
n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : [];
|
||||
@@ -73,15 +74,33 @@ function initDataButtons() {
|
||||
const toImport = importNotes.slice(0, spaceLeft);
|
||||
if (toImport.length > 0) {
|
||||
const merged = [...existingNotes, ...toImport];
|
||||
await Store.set('widgetStates', { notes: merged });
|
||||
existingWidgets.notes = merged;
|
||||
Notes._notes = merged;
|
||||
notesImported = toImport.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculator-History importieren (falls vorhanden)
|
||||
let calcImported = false;
|
||||
if (Array.isArray(data.calculator) && data.calculator.length > 0) {
|
||||
const calcHistory = data.calculator.filter(h => h && typeof h.expr === 'string' && typeof h.result === 'string');
|
||||
if (calcHistory.length > 0) {
|
||||
if (!existingWidgets.calculator) {
|
||||
existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] };
|
||||
}
|
||||
existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY);
|
||||
Calculator._history = existingWidgets.calculator.history;
|
||||
calcImported = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Gemeinsam speichern
|
||||
await Store.set('widgetStates', existingWidgets);
|
||||
|
||||
const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : '';
|
||||
const calcMsg = calcImported ? ' + Calculator-History' : '';
|
||||
await HellionDialog.alert(
|
||||
`${validBoards.length} Board(s)${noteMsg} erfolgreich importiert.`,
|
||||
`${validBoards.length} Board(s)${noteMsg}${calcMsg} erfolgreich importiert.`,
|
||||
{ type: 'success', title: 'Import erfolgreich' }
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
+9
-1
@@ -44,7 +44,13 @@ const Notes = {
|
||||
return note;
|
||||
});
|
||||
|
||||
await Store.set(this.STORAGE_KEY, { notes: merged });
|
||||
// Calculator-State beibehalten falls vorhanden
|
||||
const existing = await Store.get(this.STORAGE_KEY);
|
||||
const saveData = { notes: merged };
|
||||
if (existing && existing.calculator) {
|
||||
saveData.calculator = existing.calculator;
|
||||
}
|
||||
await Store.set(this.STORAGE_KEY, saveData);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -514,6 +520,8 @@ const Notes = {
|
||||
await this.create('text');
|
||||
} else if (action === 'new-checklist') {
|
||||
await this.create('checklist');
|
||||
} else if (action === 'calculator') {
|
||||
Calculator.toggle();
|
||||
} else if (action === 'notebook') {
|
||||
this.openNotebook();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user