Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18a04b884c | |||
| 7a16462358 |
+44
-10
@@ -59,17 +59,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- STICKY NOTE -->
|
<!-- WIDGET TOOLBAR -->
|
||||||
<div class="sticky-note" id="stickyNote">
|
<div class="widget-toolbar" id="widgetToolbar">
|
||||||
<div class="sticky-note-header" id="stickyNoteHeader">
|
<button class="widget-toolbar-btn" data-action="new-note" title="Note erstellen">
|
||||||
<span class="sticky-note-title">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
</button>
|
||||||
Note
|
<button class="widget-toolbar-btn" data-action="new-checklist" title="Checkliste erstellen">
|
||||||
</span>
|
<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 class="sticky-note-close" id="stickyNoteClose">✕</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<textarea class="sticky-note-body" id="stickyNoteBody" placeholder="Quick note…" spellcheck="false"></textarea>
|
|
||||||
|
<!-- NOTEBOOK SIDEBAR -->
|
||||||
|
<div class="notebook-overlay" id="notebookOverlay"></div>
|
||||||
|
<aside class="notebook-panel" id="notebookPanel">
|
||||||
|
<div class="notebook-header">
|
||||||
|
<span class="notebook-header-title">Notebook <span class="notebook-count" id="notebookCount">0 / 5</span></span>
|
||||||
|
<button class="btn-close" id="btnCloseNotebook">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="notebook-slots" id="notebookSlots">
|
||||||
|
<!-- dynamisch via JS -->
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- BOARDS CONTAINER -->
|
<!-- BOARDS CONTAINER -->
|
||||||
<main class="boards-wrapper" id="boardsWrapper">
|
<main class="boards-wrapper" id="boardsWrapper">
|
||||||
@@ -161,6 +174,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- WIDGETS -->
|
||||||
|
<section class="settings-section" data-section="widgets">
|
||||||
|
<button class="settings-section-title" type="button">
|
||||||
|
<span class="section-chevron">▸</span>
|
||||||
|
WIDGETS
|
||||||
|
</button>
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-label">Toolbar-Position</span>
|
||||||
|
<span class="setting-desc">Widget-Toolbar links oder rechts</span>
|
||||||
|
</div>
|
||||||
|
<select class="select-input" id="settingToolbarPos">
|
||||||
|
<option value="right" selected>Rechts</option>
|
||||||
|
<option value="left">Links</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- DATA -->
|
<!-- DATA -->
|
||||||
<section class="settings-section" data-section="data">
|
<section class="settings-section" data-section="data">
|
||||||
<button class="settings-section-title" type="button">
|
<button class="settings-section-title" type="button">
|
||||||
@@ -440,7 +473,8 @@
|
|||||||
<script src="src/js/boards.js"></script>
|
<script src="src/js/boards.js"></script>
|
||||||
<script src="src/js/settings.js"></script>
|
<script src="src/js/settings.js"></script>
|
||||||
<script src="src/js/search.js"></script>
|
<script src="src/js/search.js"></script>
|
||||||
<script src="src/js/sticky.js"></script>
|
<script src="src/js/widgets.js"></script>
|
||||||
|
<script src="src/js/notes.js"></script>
|
||||||
<script src="src/js/data.js"></script>
|
<script src="src/js/data.js"></script>
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<script src="src/js/onboarding.js"></script>
|
<script src="src/js/onboarding.js"></script>
|
||||||
|
|||||||
+307
-21
@@ -904,62 +904,149 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.search-submit:hover { color: var(--accent); }
|
.search-submit:hover { color: var(--accent); }
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
STICKY NOTE
|
WIDGET SYSTEM
|
||||||
============================================ */
|
============================================ */
|
||||||
.sticky-note {
|
.widget {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px; right: 24px;
|
z-index: 51;
|
||||||
z-index: 50;
|
min-width: 200px; min-height: 150px;
|
||||||
width: 240px;
|
|
||||||
background: var(--bg-board);
|
background: var(--bg-board);
|
||||||
border: 1px solid var(--border-accent);
|
border: 1px solid var(--border-accent);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--accent-dim);
|
box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--accent-dim);
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
transition: opacity 0.25s, transform 0.25s, border-color 0.5s;
|
transition: border-color 0.5s;
|
||||||
|
}
|
||||||
|
.widget.widget-minimized {
|
||||||
opacity: 0; transform: translateY(8px) scale(0.97);
|
opacity: 0; transform: translateY(8px) scale(0.97);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
transition: opacity 0.25s, transform 0.25s;
|
||||||
.sticky-note.visible {
|
|
||||||
opacity: 1; transform: translateY(0) scale(1);
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-note-header {
|
.widget-header {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 7px 10px 6px;
|
padding: 7px 10px 6px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
cursor: move;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.sticky-note-title {
|
.widget-header:active { cursor: grabbing; }
|
||||||
|
.widget-title {
|
||||||
display: flex; align-items: center; gap: 5px;
|
display: flex; align-items: center; gap: 5px;
|
||||||
font-family: var(--font-display); font-size: 11px; font-weight: 600;
|
font-family: var(--font-display); font-size: 11px; font-weight: 600;
|
||||||
letter-spacing: 1.5px; text-transform: uppercase;
|
letter-spacing: 1.5px; text-transform: uppercase;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.sticky-note-close {
|
.widget-title[contenteditable="true"] {
|
||||||
|
outline: 1px solid var(--border-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 3px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.widget-actions {
|
||||||
|
display: flex; align-items: center; gap: 2px;
|
||||||
|
}
|
||||||
|
.widget-btn {
|
||||||
background: none; border: none;
|
background: none; border: none;
|
||||||
color: var(--text-muted); font-size: 11px;
|
color: var(--text-muted); font-size: 11px;
|
||||||
cursor: pointer; padding: 1px 4px; border-radius: 3px;
|
cursor: pointer; padding: 1px 4px; border-radius: 3px;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.sticky-note-close:hover { background: rgba(255,255,255,0.07); color: var(--text-primary); }
|
.widget-btn:hover { background: rgba(255,255,255,0.07); color: var(--text-primary); }
|
||||||
|
|
||||||
.sticky-note-body {
|
.widget-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widget Textarea (Freitext-Note) */
|
||||||
|
.widget-textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: none; border: none; outline: none; resize: none;
|
background: none; border: none; outline: none; resize: none;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: var(--font-body); font-size: 12px; line-height: 1.6;
|
font-family: var(--font-body); font-size: 12px; line-height: 1.6;
|
||||||
min-height: 120px; max-height: 300px;
|
|
||||||
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
||||||
}
|
}
|
||||||
.sticky-note-body::placeholder { color: var(--text-muted); }
|
.widget-textarea::placeholder { color: var(--text-muted); }
|
||||||
|
.widget-char-count {
|
||||||
|
position: absolute; bottom: 4px; right: 8px;
|
||||||
|
font-family: var(--font-body); font-size: 10px;
|
||||||
|
color: var(--text-muted); pointer-events: none;
|
||||||
|
}
|
||||||
|
.widget-char-count.limit { color: var(--danger); }
|
||||||
|
|
||||||
/* Resize-handle unten rechts */
|
/* Widget Checkliste */
|
||||||
.sticky-note::after {
|
.widget-checklist {
|
||||||
|
flex: 1;
|
||||||
|
list-style: none; margin: 0; padding: 6px 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
.checklist-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-family: var(--font-body); font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
.checklist-item.checked .checklist-text {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.checklist-checkbox {
|
||||||
|
width: 14px; height: 14px;
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
cursor: pointer; flex-shrink: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: var(--accent); font-size: 10px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.checklist-checkbox:hover { border-color: var(--accent); }
|
||||||
|
.checklist-item.checked .checklist-checkbox {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.checklist-text {
|
||||||
|
flex: 1; min-width: 0;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.checklist-remove {
|
||||||
|
background: none; border: none;
|
||||||
|
color: var(--text-muted); font-size: 10px;
|
||||||
|
cursor: pointer; padding: 0 2px;
|
||||||
|
opacity: 0; transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.checklist-item:hover .checklist-remove { opacity: 1; }
|
||||||
|
.checklist-remove:hover { color: var(--danger); }
|
||||||
|
|
||||||
|
.checklist-add {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.checklist-add-input {
|
||||||
|
flex: 1;
|
||||||
|
background: none; border: none; outline: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-body); font-size: 12px;
|
||||||
|
}
|
||||||
|
.checklist-add-input::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* Widget Resize Handle */
|
||||||
|
.widget-resize-handle {
|
||||||
|
position: absolute; bottom: 0; right: 0;
|
||||||
|
width: 14px; height: 14px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
.widget-resize-handle::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute; bottom: 4px; right: 4px;
|
position: absolute; bottom: 4px; right: 4px;
|
||||||
width: 8px; height: 8px;
|
width: 8px; height: 8px;
|
||||||
@@ -968,6 +1055,198 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
border-radius: 0 0 2px 0;
|
border-radius: 0 0 2px 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
WIDGET TOOLBAR
|
||||||
|
============================================ */
|
||||||
|
.widget-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 90;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 6px;
|
||||||
|
background: var(--bg-board);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.toolbar-left .widget-toolbar {
|
||||||
|
right: auto;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
.widget-toolbar-btn {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.widget-toolbar-btn:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
NOTEBOOK SIDEBAR
|
||||||
|
============================================ */
|
||||||
|
.notebook-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 200;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
opacity: 0; pointer-events: none;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
}
|
||||||
|
.notebook-overlay.active {
|
||||||
|
opacity: 1; pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel {
|
||||||
|
position: fixed; top: 0; right: -360px;
|
||||||
|
width: 340px; height: 100vh; z-index: 201;
|
||||||
|
background: rgba(8,8,16,0.96);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.notebook-overlay.active + .notebook-panel,
|
||||||
|
.notebook-panel.open {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.toolbar-left .notebook-panel {
|
||||||
|
right: auto; left: -360px;
|
||||||
|
border-left: none;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.toolbar-left .notebook-overlay.active + .notebook-panel,
|
||||||
|
.toolbar-left .notebook-panel.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.notebook-header-title {
|
||||||
|
font-family: var(--font-display); font-size: 14px; font-weight: 600;
|
||||||
|
letter-spacing: 1.5px; text-transform: uppercase;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.notebook-count {
|
||||||
|
font-size: 11px; color: var(--text-muted);
|
||||||
|
font-weight: 400; letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-slots {
|
||||||
|
flex: 1; overflow-y: auto; padding: 12px;
|
||||||
|
display: flex; flex-direction: column; gap: 10px;
|
||||||
|
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Belegter Slot */
|
||||||
|
.notebook-slot {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.notebook-slot:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
.notebook-slot-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.notebook-slot-title {
|
||||||
|
font-family: var(--font-display); font-size: 12px; font-weight: 600;
|
||||||
|
letter-spacing: 1px; text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
}
|
||||||
|
.notebook-slot-type {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.notebook-slot-preview {
|
||||||
|
font-family: var(--font-body); font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.notebook-slot-actions {
|
||||||
|
display: flex; gap: 6px; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.notebook-slot-btn {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-body); font-size: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.notebook-slot-btn:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.notebook-slot-btn.danger:hover {
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leerer Slot */
|
||||||
|
.notebook-slot-empty {
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 14px 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-body); font-size: 12px;
|
||||||
|
}
|
||||||
|
.notebook-slot-empty:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typ-Auswahl im leeren Slot */
|
||||||
|
.notebook-type-chooser {
|
||||||
|
display: flex; gap: 8px; justify-content: center;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.notebook-type-btn {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-body); font-size: 11px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
}
|
||||||
|
.notebook-type-btn:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
::-webkit-scrollbar { width: 4px; }
|
::-webkit-scrollbar { width: 4px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
@@ -1211,6 +1490,8 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.theme-modal { max-width: 400px; }
|
.theme-modal { max-width: 400px; }
|
||||||
|
|
||||||
.search-bar { max-width: 400px; }
|
.search-bar { max-width: 400px; }
|
||||||
|
.widget-toolbar-btn { width: 32px; height: 32px; }
|
||||||
|
.notebook-panel { width: 320px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smartphone (max 480px) */
|
/* Smartphone (max 480px) */
|
||||||
@@ -1242,7 +1523,12 @@ body.show-desc .bm-desc { display: block; }
|
|||||||
.search-bar { max-width: 100%; }
|
.search-bar { max-width: 100%; }
|
||||||
.search-input { font-size: 13px; padding: 8px 10px; }
|
.search-input { font-size: 13px; padding: 8px 10px; }
|
||||||
|
|
||||||
.sticky-note { width: 200px; bottom: 12px; right: 12px; }
|
.widget { min-width: 180px; }
|
||||||
|
.widget-toolbar { bottom: 12px; top: auto; right: 50%; transform: translateX(50%); flex-direction: row; }
|
||||||
|
.toolbar-left .widget-toolbar { left: 50%; right: auto; transform: translateX(-50%); }
|
||||||
|
.widget-toolbar-btn { width: 32px; height: 32px; }
|
||||||
|
.notebook-panel { width: 100%; right: -100%; }
|
||||||
|
.toolbar-left .notebook-panel { left: -100%; }
|
||||||
|
|
||||||
.modal { width: calc(100vw - 32px); }
|
.modal { width: calc(100vw - 32px); }
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-2
@@ -16,7 +16,8 @@ async function init() {
|
|||||||
bindGlobalEvents();
|
bindGlobalEvents();
|
||||||
bindSettingsEvents();
|
bindSettingsEvents();
|
||||||
initSearch();
|
initSearch();
|
||||||
initStickyNote();
|
await migrateSticky();
|
||||||
|
await Notes.init();
|
||||||
initDataButtons();
|
initDataButtons();
|
||||||
Store.checkQuota();
|
Store.checkQuota();
|
||||||
|
|
||||||
@@ -30,6 +31,46 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- STICKY NOTE MIGRATION ----
|
||||||
|
async function migrateSticky() {
|
||||||
|
const stickyText = await Store.get('stickyNote');
|
||||||
|
const stickyPos = await Store.get('stickyPos');
|
||||||
|
const existingWidgets = await Store.get('widgetStates');
|
||||||
|
|
||||||
|
// Nur migrieren wenn alte Daten vorhanden UND noch keine Widgets existieren
|
||||||
|
if (!stickyText && !stickyPos) return;
|
||||||
|
if (existingWidgets && Array.isArray(existingWidgets.notes) && existingWidgets.notes.length > 0) return;
|
||||||
|
|
||||||
|
const noteData = {
|
||||||
|
id: 'note_' + uid(),
|
||||||
|
title: (stickyText || '').split('\n')[0].trim().slice(0, 20) || 'Note',
|
||||||
|
content: stickyText || '',
|
||||||
|
template: 'text',
|
||||||
|
x: stickyPos ? stickyPos.x : 120,
|
||||||
|
y: stickyPos ? stickyPos.y : 80,
|
||||||
|
width: 280,
|
||||||
|
height: 220,
|
||||||
|
open: true,
|
||||||
|
checkedItems: [],
|
||||||
|
checklistItems: []
|
||||||
|
};
|
||||||
|
|
||||||
|
await Store.set('widgetStates', { notes: [noteData] });
|
||||||
|
|
||||||
|
// Alte Keys aufraeumen
|
||||||
|
try {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||||||
|
chrome.storage.local.remove(['stickyNote', 'stickyPos', 'stickyVisible']);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('stickyNote');
|
||||||
|
localStorage.removeItem('stickyPos');
|
||||||
|
localStorage.removeItem('stickyVisible');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Sticky-Migration: Alte Keys konnten nicht entfernt werden', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- BACKUP REMINDER ----
|
// ---- BACKUP REMINDER ----
|
||||||
const BACKUP_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
|
const BACKUP_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
|
||||||
|
|
||||||
@@ -55,7 +96,9 @@ async function checkBackupReminder() {
|
|||||||
|
|
||||||
if (doBackup) {
|
if (doBackup) {
|
||||||
// JSON-Export auslösen (gleiche Logik wie btnExportJSON)
|
// JSON-Export auslösen (gleiche Logik wie btnExportJSON)
|
||||||
const data = { version: '1.5.2', exported: new Date().toISOString(), boards, settings };
|
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 blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
+34
-4
@@ -9,9 +9,16 @@ function initDataButtons() {
|
|||||||
const jsonInput = document.getElementById('jsonImportInput');
|
const jsonInput = document.getElementById('jsonImportInput');
|
||||||
if (!btnExport || !btnImport) return;
|
if (!btnExport || !btnImport) return;
|
||||||
|
|
||||||
// Export
|
// Export (inkl. Notes)
|
||||||
btnExport.addEventListener('click', () => {
|
btnExport.addEventListener('click', async () => {
|
||||||
const data = { version: '1.5.2', exported: new Date().toISOString(), boards, settings };
|
const widgetData = await Store.get('widgetStates');
|
||||||
|
const data = {
|
||||||
|
version: '1.6.0',
|
||||||
|
exported: new Date().toISOString(),
|
||||||
|
boards,
|
||||||
|
settings,
|
||||||
|
notes: widgetData && Array.isArray(widgetData.notes) ? widgetData.notes : []
|
||||||
|
};
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -50,8 +57,31 @@ function initDataButtons() {
|
|||||||
boards = [...boards, ...validBoards];
|
boards = [...boards, ...validBoards];
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
renderBoards();
|
renderBoards();
|
||||||
|
|
||||||
|
// Notes importieren (falls vorhanden)
|
||||||
|
let notesImported = 0;
|
||||||
|
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 importNotes = data.notes.filter(n => {
|
||||||
|
if (!n || !n.id || !n.template) return false;
|
||||||
|
n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : [];
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// Limit beachten
|
||||||
|
const spaceLeft = Notes.MAX_NOTES - existingNotes.length;
|
||||||
|
const toImport = importNotes.slice(0, spaceLeft);
|
||||||
|
if (toImport.length > 0) {
|
||||||
|
const merged = [...existingNotes, ...toImport];
|
||||||
|
await Store.set('widgetStates', { notes: merged });
|
||||||
|
Notes._notes = merged;
|
||||||
|
notesImported = toImport.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteMsg = notesImported > 0 ? ` + ${notesImported} Note(s)` : '';
|
||||||
await HellionDialog.alert(
|
await HellionDialog.alert(
|
||||||
`${validBoards.length} Board(s) erfolgreich importiert.`,
|
`${validBoards.length} Board(s)${noteMsg} erfolgreich importiert.`,
|
||||||
{ type: 'success', title: 'Import erfolgreich' }
|
{ type: 'success', title: 'Import erfolgreich' }
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+555
@@ -0,0 +1,555 @@
|
|||||||
|
/* =============================================
|
||||||
|
HELLION NEWTAB — notes.js
|
||||||
|
Notes: Freitext, Checklisten, Notebook-Sidebar
|
||||||
|
============================================= */
|
||||||
|
|
||||||
|
const Notes = {
|
||||||
|
MAX_NOTES: 5,
|
||||||
|
MAX_CHARS: 2500,
|
||||||
|
STORAGE_KEY: 'widgetStates',
|
||||||
|
/** @type {Array<Object>} */
|
||||||
|
_notes: [],
|
||||||
|
_saveTimer: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notes aus Storage laden
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
const data = await Store.get(this.STORAGE_KEY);
|
||||||
|
if (data && Array.isArray(data.notes)) {
|
||||||
|
this._notes = data.notes;
|
||||||
|
}
|
||||||
|
return this._notes;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Notes in Storage speichern
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
// Widget-States mit Note-Daten mergen
|
||||||
|
const widgetStates = WidgetManager.save ? await WidgetManager.save() : [];
|
||||||
|
|
||||||
|
// Note-Daten mit aktuellen Widget-Positionen mergen
|
||||||
|
const merged = this._notes.map(note => {
|
||||||
|
const ws = widgetStates.find(w => w.id === note.id);
|
||||||
|
if (ws) {
|
||||||
|
note.x = ws.x;
|
||||||
|
note.y = ws.y;
|
||||||
|
note.width = ws.width;
|
||||||
|
note.height = ws.height;
|
||||||
|
note.open = ws.open;
|
||||||
|
note.title = ws.title;
|
||||||
|
}
|
||||||
|
return note;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Store.set(this.STORAGE_KEY, { notes: merged });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced Save (fuer Auto-Save bei Input)
|
||||||
|
*/
|
||||||
|
_debouncedSave() {
|
||||||
|
clearTimeout(this._saveTimer);
|
||||||
|
this._saveTimer = setTimeout(() => this.save(), 500);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neue Note erstellen
|
||||||
|
* @param {'text'|'checklist'} template
|
||||||
|
* @returns {Promise<string|null>} widget-id oder null bei vollem Limit
|
||||||
|
*/
|
||||||
|
async create(template) {
|
||||||
|
if (this._notes.length >= this.MAX_NOTES) {
|
||||||
|
await HellionDialog.alert(
|
||||||
|
'Maximale Anzahl erreicht! Du kannst maximal ' + this.MAX_NOTES + ' Notes gleichzeitig haben. Lösche eine bestehende Note um eine neue zu erstellen.',
|
||||||
|
{ type: 'warning', title: 'Limit erreicht' }
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteData = {
|
||||||
|
id: 'note_' + uid(),
|
||||||
|
title: template === 'checklist' ? 'Checkliste' : 'Note',
|
||||||
|
content: '',
|
||||||
|
template: template,
|
||||||
|
x: 120 + (this._notes.length * 30),
|
||||||
|
y: 80 + (this._notes.length * 30),
|
||||||
|
width: 280,
|
||||||
|
height: 220,
|
||||||
|
open: true,
|
||||||
|
checkedItems: [],
|
||||||
|
checklistItems: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this._notes.push(noteData);
|
||||||
|
|
||||||
|
// Widget erstellen
|
||||||
|
const widgetId = WidgetManager.create('note', {
|
||||||
|
id: noteData.id,
|
||||||
|
title: noteData.title,
|
||||||
|
x: noteData.x,
|
||||||
|
y: noteData.y,
|
||||||
|
width: noteData.width,
|
||||||
|
height: noteData.height,
|
||||||
|
open: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body rendern
|
||||||
|
const body = WidgetManager.getBody(widgetId);
|
||||||
|
if (body) this.renderBody(noteData, body);
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
return widgetId;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note-Body rendern (in Widget-Body einfuegen)
|
||||||
|
* @param {Object} noteData
|
||||||
|
* @param {HTMLElement} bodyEl
|
||||||
|
*/
|
||||||
|
renderBody(noteData, bodyEl) {
|
||||||
|
bodyEl.textContent = '';
|
||||||
|
if (noteData.template === 'checklist') {
|
||||||
|
this._renderChecklistBody(noteData, bodyEl);
|
||||||
|
} else {
|
||||||
|
this._renderTextBody(noteData, bodyEl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freitext-Body: Textarea mit Zeichenzaehler
|
||||||
|
* @param {Object} noteData
|
||||||
|
* @param {HTMLElement} bodyEl
|
||||||
|
*/
|
||||||
|
_renderTextBody(noteData, bodyEl) {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'widget-textarea';
|
||||||
|
textarea.placeholder = 'Notiz schreiben...';
|
||||||
|
textarea.spellcheck = false;
|
||||||
|
textarea.value = noteData.content || '';
|
||||||
|
textarea.maxLength = this.MAX_CHARS;
|
||||||
|
|
||||||
|
const counter = document.createElement('span');
|
||||||
|
counter.className = 'widget-char-count';
|
||||||
|
counter.textContent = (noteData.content || '').length + ' / ' + this.MAX_CHARS;
|
||||||
|
|
||||||
|
textarea.addEventListener('input', () => {
|
||||||
|
noteData.content = textarea.value;
|
||||||
|
const len = textarea.value.length;
|
||||||
|
counter.textContent = len + ' / ' + this.MAX_CHARS;
|
||||||
|
counter.classList.toggle('limit', len >= this.MAX_CHARS);
|
||||||
|
|
||||||
|
// Auto-Titel aus erster Zeile
|
||||||
|
const firstLine = textarea.value.split('\n')[0].trim().slice(0, 20);
|
||||||
|
if (firstLine) {
|
||||||
|
noteData.title = firstLine;
|
||||||
|
const widgetEntry = WidgetManager._widgets.get(noteData.id);
|
||||||
|
if (widgetEntry) {
|
||||||
|
const titleEl = widgetEntry.el.querySelector('.widget-title');
|
||||||
|
if (titleEl && titleEl.contentEditable !== 'true') {
|
||||||
|
titleEl.textContent = firstLine;
|
||||||
|
}
|
||||||
|
widgetEntry.state.title = firstLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._debouncedSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
bodyEl.append(textarea, counter);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklisten-Body: Items mit Checkboxen
|
||||||
|
* @param {Object} noteData
|
||||||
|
* @param {HTMLElement} bodyEl
|
||||||
|
*/
|
||||||
|
_renderChecklistBody(noteData, bodyEl) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'widget-checklist';
|
||||||
|
|
||||||
|
// Bestehende Items rendern
|
||||||
|
if (!Array.isArray(noteData.checklistItems)) {
|
||||||
|
noteData.checklistItems = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItems = () => {
|
||||||
|
list.textContent = '';
|
||||||
|
noteData.checklistItems.forEach((item, idx) => {
|
||||||
|
const li = this._createChecklistItem(noteData, item, idx, renderItems);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderItems();
|
||||||
|
|
||||||
|
// Eingabefeld fuer neue Items
|
||||||
|
const addRow = document.createElement('div');
|
||||||
|
addRow.className = 'checklist-add';
|
||||||
|
|
||||||
|
const addInput = document.createElement('input');
|
||||||
|
addInput.className = 'checklist-add-input';
|
||||||
|
addInput.type = 'text';
|
||||||
|
addInput.placeholder = 'Neues Item...';
|
||||||
|
addInput.maxLength = 100;
|
||||||
|
|
||||||
|
addInput.addEventListener('keydown', async (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const text = addInput.value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
noteData.checklistItems.push({ text, checked: false });
|
||||||
|
addInput.value = '';
|
||||||
|
renderItems();
|
||||||
|
this._updateChecklistContent(noteData);
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addRow.appendChild(addInput);
|
||||||
|
bodyEl.append(list, addRow);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelnes Checklisten-Item erstellen
|
||||||
|
* @param {Object} noteData
|
||||||
|
* @param {Object} item - { text, checked }
|
||||||
|
* @param {number} idx
|
||||||
|
* @param {Function} rerenderFn
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
_createChecklistItem(noteData, item, idx, rerenderFn) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'checklist-item' + (item.checked ? ' checked' : '');
|
||||||
|
|
||||||
|
const checkbox = document.createElement('span');
|
||||||
|
checkbox.className = 'checklist-checkbox';
|
||||||
|
checkbox.textContent = item.checked ? '\u2713' : '';
|
||||||
|
checkbox.addEventListener('click', async () => {
|
||||||
|
item.checked = !item.checked;
|
||||||
|
li.classList.toggle('checked', item.checked);
|
||||||
|
checkbox.textContent = item.checked ? '\u2713' : '';
|
||||||
|
this._updateChecklistContent(noteData);
|
||||||
|
await this.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.className = 'checklist-text';
|
||||||
|
text.textContent = item.text;
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'checklist-remove';
|
||||||
|
removeBtn.textContent = '\u2715';
|
||||||
|
removeBtn.addEventListener('click', async () => {
|
||||||
|
noteData.checklistItems.splice(idx, 1);
|
||||||
|
rerenderFn();
|
||||||
|
this._updateChecklistContent(noteData);
|
||||||
|
await this.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
li.append(checkbox, text, removeBtn);
|
||||||
|
return li;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checklisten-Content fuer Export/Vorschau aktualisieren
|
||||||
|
* @param {Object} noteData
|
||||||
|
*/
|
||||||
|
_updateChecklistContent(noteData) {
|
||||||
|
const total = noteData.checklistItems.length;
|
||||||
|
const done = noteData.checklistItems.filter(i => i.checked).length;
|
||||||
|
noteData.content = noteData.checklistItems.map(i => (i.checked ? '[x] ' : '[ ] ') + i.text).join('\n');
|
||||||
|
|
||||||
|
// Auto-Titel: "X/Y erledigt" falls kein manueller Titel
|
||||||
|
const widgetEntry = WidgetManager._widgets.get(noteData.id);
|
||||||
|
if (widgetEntry) {
|
||||||
|
const defaultTitle = done + '/' + total + ' erledigt';
|
||||||
|
const titleEl = widgetEntry.el.querySelector('.widget-title');
|
||||||
|
if (titleEl && titleEl.contentEditable !== 'true') {
|
||||||
|
// Nur wenn Titel noch Standard ist
|
||||||
|
if (noteData.title === 'Checkliste' || /^\d+\/\d+ erledigt$/.test(noteData.title)) {
|
||||||
|
noteData.title = defaultTitle;
|
||||||
|
titleEl.textContent = defaultTitle;
|
||||||
|
widgetEntry.state.title = defaultTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note anhand ID finden
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
getNote(id) {
|
||||||
|
return this._notes.find(n => n.id === id) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note loeschen
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
async deleteNote(id) {
|
||||||
|
const idx = this._notes.findIndex(n => n.id === id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
const ok = await HellionDialog.confirm(
|
||||||
|
'Note endgültig löschen? Das kann nicht rückgängig gemacht werden.',
|
||||||
|
{ type: 'danger', title: 'Note löschen', confirmText: 'Löschen' }
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
this._notes.splice(idx, 1);
|
||||||
|
WidgetManager.close(id);
|
||||||
|
await this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note als .md exportieren
|
||||||
|
* @param {Object} noteData
|
||||||
|
*/
|
||||||
|
exportNote(noteData) {
|
||||||
|
let md = '# ' + noteData.title + '\n\n';
|
||||||
|
if (noteData.template === 'checklist') {
|
||||||
|
noteData.checklistItems.forEach(item => {
|
||||||
|
md += (item.checked ? '- [x] ' : '- [ ] ') + item.text + '\n';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
md += noteData.content || '';
|
||||||
|
}
|
||||||
|
md += '\n\n---\n*Exportiert aus Hellion Dashboard*\n';
|
||||||
|
|
||||||
|
const blob = new Blob([md], { type: 'text/markdown' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = (noteData.title || 'note').replace(/[^a-zA-Z0-9äöüÄÖÜß_-]/g, '_') + '.md';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- NOTEBOOK SIDEBAR ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notebook-Sidebar oeffnen
|
||||||
|
*/
|
||||||
|
openNotebook() {
|
||||||
|
const overlay = document.getElementById('notebookOverlay');
|
||||||
|
const panel = document.getElementById('notebookPanel');
|
||||||
|
if (overlay) overlay.classList.add('active');
|
||||||
|
if (panel) panel.classList.add('open');
|
||||||
|
this._renderNotebookSlots();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notebook-Sidebar schliessen
|
||||||
|
*/
|
||||||
|
closeNotebook() {
|
||||||
|
const overlay = document.getElementById('notebookOverlay');
|
||||||
|
const panel = document.getElementById('notebookPanel');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
if (panel) panel.classList.remove('open');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notebook-Slots rendern
|
||||||
|
*/
|
||||||
|
_renderNotebookSlots() {
|
||||||
|
const container = document.getElementById('notebookSlots');
|
||||||
|
const countEl = document.getElementById('notebookCount');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.textContent = '';
|
||||||
|
if (countEl) countEl.textContent = this._notes.length + ' / ' + this.MAX_NOTES;
|
||||||
|
|
||||||
|
// Belegte Slots
|
||||||
|
this._notes.forEach(note => {
|
||||||
|
const slot = this._createNotebookSlot(note);
|
||||||
|
container.appendChild(slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leere Slots
|
||||||
|
const remaining = this.MAX_NOTES - this._notes.length;
|
||||||
|
for (let i = 0; i < remaining; i++) {
|
||||||
|
const emptySlot = this._createEmptySlot();
|
||||||
|
container.appendChild(emptySlot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Belegten Notebook-Slot erstellen
|
||||||
|
* @param {Object} note
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
_createNotebookSlot(note) {
|
||||||
|
const slot = document.createElement('div');
|
||||||
|
slot.className = 'notebook-slot';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'notebook-slot-header';
|
||||||
|
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.className = 'notebook-slot-title';
|
||||||
|
|
||||||
|
const typeIcon = document.createElement('span');
|
||||||
|
typeIcon.className = 'notebook-slot-type';
|
||||||
|
typeIcon.textContent = note.template === 'checklist' ? '\u2611' : '\u270E';
|
||||||
|
title.append(typeIcon);
|
||||||
|
title.append(document.createTextNode(' ' + note.title));
|
||||||
|
|
||||||
|
header.appendChild(title);
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
const preview = document.createElement('div');
|
||||||
|
preview.className = 'notebook-slot-preview';
|
||||||
|
if (note.template === 'checklist') {
|
||||||
|
const total = note.checklistItems ? note.checklistItems.length : 0;
|
||||||
|
const done = note.checklistItems ? note.checklistItems.filter(i => i.checked).length : 0;
|
||||||
|
preview.textContent = done + '/' + total + ' erledigt';
|
||||||
|
} else {
|
||||||
|
preview.textContent = (note.content || '').slice(0, 50) || 'Leer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'notebook-slot-actions';
|
||||||
|
|
||||||
|
const btnExport = document.createElement('button');
|
||||||
|
btnExport.className = 'notebook-slot-btn';
|
||||||
|
btnExport.textContent = 'Export';
|
||||||
|
btnExport.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.exportNote(note);
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnDelete = document.createElement('button');
|
||||||
|
btnDelete.className = 'notebook-slot-btn danger';
|
||||||
|
btnDelete.textContent = '\uD83D\uDDD1';
|
||||||
|
btnDelete.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await this.deleteNote(note.id);
|
||||||
|
this._renderNotebookSlots();
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.append(btnExport, btnDelete);
|
||||||
|
slot.append(header, preview, actions);
|
||||||
|
|
||||||
|
// Klick oeffnet Note als Widget
|
||||||
|
slot.addEventListener('click', async () => {
|
||||||
|
if (WidgetManager.isOpen(note.id)) {
|
||||||
|
WidgetManager.bringToFront(note.id);
|
||||||
|
} else {
|
||||||
|
await WidgetManager.openWidget(note.id);
|
||||||
|
}
|
||||||
|
this.closeNotebook();
|
||||||
|
});
|
||||||
|
|
||||||
|
return slot;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leeren Notebook-Slot erstellen
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
_createEmptySlot() {
|
||||||
|
const slot = document.createElement('div');
|
||||||
|
slot.className = 'notebook-slot-empty';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = '+ Note erstellen';
|
||||||
|
slot.appendChild(label);
|
||||||
|
|
||||||
|
// Klick zeigt Typ-Auswahl
|
||||||
|
let chooserOpen = false;
|
||||||
|
slot.addEventListener('click', () => {
|
||||||
|
if (chooserOpen) return;
|
||||||
|
chooserOpen = true;
|
||||||
|
label.style.display = 'none';
|
||||||
|
|
||||||
|
const chooser = document.createElement('div');
|
||||||
|
chooser.className = 'notebook-type-chooser';
|
||||||
|
|
||||||
|
const btnText = document.createElement('button');
|
||||||
|
btnText.className = 'notebook-type-btn';
|
||||||
|
btnText.textContent = '\u270E Freitext';
|
||||||
|
btnText.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await this.create('text');
|
||||||
|
this._renderNotebookSlots();
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnCheck = document.createElement('button');
|
||||||
|
btnCheck.className = 'notebook-type-btn';
|
||||||
|
btnCheck.textContent = '\u2611 Checkliste';
|
||||||
|
btnCheck.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await this.create('checklist');
|
||||||
|
this._renderNotebookSlots();
|
||||||
|
});
|
||||||
|
|
||||||
|
chooser.append(btnText, btnCheck);
|
||||||
|
slot.appendChild(chooser);
|
||||||
|
});
|
||||||
|
|
||||||
|
return slot;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- TOOLBAR EVENTS ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget-Toolbar initialisieren
|
||||||
|
*/
|
||||||
|
initToolbar() {
|
||||||
|
const toolbar = document.getElementById('widgetToolbar');
|
||||||
|
if (!toolbar) return;
|
||||||
|
|
||||||
|
toolbar.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('.widget-toolbar-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
if (action === 'new-note') {
|
||||||
|
await this.create('text');
|
||||||
|
} else if (action === 'new-checklist') {
|
||||||
|
await this.create('checklist');
|
||||||
|
} else if (action === 'notebook') {
|
||||||
|
this.openNotebook();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---- INIT ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notes-System initialisieren (ersetzt initStickyNote)
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
await this.load();
|
||||||
|
|
||||||
|
// Widgets wiederherstellen
|
||||||
|
await WidgetManager.restore((noteData, bodyEl) => {
|
||||||
|
this.renderBody(noteData, bodyEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toolbar initialisieren
|
||||||
|
this.initToolbar();
|
||||||
|
|
||||||
|
// Notebook-Sidebar Events
|
||||||
|
const notebookOverlay = document.getElementById('notebookOverlay');
|
||||||
|
if (notebookOverlay) {
|
||||||
|
notebookOverlay.addEventListener('click', () => this.closeNotebook());
|
||||||
|
}
|
||||||
|
const btnCloseNotebook = document.getElementById('btnCloseNotebook');
|
||||||
|
if (btnCloseNotebook) {
|
||||||
|
btnCloseNotebook.addEventListener('click', () => this.closeNotebook());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header btnNote oeffnet Notebook
|
||||||
|
const btnNote = document.getElementById('btnNote');
|
||||||
|
if (btnNote) {
|
||||||
|
btnNote.addEventListener('click', () => this.openNotebook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -33,7 +33,8 @@ const Onboarding = {
|
|||||||
title: 'Weitere Features',
|
title: 'Weitere Features',
|
||||||
features: [
|
features: [
|
||||||
'Suchleiste mit Google, DuckDuckGo oder Bing',
|
'Suchleiste mit Google, DuckDuckGo oder Bing',
|
||||||
'Sticky Notes f\u00FCr schnelle Notizen',
|
'Widget-Toolbar rechts \u2014 Notes und Checklisten erstellen',
|
||||||
|
'Notebook-Sidebar \u00FCber den \u201ENote\u201C Button oder die Toolbar',
|
||||||
'Funktioniert komplett offline \u2014 alles lokal gespeichert'
|
'Funktioniert komplett offline \u2014 alles lokal gespeichert'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+18
-2
@@ -25,7 +25,7 @@ function closeThemeModal() {
|
|||||||
|
|
||||||
// ---- ACCORDION ----
|
// ---- ACCORDION ----
|
||||||
function initAccordion() {
|
function initAccordion() {
|
||||||
const defaultOpen = new Set(['appearance', 'behavior', 'data', 'help']);
|
const defaultOpen = new Set(['appearance', 'behavior', 'widgets', 'data', 'help']);
|
||||||
const sections = document.querySelectorAll('.settings-section[data-section]');
|
const sections = document.querySelectorAll('.settings-section[data-section]');
|
||||||
|
|
||||||
sections.forEach(section => {
|
sections.forEach(section => {
|
||||||
@@ -71,6 +71,11 @@ function applySettings() {
|
|||||||
const showSearchEl = document.getElementById('settingShowSearch');
|
const showSearchEl = document.getElementById('settingShowSearch');
|
||||||
if (showSearchEl) showSearchEl.checked = settings.showSearch;
|
if (showSearchEl) showSearchEl.checked = settings.showSearch;
|
||||||
|
|
||||||
|
// Toolbar-Position
|
||||||
|
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
|
||||||
|
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
||||||
|
if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right';
|
||||||
|
|
||||||
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
|
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
|
||||||
|
|
||||||
if (settings.bgUrl) {
|
if (settings.bgUrl) {
|
||||||
@@ -172,6 +177,17 @@ function bindSettingsEvents() {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toolbar-Position Setting
|
||||||
|
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
||||||
|
if (toolbarPosEl) {
|
||||||
|
toolbarPosEl.value = settings.toolbarPos || 'right';
|
||||||
|
toolbarPosEl.addEventListener('change', async (e) => {
|
||||||
|
settings.toolbarPos = e.target.value;
|
||||||
|
document.body.classList.toggle('toolbar-left', e.target.value === 'left');
|
||||||
|
await saveSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Onboarding wiederholen
|
// Onboarding wiederholen
|
||||||
document.getElementById('btnRestartOnboarding').addEventListener('click', () => {
|
document.getElementById('btnRestartOnboarding').addEventListener('click', () => {
|
||||||
closeSettings();
|
closeSettings();
|
||||||
@@ -188,7 +204,7 @@ function bindSettingsEvents() {
|
|||||||
boards = [];
|
boards = [];
|
||||||
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
||||||
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
||||||
showSearch: true, searchEngine: 'google' };
|
showSearch: true, searchEngine: 'google', toolbarPos: 'right' };
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
applySettings();
|
applySettings();
|
||||||
|
|||||||
+2
-1
@@ -15,7 +15,8 @@ let settings = {
|
|||||||
bgUrl: '',
|
bgUrl: '',
|
||||||
theme: 'nebula',
|
theme: 'nebula',
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
searchEngine: 'google'
|
searchEngine: 'google',
|
||||||
|
toolbarPos: 'right'
|
||||||
};
|
};
|
||||||
|
|
||||||
function uid() {
|
function uid() {
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
/* =============================================
|
||||||
|
HELLION NEWTAB — widgets.js
|
||||||
|
Widget-Manager: Registry, Drag, Resize, Z-Index, Persistierung
|
||||||
|
============================================= */
|
||||||
|
|
||||||
|
const WidgetManager = {
|
||||||
|
/** @type {Map<string, {el: HTMLElement, type: string, state: Object}>} */
|
||||||
|
_widgets: new Map(),
|
||||||
|
_topZ: 51,
|
||||||
|
STORAGE_KEY: 'widgetStates',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget erstellen und in DOM einfuegen
|
||||||
|
* @param {string} type - 'note'
|
||||||
|
* @param {Object} config - { id, title, x, y, width, height, open }
|
||||||
|
* @returns {string} widget-id
|
||||||
|
*/
|
||||||
|
create(type, config) {
|
||||||
|
const id = config.id || ('widget_' + uid());
|
||||||
|
const state = {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
title: config.title || 'Note',
|
||||||
|
x: config.x || 120,
|
||||||
|
y: config.y || 80,
|
||||||
|
width: config.width || 280,
|
||||||
|
height: config.height || 220,
|
||||||
|
open: config.open !== false
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = this._buildDOM(state);
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
this._widgets.set(id, { el, type, state });
|
||||||
|
this._initDrag(el);
|
||||||
|
this._initResize(el);
|
||||||
|
this.bringToFront(id);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget-DOM erzeugen (createElement, kein innerHTML)
|
||||||
|
* @param {Object} state
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
_buildDOM(state) {
|
||||||
|
const widget = document.createElement('div');
|
||||||
|
widget.className = 'widget';
|
||||||
|
widget.dataset.widgetId = state.id;
|
||||||
|
widget.style.left = state.x + 'px';
|
||||||
|
widget.style.top = state.y + 'px';
|
||||||
|
widget.style.width = state.width + 'px';
|
||||||
|
widget.style.height = state.height + 'px';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'widget-header';
|
||||||
|
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.className = 'widget-title';
|
||||||
|
title.textContent = state.title;
|
||||||
|
|
||||||
|
// Doppelklick auf Titel zum Editieren
|
||||||
|
title.addEventListener('dblclick', () => {
|
||||||
|
title.contentEditable = 'true';
|
||||||
|
title.focus();
|
||||||
|
// Text selektieren
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(title);
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
});
|
||||||
|
title.addEventListener('blur', async () => {
|
||||||
|
title.contentEditable = 'false';
|
||||||
|
const newTitle = title.textContent.trim().slice(0, 20);
|
||||||
|
title.textContent = newTitle || 'Note';
|
||||||
|
const entry = this._widgets.get(state.id);
|
||||||
|
if (entry) {
|
||||||
|
entry.state.title = title.textContent;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
title.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
title.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'widget-actions';
|
||||||
|
|
||||||
|
const btnMin = document.createElement('button');
|
||||||
|
btnMin.className = 'widget-btn widget-minimize';
|
||||||
|
btnMin.title = 'Minimieren';
|
||||||
|
btnMin.textContent = '\u2500';
|
||||||
|
btnMin.addEventListener('click', () => this.minimize(state.id));
|
||||||
|
|
||||||
|
const btnClose = document.createElement('button');
|
||||||
|
btnClose.className = 'widget-btn widget-close';
|
||||||
|
btnClose.title = 'Schließen';
|
||||||
|
btnClose.textContent = '\u2715';
|
||||||
|
btnClose.addEventListener('click', () => this.close(state.id));
|
||||||
|
|
||||||
|
actions.append(btnMin, btnClose);
|
||||||
|
header.append(title, actions);
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'widget-body';
|
||||||
|
|
||||||
|
// Resize Handle
|
||||||
|
const resizeHandle = document.createElement('div');
|
||||||
|
resizeHandle.className = 'widget-resize-handle';
|
||||||
|
|
||||||
|
widget.append(header, body, resizeHandle);
|
||||||
|
|
||||||
|
// Klick auf Widget bringt es nach vorne
|
||||||
|
widget.addEventListener('pointerdown', () => {
|
||||||
|
this.bringToFront(state.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget-Body-Element holen
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {HTMLElement|null}
|
||||||
|
*/
|
||||||
|
getBody(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (!entry) return null;
|
||||||
|
return entry.el.querySelector('.widget-body');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget entfernen (endgueltig loeschen)
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
close(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
entry.el.remove();
|
||||||
|
this._widgets.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget minimieren (aus DOM verstecken, bleibt im Notebook)
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
async minimize(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
entry.state.open = false;
|
||||||
|
entry.el.classList.add('widget-minimized');
|
||||||
|
setTimeout(() => {
|
||||||
|
entry.el.style.display = 'none';
|
||||||
|
}, 250);
|
||||||
|
await this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget oeffnen (aus minimiertem Zustand wiederherstellen)
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
async openWidget(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
entry.state.open = true;
|
||||||
|
entry.el.style.display = 'flex';
|
||||||
|
// Naechster Frame fuer Animation
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
entry.el.classList.remove('widget-minimized');
|
||||||
|
});
|
||||||
|
this.bringToFront(id);
|
||||||
|
await this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget in den Vordergrund bringen
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
bringToFront(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (!entry) return;
|
||||||
|
this._topZ++;
|
||||||
|
entry.el.style.zIndex = this._topZ;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag initialisieren (Pointer Events auf Header)
|
||||||
|
* @param {HTMLElement} widgetEl
|
||||||
|
*/
|
||||||
|
_initDrag(widgetEl) {
|
||||||
|
const header = widgetEl.querySelector('.widget-header');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
header.addEventListener('pointerdown', function onDown(e) {
|
||||||
|
if (e.target.closest('.widget-btn') || e.target.closest('.widget-title[contenteditable="true"]')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
header.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
const rect = widgetEl.getBoundingClientRect();
|
||||||
|
const offX = e.clientX - rect.left;
|
||||||
|
const offY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
function onMove(ev) {
|
||||||
|
const maxX = window.innerWidth - widgetEl.offsetWidth;
|
||||||
|
const maxY = window.innerHeight - widgetEl.offsetHeight;
|
||||||
|
widgetEl.style.left = Math.max(0, Math.min(maxX, ev.clientX - offX)) + 'px';
|
||||||
|
widgetEl.style.top = Math.max(48, Math.min(maxY, ev.clientY - offY)) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUp() {
|
||||||
|
header.releasePointerCapture(e.pointerId);
|
||||||
|
header.removeEventListener('pointermove', onMove);
|
||||||
|
header.removeEventListener('pointerup', onUp);
|
||||||
|
// State aktualisieren
|
||||||
|
const id = widgetEl.dataset.widgetId;
|
||||||
|
const entry = self._widgets.get(id);
|
||||||
|
if (entry) {
|
||||||
|
entry.state.x = parseFloat(widgetEl.style.left);
|
||||||
|
entry.state.y = parseFloat(widgetEl.style.top);
|
||||||
|
await self.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header.addEventListener('pointermove', onMove);
|
||||||
|
header.addEventListener('pointerup', onUp);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize initialisieren (Pointer Events auf Handle)
|
||||||
|
* @param {HTMLElement} widgetEl
|
||||||
|
*/
|
||||||
|
_initResize(widgetEl) {
|
||||||
|
const handle = widgetEl.querySelector('.widget-resize-handle');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
handle.addEventListener('pointerdown', function onDown(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handle.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
const startW = widgetEl.offsetWidth;
|
||||||
|
const startH = widgetEl.offsetHeight;
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startY = e.clientY;
|
||||||
|
|
||||||
|
function onMove(ev) {
|
||||||
|
widgetEl.style.width = Math.max(200, startW + (ev.clientX - startX)) + 'px';
|
||||||
|
widgetEl.style.height = Math.max(150, startH + (ev.clientY - startY)) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUp() {
|
||||||
|
handle.releasePointerCapture(e.pointerId);
|
||||||
|
handle.removeEventListener('pointermove', onMove);
|
||||||
|
handle.removeEventListener('pointerup', onUp);
|
||||||
|
const id = widgetEl.dataset.widgetId;
|
||||||
|
const entry = self._widgets.get(id);
|
||||||
|
if (entry) {
|
||||||
|
entry.state.width = widgetEl.offsetWidth;
|
||||||
|
entry.state.height = widgetEl.offsetHeight;
|
||||||
|
await self.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.addEventListener('pointermove', onMove);
|
||||||
|
handle.addEventListener('pointerup', onUp);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Widget-States aus Storage laden und wiederherstellen
|
||||||
|
* @param {Function} renderCallback - Funktion die den Body rendert (noteData, bodyEl)
|
||||||
|
*/
|
||||||
|
async restore(renderCallback) {
|
||||||
|
const data = await Store.get(this.STORAGE_KEY);
|
||||||
|
if (!data || !Array.isArray(data.notes)) return;
|
||||||
|
|
||||||
|
for (const noteData of data.notes) {
|
||||||
|
const id = this.create('note', {
|
||||||
|
id: noteData.id,
|
||||||
|
title: noteData.title,
|
||||||
|
x: noteData.x,
|
||||||
|
y: noteData.y,
|
||||||
|
width: noteData.width,
|
||||||
|
height: noteData.height,
|
||||||
|
open: noteData.open
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body rendern lassen (von Notes-Modul)
|
||||||
|
if (renderCallback) {
|
||||||
|
const body = this.getBody(id);
|
||||||
|
if (body) renderCallback(noteData, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls minimiert, sofort verstecken
|
||||||
|
if (!noteData.open) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
if (entry) {
|
||||||
|
entry.el.classList.add('widget-minimized');
|
||||||
|
entry.el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Widget-States speichern
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
const notes = [];
|
||||||
|
for (const [id, entry] of this._widgets) {
|
||||||
|
if (entry.type === 'note') {
|
||||||
|
notes.push({
|
||||||
|
...entry.state,
|
||||||
|
// Zusaetzliche Note-Daten werden von Notes.save() ergaenzt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nicht direkt speichern — Notes-Modul merged die Daten
|
||||||
|
return notes;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget-State fuer eine bestimmte ID holen
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
getState(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
return entry ? entry.state : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pruefen ob Widget offen ist
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isOpen(id) {
|
||||||
|
const entry = this._widgets.get(id);
|
||||||
|
return entry ? entry.state.open : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzahl aller Widgets
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
count() {
|
||||||
|
return this._widgets.size;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Widget-IDs
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
getAllIds() {
|
||||||
|
return Array.from(this._widgets.keys());
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user