@a83/orbiter-admin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/package.json +33 -0
- package/public/admin-utils.js +302 -0
- package/public/build.html +129 -0
- package/public/collections.html +100 -0
- package/public/dashboard.html +478 -0
- package/public/editor.html +1569 -0
- package/public/entries.html +367 -0
- package/public/favicon.svg +6 -0
- package/public/import.html +514 -0
- package/public/login.html +76 -0
- package/public/media.html +233 -0
- package/public/router.js +142 -0
- package/public/schema.html +366 -0
- package/public/search.js +209 -0
- package/public/settings.html +688 -0
- package/public/sidebar.js +90 -0
- package/public/style.css +1020 -0
- package/public/theme.js +63 -0
- package/public/users.html +192 -0
- package/src/index.js +4 -0
- package/src/middleware/auth.js +20 -0
- package/src/routes/account.js +41 -0
- package/src/routes/auth.js +55 -0
- package/src/routes/build.js +25 -0
- package/src/routes/collections.js +65 -0
- package/src/routes/entries.js +103 -0
- package/src/routes/github.js +133 -0
- package/src/routes/import.js +120 -0
- package/src/routes/info.js +19 -0
- package/src/routes/media.js +95 -0
- package/src/routes/meta.js +54 -0
- package/src/routes/search.js +62 -0
- package/src/routes/users.js +46 -0
- package/src/server.js +85 -0
- package/src/wp-importer.js +299 -0
|
@@ -0,0 +1,1569 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Orbiter Admin — Editor</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Space+Grotesk:wght@300;400;500;600&family=Noto+Serif+JP:wght@200;300&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/style.css" />
|
|
11
|
+
<script src="/theme.js"></script>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
|
13
|
+
<style>
|
|
14
|
+
html,body,.app { height:100%; overflow:hidden; }
|
|
15
|
+
.app { display:flex; flex-direction:column; }
|
|
16
|
+
.editor-shell { display:grid; grid-template-columns:1fr 280px; flex:1; overflow:hidden; }
|
|
17
|
+
.editor-shell.mode-split { grid-template-columns:1fr 1fr 280px; }
|
|
18
|
+
.editor-shell.mode-preview { grid-template-columns:0 1fr 280px; }
|
|
19
|
+
|
|
20
|
+
/* Main writing area */
|
|
21
|
+
.editor-main { display:flex; flex-direction:column; overflow:hidden; background:var(--bg0); }
|
|
22
|
+
|
|
23
|
+
/* Toolbar */
|
|
24
|
+
.editor-toolbar { display:flex; align-items:center; gap:1px; padding:0 20px; height:40px; border-bottom:1px solid var(--line); background:var(--bg2); flex-shrink:0; }
|
|
25
|
+
.tool-group { display:flex; align-items:center; gap:3px; }
|
|
26
|
+
.tool-btn { height:26px; min-width:26px; padding:0 7px; display:flex; align-items:center; justify-content:center; background:transparent; border:1px solid var(--line); color:var(--muted); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:4px; }
|
|
27
|
+
.tool-btn:hover { color:var(--heading); background:var(--bg3); border-color:var(--mid); }
|
|
28
|
+
.tool-btn.active { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
|
|
29
|
+
.tool-sep { width:1px; height:16px; background:var(--line); margin:0 6px; flex-shrink:0; }
|
|
30
|
+
.view-toggle { display:flex; gap:3px; margin-left:0; }
|
|
31
|
+
.view-btn { height:24px; padding:0 10px; border:1px solid var(--line); border-radius:4px; background:transparent; color:var(--muted); font-family:var(--mono); font-size:9px; letter-spacing:.06em; cursor:pointer; transition:all .12s; }
|
|
32
|
+
.view-btn:hover { color:var(--text); border-color:var(--mid); }
|
|
33
|
+
.view-btn.active { background:var(--gold); color:var(--bg0); border-color:var(--gold); }
|
|
34
|
+
|
|
35
|
+
/* Writing page */
|
|
36
|
+
.editor-scroll { flex:1; overflow-y:auto; }
|
|
37
|
+
.editor-page { max-width:680px; margin:0 auto; padding:48px 40px 120px; }
|
|
38
|
+
.editor-title-input { width:100%; background:transparent; border:none; outline:none; font-family:var(--serif); font-weight:200; font-size:32px; color:var(--heading); letter-spacing:.02em; line-height:1.2; margin-bottom:8px; resize:none; overflow:hidden; }
|
|
39
|
+
.editor-title-input::placeholder { color:var(--line); }
|
|
40
|
+
.editor-slug-line { font-size:10px; color:var(--muted); margin-bottom:32px; padding-bottom:14px; border-bottom:1px solid var(--line); }
|
|
41
|
+
.editor-slug-line span { color:var(--accent); }
|
|
42
|
+
.word-count { font-size:10px; color:var(--muted); margin-top:28px; padding-top:12px; border-top:1px solid var(--line2); }
|
|
43
|
+
|
|
44
|
+
/* Block editor */
|
|
45
|
+
.be-editor { min-height:400px; cursor:text; }
|
|
46
|
+
.be-block { outline:none; font-family:var(--serif); font-weight:300; font-size:16px; color:var(--text); line-height:2; letter-spacing:.01em; padding:1px 0; min-height:1.6em; position:relative; word-break:break-word; }
|
|
47
|
+
.be-block:focus { outline:none; }
|
|
48
|
+
.be-block:empty::before { content:attr(data-ph); color:var(--line); pointer-events:none; position:absolute; }
|
|
49
|
+
.be-block[data-type="h1"] { font-size:26px; color:var(--heading); line-height:1.3; padding:4px 0 2px; font-family:var(--serif); font-weight:200; }
|
|
50
|
+
.be-block[data-type="h2"] { font-size:20px; color:var(--heading); line-height:1.4; padding:3px 0 2px; font-family:var(--serif); font-weight:200; }
|
|
51
|
+
.be-block[data-type="h3"] { font-size:13px; color:var(--text); line-height:1.6; padding:2px 0; font-family:var(--mono); }
|
|
52
|
+
.be-block[data-type="blockquote"] { border-left:2px solid var(--gold); padding:2px 0 2px 16px; color:var(--mid); font-style:italic; }
|
|
53
|
+
.be-block[data-type="pre"] { font-family:var(--mono); font-size:12px; background:var(--bg1); border:1px solid var(--line); padding:14px 16px; white-space:pre-wrap; line-height:1.6; margin:4px 0; }
|
|
54
|
+
.be-block[data-type="ul"] { padding-left:20px; }
|
|
55
|
+
.be-block[data-type="ul"]::before { content:"·"; position:absolute; left:6px; color:var(--muted); }
|
|
56
|
+
.be-block[data-type="ol"] { padding-left:20px; }
|
|
57
|
+
.be-block[data-type="hr"] { height:1px; min-height:1px; border:none; border-top:1px solid var(--line); margin:12px 0; padding:0; pointer-events:none; }
|
|
58
|
+
.be-block code { font-family:var(--mono); font-size:12px; background:var(--bg3); padding:1px 5px; }
|
|
59
|
+
.be-block strong { font-weight:600; color:var(--heading); }
|
|
60
|
+
.be-block em { color:var(--mid); font-style:italic; }
|
|
61
|
+
|
|
62
|
+
/* Saved indicator */
|
|
63
|
+
.saved-flash { background:var(--jade-bg); border-bottom:1px solid rgba(45,139,106,.15); padding:6px 20px; font-size:10px; color:var(--jade); display:flex; align-items:center; gap:6px; flex-shrink:0; }
|
|
64
|
+
.saved-flash::before { content:"✓"; }
|
|
65
|
+
|
|
66
|
+
/* Preview panel */
|
|
67
|
+
.preview-panel { display:none; border-left:1px solid var(--line); flex-direction:column; overflow:hidden; background:var(--bg2); }
|
|
68
|
+
.editor-shell.mode-split .preview-panel,
|
|
69
|
+
.editor-shell.mode-preview .preview-panel { display:flex; }
|
|
70
|
+
.preview-src-bar { display:flex; align-items:center; gap:4px; padding:6px 12px; border-bottom:1px solid var(--line); background:var(--bg1); flex-shrink:0; }
|
|
71
|
+
.src-btn { height:22px; padding:0 10px; border:1px solid var(--line); border-radius:4px; background:transparent; color:var(--muted); font-family:var(--mono); font-size:9px; cursor:pointer; transition:all .12s; }
|
|
72
|
+
.src-btn:hover { color:var(--text); border-color:var(--mid); }
|
|
73
|
+
.src-btn.active { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
|
|
74
|
+
.preview-scroll { flex:1; overflow-y:auto; padding:36px 40px 80px; }
|
|
75
|
+
.md-preview h1 { font-family:var(--serif); font-weight:200; font-size:28px; color:var(--heading); margin:0 0 16px; }
|
|
76
|
+
.md-preview h2 { font-family:var(--serif); font-weight:200; font-size:22px; color:var(--heading); margin:28px 0 12px; padding-bottom:8px; border-bottom:1px solid var(--line); }
|
|
77
|
+
.md-preview h3 { font-family:var(--mono); font-size:13px; color:var(--text); margin:20px 0 8px; }
|
|
78
|
+
.md-preview p { font-family:var(--serif); font-size:15px; color:var(--text); line-height:1.9; margin:0 0 16px; }
|
|
79
|
+
.md-preview blockquote { border-left:2px solid var(--gold); padding:4px 16px; margin:16px 0; color:var(--mid); font-style:italic; }
|
|
80
|
+
.md-preview code { font-family:var(--mono); font-size:12px; background:var(--bg3); padding:1px 5px; }
|
|
81
|
+
.md-preview pre { background:var(--bg1); border:1px solid var(--line); padding:16px; margin:16px 0; }
|
|
82
|
+
.md-preview strong { font-weight:600; color:var(--heading); }
|
|
83
|
+
.md-preview ul,.md-preview ol { padding-left:20px; margin:0 0 16px; }
|
|
84
|
+
.md-preview li { font-family:var(--serif); font-size:15px; line-height:1.9; }
|
|
85
|
+
.md-preview hr { border:none; border-top:1px solid var(--line); margin:28px 0; }
|
|
86
|
+
.md-preview-title { font-family:var(--serif); font-weight:200; font-size:30px; color:var(--heading); margin-bottom:6px; }
|
|
87
|
+
.md-preview-slug { font-size:10px; color:var(--muted); margin-bottom:28px; padding-bottom:16px; border-bottom:1px solid var(--line); }
|
|
88
|
+
.md-preview-slug span { color:var(--accent); }
|
|
89
|
+
.preview-iframe { flex:1; border:none; width:100%; }
|
|
90
|
+
|
|
91
|
+
/* Meta panel */
|
|
92
|
+
.meta-panel { background:var(--bg1); border-left:1px solid var(--line); display:flex; flex-direction:column; overflow-y:auto; width:280px; flex-shrink:0; }
|
|
93
|
+
.meta-section { padding:14px 16px; border-bottom:1px solid var(--line); }
|
|
94
|
+
.meta-section:last-child { border-bottom:none; }
|
|
95
|
+
.meta-label { font-size:9px; letter-spacing:.28em; text-transform:uppercase; color:var(--muted); margin-bottom:10px; }
|
|
96
|
+
.meta-field { margin-bottom:10px; }
|
|
97
|
+
.meta-field:last-child { margin-bottom:0; }
|
|
98
|
+
.field-label { font-size:9px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }
|
|
99
|
+
.field-readonly { font-size:11px; color:var(--muted); padding:6px 0; }
|
|
100
|
+
|
|
101
|
+
/* Status */
|
|
102
|
+
.status-bar { display:flex; align-items:center; gap:8px; padding:8px 0; margin-bottom:10px; }
|
|
103
|
+
.status-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
|
|
104
|
+
.status-dot.published { background:var(--jade); }
|
|
105
|
+
.status-dot.draft { background:var(--gold); }
|
|
106
|
+
.status-lbl { font-size:11px; }
|
|
107
|
+
.status-lbl.published { color:var(--jade); }
|
|
108
|
+
.status-lbl.draft { color:var(--gold); }
|
|
109
|
+
.btn-publish { display:block; width:100%; padding:9px; background:var(--gold); border:none; color:var(--bg0); font-family:var(--mono); font-size:10px; letter-spacing:.12em; cursor:pointer; transition:background .15s; margin-bottom:5px; border-radius:var(--radius); }
|
|
110
|
+
.btn-publish:hover { background:#7a5520; }
|
|
111
|
+
.btn-draft { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--line); color:var(--mid); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); }
|
|
112
|
+
.btn-draft:hover { border-color:var(--mid); color:var(--text); }
|
|
113
|
+
|
|
114
|
+
/* Schema fields in sidebar */
|
|
115
|
+
.field-input,.field-select { width:100%; background:var(--bg0); border:1px solid var(--line); padding:6px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; appearance:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
|
|
116
|
+
.field-input:focus,.field-select:focus { border-color:var(--accent); }
|
|
117
|
+
.media-drop-zone { border:1px dashed var(--line); background:var(--bg0); height:72px; cursor:pointer; transition:border-color .12s,background .12s; overflow:hidden; display:flex; align-items:center; justify-content:center; position:relative; border-radius:var(--radius); margin-top:5px; }
|
|
118
|
+
.media-drop-zone:hover { border-color:var(--gold); background:var(--gold-bg); }
|
|
119
|
+
.media-drop-zone.has-image { border-style:solid; }
|
|
120
|
+
.media-drop-zone img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
|
|
121
|
+
.media-drop-label { font-size:9px; color:var(--muted); letter-spacing:.08em; position:relative; z-index:1; }
|
|
122
|
+
.media-drop-zone.has-image .media-drop-label { display:none; }
|
|
123
|
+
.weekday-grid { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }
|
|
124
|
+
.wd-btn { height:26px; min-width:32px; padding:0 4px; background:var(--bg0); border:1px solid var(--line); color:var(--muted); font-family:var(--mono); font-size:9px; cursor:pointer; transition:all .1s; border-radius:2px; }
|
|
125
|
+
.wd-btn.active { background:var(--accent-bg); border-color:var(--accent); color:var(--accent); }
|
|
126
|
+
.tag-input { width:100%; background:var(--bg0); border:1px solid var(--line); padding:6px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
|
|
127
|
+
.tag-input:focus { border-color:var(--accent); }
|
|
128
|
+
.tag-preview { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
|
|
129
|
+
.tag-chip { font-size:9px; padding:2px 6px; background:var(--accent-bg); color:var(--accent); border:1px solid rgba(90,122,240,.2); border-radius:2px; }
|
|
130
|
+
.rel-picker { display:flex; flex-direction:column; gap:4px; }
|
|
131
|
+
.rel-search { width:100%; background:var(--bg0); border:1px solid var(--line); padding:5px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
|
|
132
|
+
.rel-search:focus { border-color:var(--accent); }
|
|
133
|
+
.rel-list { max-height:140px; overflow-y:auto; border:1px solid var(--line); background:var(--bg0); border-radius:var(--radius); }
|
|
134
|
+
.rel-item { display:flex; align-items:center; gap:6px; padding:5px 8px; cursor:pointer; border-bottom:1px solid var(--line2); }
|
|
135
|
+
.rel-item:last-child { border-bottom:none; }
|
|
136
|
+
.rel-item:hover { background:var(--line2); }
|
|
137
|
+
.rel-item input[type=checkbox] { display:none; }
|
|
138
|
+
.rel-check { width:13px; height:13px; flex-shrink:0; border:1px solid var(--line); background:var(--bg0); display:flex; align-items:center; justify-content:center; font-size:8px; color:transparent; transition:all .12s; }
|
|
139
|
+
.rel-item:has(input:checked) .rel-check { background:var(--accent); border-color:var(--accent); color:var(--bg0); }
|
|
140
|
+
.rel-name { font-size:11px; color:var(--text); }
|
|
141
|
+
.rel-selected { display:flex; flex-wrap:wrap; gap:3px; margin-bottom:4px; min-height:18px; }
|
|
142
|
+
.rel-tag { font-size:9px; padding:2px 6px; background:var(--accent-bg); color:var(--accent); border:1px solid rgba(90,122,240,.2); border-radius:2px; }
|
|
143
|
+
|
|
144
|
+
/* Version history */
|
|
145
|
+
.version-row { display:flex; align-items:center; gap:8px; padding:5px 0; border-bottom:1px solid var(--line2); }
|
|
146
|
+
.version-row:last-child { border-bottom:none; }
|
|
147
|
+
.v-dot { width:6px; height:6px; background:var(--line); border-radius:50%; flex-shrink:0; }
|
|
148
|
+
.v-dot.cur { background:var(--gold); }
|
|
149
|
+
.v-hash { font-size:10px; color:var(--mid); font-family:var(--mono); flex:1; }
|
|
150
|
+
.v-time { font-size:9px; color:var(--muted); }
|
|
151
|
+
|
|
152
|
+
/* Block picker */
|
|
153
|
+
.block-picker { position:fixed; z-index:9999; width:220px; background:var(--bg2); border:1px solid var(--line); box-shadow:0 4px 16px rgba(0,0,0,.12); border-radius:2px; overflow:hidden; display:none; font-family:var(--mono); }
|
|
154
|
+
.bp-header { padding:7px 12px; border-bottom:1px solid var(--line); font-size:10px; color:var(--muted); letter-spacing:.06em; }
|
|
155
|
+
.bp-list { max-height:240px; overflow-y:auto; }
|
|
156
|
+
.bp-item { display:flex; align-items:center; gap:10px; padding:7px 12px; cursor:pointer; }
|
|
157
|
+
.bp-item:hover,.bp-item.active { background:var(--accent-bg); }
|
|
158
|
+
.bp-icon { width:24px; height:24px; display:flex; align-items:center; justify-content:center; background:var(--bg3); border:1px solid var(--line); font-size:10px; flex-shrink:0; }
|
|
159
|
+
.bp-label { font-size:11px; color:var(--text); flex:1; }
|
|
160
|
+
.bp-hint { font-size:9px; color:var(--muted); }
|
|
161
|
+
|
|
162
|
+
/* Autosave indicator */
|
|
163
|
+
#autosave-indicator { font-size:10px; color:var(--muted); padding:0 12px 0 8px; border-left:1px solid var(--line); margin-left:8px; }
|
|
164
|
+
|
|
165
|
+
/* SEO char counter */
|
|
166
|
+
.seo-counter { font-size:9px; float:right; color:var(--muted); transition:color .12s; }
|
|
167
|
+
.seo-counter.ok { color:var(--jade); }
|
|
168
|
+
.seo-counter.warn { color:var(--gold); }
|
|
169
|
+
.seo-counter.over { color:var(--red); }
|
|
170
|
+
|
|
171
|
+
/* ── Image blocks ───────────────────────────────────────────── */
|
|
172
|
+
.be-block[data-type="image"] { position:relative; min-height:auto; padding:6px 0; line-height:1; cursor:default; user-select:none; outline:none; }
|
|
173
|
+
.be-block[data-type="image"] > .be-img { display:block; max-width:100%; border-radius:6px; border:2px solid transparent; transition:border-color .15s; }
|
|
174
|
+
.be-block[data-type="image"].img-sel > .be-img { border-color:var(--accent); }
|
|
175
|
+
.be-block[data-type="image"][data-align="left"] { float:left; max-width:48%; margin:4px 20px 12px 0; }
|
|
176
|
+
.be-block[data-type="image"][data-align="right"] { float:right; max-width:48%; margin:4px 0 12px 20px; }
|
|
177
|
+
.be-block[data-type="image"][data-align="center"] { clear:both; }
|
|
178
|
+
.be-block[data-type="image"][data-align="center"] > .be-img { margin:0 auto; }
|
|
179
|
+
.be-block[data-type="image"][data-align="full"] { clear:both; }
|
|
180
|
+
.be-block[data-type="image"][data-align="full"] > .be-img { width:100%; max-width:100%; }
|
|
181
|
+
.be-img-ctrl {
|
|
182
|
+
position:absolute; top:10px; left:50%; transform:translateX(-50%);
|
|
183
|
+
display:flex; align-items:center; gap:2px;
|
|
184
|
+
background:var(--bg2); border:1px solid var(--mid); border-radius:6px;
|
|
185
|
+
padding:3px 4px; box-shadow:0 4px 14px rgba(0,0,0,.35);
|
|
186
|
+
z-index:20; opacity:0; pointer-events:none; transition:opacity .15s; white-space:nowrap;
|
|
187
|
+
}
|
|
188
|
+
.be-block[data-type="image"].img-sel .be-img-ctrl { opacity:1; pointer-events:auto; }
|
|
189
|
+
.img-ab {
|
|
190
|
+
height:22px; min-width:28px; padding:0 6px;
|
|
191
|
+
background:transparent; border:1px solid transparent; border-radius:3px;
|
|
192
|
+
color:var(--muted); font-size:9px; font-family:var(--mono);
|
|
193
|
+
cursor:pointer; transition:all .1s;
|
|
194
|
+
display:flex; align-items:center; justify-content:center; gap:3px;
|
|
195
|
+
}
|
|
196
|
+
.img-ab:hover { background:var(--bg3); color:var(--text); border-color:var(--line); }
|
|
197
|
+
.img-ab.on { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
|
|
198
|
+
.img-ab.del:hover { background:var(--red); color:#fff; border-color:var(--red); }
|
|
199
|
+
.img-ctrl-sep { width:1px; height:14px; background:var(--line); margin:0 2px; }
|
|
200
|
+
.be-img-cap {
|
|
201
|
+
display:block; width:100%; margin-top:5px;
|
|
202
|
+
background:transparent; border:none; border-bottom:1px dashed transparent;
|
|
203
|
+
color:var(--muted); font-family:var(--mono); font-size:10px; letter-spacing:.02em;
|
|
204
|
+
padding:2px 0; outline:none; text-align:center; transition:border-color .12s; box-sizing:border-box;
|
|
205
|
+
}
|
|
206
|
+
.be-img-cap:focus { border-color:var(--line); color:var(--text); }
|
|
207
|
+
.be-block[data-type="image"].img-sel .be-img-cap { border-color:var(--line2); }
|
|
208
|
+
|
|
209
|
+
/* ── Image picker sheet ──────────────────────────────────────── */
|
|
210
|
+
.img-pick-overlay {
|
|
211
|
+
position:fixed; inset:0; background:rgba(0,0,0,.55); backdrop-filter:blur(4px);
|
|
212
|
+
z-index:8000; display:none; align-items:flex-end;
|
|
213
|
+
}
|
|
214
|
+
.img-pick-overlay.open { display:flex; }
|
|
215
|
+
.img-pick-sheet {
|
|
216
|
+
width:100%; max-height:64vh;
|
|
217
|
+
background:var(--bg1); border-top:1px solid var(--mid);
|
|
218
|
+
border-radius:12px 12px 0 0; display:flex; flex-direction:column; overflow:hidden;
|
|
219
|
+
}
|
|
220
|
+
.img-pick-head {
|
|
221
|
+
display:flex; align-items:center; gap:8px; padding:10px 16px;
|
|
222
|
+
border-bottom:1px solid var(--line); background:var(--bg2); flex-shrink:0;
|
|
223
|
+
}
|
|
224
|
+
.img-pick-title { font-family:var(--mono); font-size:10px; letter-spacing:.1em; color:var(--muted); text-transform:uppercase; flex:1; }
|
|
225
|
+
.img-pick-upload {
|
|
226
|
+
height:26px; padding:0 12px; background:var(--accent-bg); border:1px solid var(--accent);
|
|
227
|
+
color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer; transition:background .12s;
|
|
228
|
+
}
|
|
229
|
+
.img-pick-upload:hover { background:rgba(90,122,240,.2); }
|
|
230
|
+
.img-pick-close {
|
|
231
|
+
width:26px; height:26px; background:transparent; border:1px solid var(--line);
|
|
232
|
+
border-radius:4px; color:var(--muted); font-size:16px; cursor:pointer;
|
|
233
|
+
display:flex; align-items:center; justify-content:center; line-height:1;
|
|
234
|
+
}
|
|
235
|
+
.img-pick-close:hover { color:var(--text); border-color:var(--mid); }
|
|
236
|
+
.img-pick-body { flex:1; overflow-y:auto; padding:12px 16px 24px; }
|
|
237
|
+
.img-pick-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); gap:8px; }
|
|
238
|
+
.img-pick-thumb {
|
|
239
|
+
aspect-ratio:4/3; border:1px solid var(--line); border-radius:6px;
|
|
240
|
+
overflow:hidden; cursor:pointer; position:relative; background:var(--bg0);
|
|
241
|
+
transition:border-color .12s, transform .1s;
|
|
242
|
+
}
|
|
243
|
+
.img-pick-thumb:hover { border-color:var(--accent); transform:scale(1.03); }
|
|
244
|
+
.img-pick-thumb img { width:100%; height:100%; object-fit:cover; }
|
|
245
|
+
.img-pick-name {
|
|
246
|
+
position:absolute; bottom:0; left:0; right:0;
|
|
247
|
+
padding:3px 6px; background:rgba(0,0,0,.6);
|
|
248
|
+
font-size:9px; color:rgba(255,255,255,.75);
|
|
249
|
+
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
|
250
|
+
}
|
|
251
|
+
.img-pick-empty { padding:40px; text-align:center; font-size:12px; color:var(--muted); }
|
|
252
|
+
.img-pick-tabs { display:flex; gap:0; border-bottom:1px solid var(--line); flex-shrink:0; }
|
|
253
|
+
.img-pick-tab { flex:1; padding:8px; background:transparent; border:none; font-family:var(--mono); font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); cursor:pointer; transition:all .12s; border-bottom:2px solid transparent; margin-bottom:-1px; }
|
|
254
|
+
.img-pick-tab:hover { color:var(--text); }
|
|
255
|
+
.img-pick-tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
|
256
|
+
.img-pick-pane { display:none; flex:1; overflow-y:auto; }
|
|
257
|
+
.img-pick-pane.active { display:block; }
|
|
258
|
+
.img-url-pane { padding:16px; display:flex; flex-direction:column; gap:10px; }
|
|
259
|
+
.img-url-inp { width:100%; background:var(--bg2); border:1px solid var(--line); color:var(--text); font-family:var(--mono); font-size:11px; padding:8px 10px; border-radius:4px; box-sizing:border-box; }
|
|
260
|
+
.img-url-inp:focus { outline:none; border-color:var(--accent); }
|
|
261
|
+
.img-url-hint { font-size:10px; color:var(--muted); line-height:1.6; }
|
|
262
|
+
.img-url-hint strong { color:var(--text); font-weight:500; }
|
|
263
|
+
.img-url-btn { align-self:flex-end; padding:6px 16px; background:var(--accent-bg); border:1px solid var(--accent); color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer; transition:all .12s; }
|
|
264
|
+
.img-url-btn:hover { background:rgba(90,122,240,.25); }
|
|
265
|
+
.img-url-btn:disabled { opacity:.5; cursor:default; }
|
|
266
|
+
.img-url-status { font-size:10px; color:var(--muted); min-height:14px; }
|
|
267
|
+
|
|
268
|
+
/* ── Video blocks ───────────────────────────────────────────── */
|
|
269
|
+
.be-block[data-type="video"] {
|
|
270
|
+
position:relative; min-height:auto; padding:4px 0;
|
|
271
|
+
line-height:1; cursor:default; user-select:none; outline:none; clear:both;
|
|
272
|
+
}
|
|
273
|
+
.be-video-wrap {
|
|
274
|
+
position:relative; width:100%; padding-bottom:56.25%; /* 16:9 */
|
|
275
|
+
border-radius:8px; overflow:hidden;
|
|
276
|
+
border:2px solid transparent; transition:border-color .15s; background:var(--bg1);
|
|
277
|
+
}
|
|
278
|
+
.be-block[data-type="video"].vid-sel .be-video-wrap { border-color:var(--accent); }
|
|
279
|
+
.be-video-wrap iframe, .be-video-wrap video {
|
|
280
|
+
position:absolute; inset:0; width:100%; height:100%; border:none; border-radius:6px;
|
|
281
|
+
}
|
|
282
|
+
.be-video-overlay {
|
|
283
|
+
position:absolute; inset:0; cursor:default; z-index:5; border-radius:6px;
|
|
284
|
+
}
|
|
285
|
+
.be-block[data-type="video"].vid-sel .be-video-overlay { pointer-events:none; }
|
|
286
|
+
.be-vid-ctrl {
|
|
287
|
+
position:absolute; top:8px; left:50%; transform:translateX(-50%);
|
|
288
|
+
display:flex; align-items:center; gap:2px;
|
|
289
|
+
background:var(--bg2); border:1px solid var(--mid); border-radius:6px;
|
|
290
|
+
padding:3px 4px; box-shadow:0 4px 14px rgba(0,0,0,.35);
|
|
291
|
+
z-index:20; opacity:0; pointer-events:none; transition:opacity .15s; white-space:nowrap;
|
|
292
|
+
}
|
|
293
|
+
.be-block[data-type="video"].vid-sel .be-vid-ctrl { opacity:1; pointer-events:auto; }
|
|
294
|
+
.be-vid-url {
|
|
295
|
+
display:block; width:100%; margin-top:5px;
|
|
296
|
+
background:transparent; border:none; border-bottom:1px dashed transparent;
|
|
297
|
+
color:var(--muted); font-family:var(--mono); font-size:10px;
|
|
298
|
+
padding:2px 0; outline:none; text-align:center; transition:border-color .12s; box-sizing:border-box;
|
|
299
|
+
}
|
|
300
|
+
.be-vid-url:focus { border-color:var(--line); color:var(--text); }
|
|
301
|
+
.be-block[data-type="video"].vid-sel .be-vid-url { border-color:var(--line2); }
|
|
302
|
+
|
|
303
|
+
/* Video URL input modal */
|
|
304
|
+
.vid-url-overlay {
|
|
305
|
+
position:fixed; inset:0; background:rgba(0,0,0,.55); backdrop-filter:blur(4px);
|
|
306
|
+
z-index:8000; display:none; align-items:center; justify-content:center;
|
|
307
|
+
}
|
|
308
|
+
.vid-url-overlay.open { display:flex; }
|
|
309
|
+
.vid-url-box {
|
|
310
|
+
background:var(--bg2); border:1px solid var(--mid); border-radius:12px;
|
|
311
|
+
padding:24px 28px; width:460px; max-width:90vw;
|
|
312
|
+
box-shadow:0 8px 40px rgba(0,0,0,.5);
|
|
313
|
+
}
|
|
314
|
+
.vid-url-title { font-family:var(--mono); font-size:10px; letter-spacing:.1em; color:var(--muted); text-transform:uppercase; margin-bottom:12px; }
|
|
315
|
+
.vid-url-inp {
|
|
316
|
+
width:100%; background:var(--bg0); border:1px solid var(--line); border-radius:6px;
|
|
317
|
+
padding:9px 12px; color:var(--heading); font-family:var(--mono); font-size:12px;
|
|
318
|
+
outline:none; box-sizing:border-box; transition:border-color .15s;
|
|
319
|
+
}
|
|
320
|
+
.vid-url-inp:focus { border-color:var(--accent); }
|
|
321
|
+
.vid-url-hint { font-size:10px; color:var(--muted); margin-top:6px; }
|
|
322
|
+
.vid-url-actions { display:flex; gap:8px; margin-top:14px; justify-content:flex-end; }
|
|
323
|
+
.vid-url-ok {
|
|
324
|
+
height:30px; padding:0 16px; background:var(--accent-bg); border:1px solid var(--accent);
|
|
325
|
+
color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer;
|
|
326
|
+
}
|
|
327
|
+
.vid-url-ok:hover { background:rgba(90,122,240,.2); }
|
|
328
|
+
.vid-url-cancel {
|
|
329
|
+
height:30px; padding:0 12px; background:transparent; border:1px solid var(--line);
|
|
330
|
+
color:var(--muted); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer;
|
|
331
|
+
}
|
|
332
|
+
.vid-url-cancel:hover { border-color:var(--mid); color:var(--text); }
|
|
333
|
+
|
|
334
|
+
/* SERP preview */
|
|
335
|
+
.serp-preview { background:var(--bg0); border:1px solid var(--line); border-radius:var(--radius); padding:12px 14px; margin-top:12px; }
|
|
336
|
+
.serp-url { font-size:9px; color:var(--jade); margin-bottom:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
337
|
+
.serp-title { font-size:13px; color:#8ab4f8; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
338
|
+
.serp-desc { font-size:10px; color:var(--mid); line-height:1.5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
|
339
|
+
</style>
|
|
340
|
+
</head>
|
|
341
|
+
<body>
|
|
342
|
+
<div class="app">
|
|
343
|
+
<header class="topbar">
|
|
344
|
+
<a class="logo" href="/dashboard.html"><div class="logo-mark">OR</div>Orbiter</a>
|
|
345
|
+
<div id="editor-breadcrumb" style="font-size:11px;color:var(--muted);margin-left:16px;padding-left:16px;border-left:1px solid var(--line);display:flex;align-items:center;gap:6px;">
|
|
346
|
+
<a href="/collections.html" style="color:var(--muted);text-decoration:none;">Collections</a>
|
|
347
|
+
<span style="color:var(--line);">/</span>
|
|
348
|
+
<a id="back-to-col" href="/collections.html" style="color:var(--mid);text-decoration:none;"></a>
|
|
349
|
+
<span style="color:var(--line);">/</span>
|
|
350
|
+
<span id="breadcrumb-slug" style="color:var(--text);"></span>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="topbar-right">
|
|
353
|
+
<span id="autosave-indicator"></span>
|
|
354
|
+
<span id="wc" style="font-size:10px;color:var(--muted);padding:0 8px;"></span>
|
|
355
|
+
<div class="view-toggle" onmousedown="event.preventDefault()" style="margin-left:0;margin-right:12px;">
|
|
356
|
+
<button type="button" class="view-btn active" id="vbtn-edit" onclick="setViewMode('edit')">Edit</button>
|
|
357
|
+
<button type="button" class="view-btn" id="vbtn-split" onclick="setViewMode('split')">Split</button>
|
|
358
|
+
<button type="button" class="view-btn" id="vbtn-preview" onclick="setViewMode('preview')">Preview</button>
|
|
359
|
+
</div>
|
|
360
|
+
<button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
|
|
361
|
+
<span class="user" id="topbar-user"></span>
|
|
362
|
+
<span class="logout" id="logout-btn">Sign out</span>
|
|
363
|
+
</div>
|
|
364
|
+
</header>
|
|
365
|
+
<div class="editor-shell" id="editor-shell">
|
|
366
|
+
<div class="editor-main" id="editor-main">
|
|
367
|
+
<div id="saved-flash" style="display:none;" class="saved-flash">Entry saved</div>
|
|
368
|
+
<div class="editor-toolbar">
|
|
369
|
+
<div class="tool-group" onmousedown="event.preventDefault()">
|
|
370
|
+
<button type="button" class="tool-btn" onclick="beCmd('bold')" title="Bold"><b style="font-size:11px;">B</b></button>
|
|
371
|
+
<button type="button" class="tool-btn" onclick="beCmd('italic')" title="Italic"><i style="font-size:11px;">I</i></button>
|
|
372
|
+
<button type="button" class="tool-btn" onclick="beCmd('code')" title="Inline code" style="font-size:9px;"></></button>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="tool-sep"></div>
|
|
375
|
+
<div class="tool-group" onmousedown="event.preventDefault()">
|
|
376
|
+
<button type="button" class="tool-btn" onclick="changeBlockType('h1')" style="font-size:9px;">H1</button>
|
|
377
|
+
<button type="button" class="tool-btn" onclick="changeBlockType('h2')" style="font-size:9px;">H2</button>
|
|
378
|
+
<button type="button" class="tool-btn" onclick="changeBlockType('h3')" style="font-size:9px;">H3</button>
|
|
379
|
+
<button type="button" class="tool-btn" onclick="changeBlockType('blockquote')" >❝</button>
|
|
380
|
+
<button type="button" class="tool-btn" onclick="changeBlockType('ul')" style="font-size:12px;">·</button>
|
|
381
|
+
<button type="button" class="tool-btn" onclick="insertHr()" style="font-size:9px;">—</button>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="tool-sep"></div>
|
|
384
|
+
<div class="tool-group" onmousedown="event.preventDefault()">
|
|
385
|
+
<button type="button" class="tool-btn" onclick="openImgPicker()" title="Insert image">
|
|
386
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" style="display:block"><rect x=".75" y="1.75" width="11.5" height="9.5" rx="1.5" stroke="currentColor" stroke-width="1.1"/><circle cx="4.25" cy="5" r=".9" fill="currentColor"/><path d="M.75 9l2.9-2.9L6 8.45l1.85-1.85L12.25 10.5" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
387
|
+
</button>
|
|
388
|
+
<button type="button" class="tool-btn" onclick="openVideoPicker()" title="Insert video (YouTube, Vimeo, mp4)">
|
|
389
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" style="display:block"><rect x=".75" y="2.25" width="11.5" height="8.5" rx="1.5" stroke="currentColor" stroke-width="1.1"/><path d="M5.25 4.75l3.5 1.75-3.5 1.75V4.75z" fill="currentColor"/></svg>
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="editor-scroll">
|
|
394
|
+
<div class="editor-page">
|
|
395
|
+
<textarea id="title-input" class="editor-title-input" placeholder="Untitled" rows="1"
|
|
396
|
+
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px';onTitleInput()"></textarea>
|
|
397
|
+
<div class="editor-slug-line">/<span id="collection-id-display"></span>/<span id="slug-preview">…</span></div>
|
|
398
|
+
<div id="be-editor" class="be-editor" spellcheck="false"></div>
|
|
399
|
+
<textarea id="body-input" style="display:none"></textarea>
|
|
400
|
+
<div class="word-count" id="word-count-display"></div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<!-- Preview panel -->
|
|
406
|
+
<div class="preview-panel" id="preview-panel">
|
|
407
|
+
<div class="preview-src-bar">
|
|
408
|
+
<button class="src-btn active" id="src-md" onclick="setPreviewSrc('md')">Markdown</button>
|
|
409
|
+
<button class="src-btn" id="src-site" onclick="setPreviewSrc('site')">Site</button>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="preview-scroll" id="preview-md-pane">
|
|
412
|
+
<div class="md-preview-title" id="preview-title"></div>
|
|
413
|
+
<div class="md-preview-slug" id="preview-slug-line"></div>
|
|
414
|
+
<div class="md-preview" id="preview-body"></div>
|
|
415
|
+
</div>
|
|
416
|
+
<iframe class="preview-iframe" id="preview-iframe" style="display:none;" src="about:blank"></iframe>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<!-- Meta / sidebar -->
|
|
420
|
+
<div class="meta-panel" id="meta-panel">
|
|
421
|
+
<!-- filled by JS -->
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="block-picker" id="block-picker">
|
|
427
|
+
<div class="bp-header">/ Blocks — type to filter</div>
|
|
428
|
+
<div class="bp-list" id="bp-list"></div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<script type="module">
|
|
432
|
+
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
433
|
+
const me = await fetch('/api/auth/me',{credentials:'include'}).then(r=>r.json()).catch(()=>null);
|
|
434
|
+
if (!me?.user) { location.replace('/login.html'); }
|
|
435
|
+
document.getElementById('topbar-user').textContent = me.user.username;
|
|
436
|
+
document.getElementById('logout-btn').addEventListener('click',async()=>{
|
|
437
|
+
await fetch('/api/auth/logout',{method:'POST',credentials:'include'});
|
|
438
|
+
location.replace('/login.html');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Parse ?collection=X&slug=Y from URL
|
|
442
|
+
const params = new URLSearchParams(location.search);
|
|
443
|
+
const COLLECTION = params.get('collection');
|
|
444
|
+
const SLUG = params.get('slug') ?? 'new';
|
|
445
|
+
const IS_NEW = SLUG === 'new';
|
|
446
|
+
|
|
447
|
+
if (!COLLECTION) { location.replace('/collections.html'); }
|
|
448
|
+
document.getElementById('collection-id-display').textContent = COLLECTION;
|
|
449
|
+
|
|
450
|
+
// Load collection schema + entry
|
|
451
|
+
const [colData, entryData, versionsData, mediaData] = await Promise.all([
|
|
452
|
+
fetch(`/api/collections/${COLLECTION}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
|
|
453
|
+
IS_NEW ? null : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
|
|
454
|
+
IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/versions`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
455
|
+
fetch('/api/media',{credentials:'include'}).then(r=>r.json()).catch(()=>[]),
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
if (!colData) { location.replace('/collections.html'); }
|
|
459
|
+
|
|
460
|
+
// Breadcrumb
|
|
461
|
+
document.getElementById('back-to-col').textContent = colData.label;
|
|
462
|
+
document.getElementById('back-to-col').href = `/entries.html?col=${COLLECTION}&label=${encodeURIComponent(colData.label)}`;
|
|
463
|
+
document.getElementById('breadcrumb-slug').textContent = IS_NEW ? 'New entry' : SLUG;
|
|
464
|
+
document.title = `${IS_NEW ? 'New entry' : SLUG} — ${colData.label} — Orbiter`;
|
|
465
|
+
|
|
466
|
+
const schema = colData.schema ? (typeof colData.schema==='string'?JSON.parse(colData.schema):colData.schema) : {};
|
|
467
|
+
const extraFields = Object.entries(schema).filter(([k])=>k!=='title'&&k!=='body');
|
|
468
|
+
|
|
469
|
+
// Load relation entries for any relation fields
|
|
470
|
+
const relationEntries = {};
|
|
471
|
+
for (const [,f] of extraFields) {
|
|
472
|
+
if (f.type==='relation' && f.collection && !relationEntries[f.collection]) {
|
|
473
|
+
relationEntries[f.collection] = await fetch(`/api/collections/${f.collection}/entries`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Set title + slug
|
|
478
|
+
const titleInput = document.getElementById('title-input');
|
|
479
|
+
titleInput.value = IS_NEW ? '' : (entryData?.data?.title ?? SLUG);
|
|
480
|
+
titleInput.style.height = 'auto';
|
|
481
|
+
titleInput.style.height = titleInput.scrollHeight + 'px';
|
|
482
|
+
|
|
483
|
+
let currentSlug = IS_NEW ? '' : SLUG;
|
|
484
|
+
document.getElementById('slug-preview').textContent = currentSlug || '…';
|
|
485
|
+
|
|
486
|
+
// ── Meta panel ────────────────────────────────────────────────────
|
|
487
|
+
const SEO_TITLE_KEYS = new Set(['meta_title','seo_title','og_title']);
|
|
488
|
+
const SEO_DESC_KEYS = new Set(['meta_description','seo_description','og_description']);
|
|
489
|
+
|
|
490
|
+
function renderMetaPanel() {
|
|
491
|
+
const panel = document.getElementById('meta-panel');
|
|
492
|
+
const status = IS_NEW ? 'draft' : (entryData?.status ?? 'draft');
|
|
493
|
+
const updatedAt = entryData?.updated_at;
|
|
494
|
+
|
|
495
|
+
let fieldsHtml = '';
|
|
496
|
+
const seoFields = {};
|
|
497
|
+
for (const [key, field] of extraFields) {
|
|
498
|
+
const val = entryData?.data?.[key];
|
|
499
|
+
if (SEO_TITLE_KEYS.has(key)) seoFields.titleKey = key;
|
|
500
|
+
if (SEO_DESC_KEYS.has(key)) seoFields.descKey = key;
|
|
501
|
+
const isSeoTitle = SEO_TITLE_KEYS.has(key);
|
|
502
|
+
const isSeoDesc = SEO_DESC_KEYS.has(key);
|
|
503
|
+
if (isSeoTitle || isSeoDesc) {
|
|
504
|
+
fieldsHtml += `<div class="meta-field" data-field-key="${key}" ${field.showWhen?`data-show-when="${field.showWhen}"`:''}><div class="field-label" style="display:flex;justify-content:space-between;">${escHtml(field.label??key)}<span class="seo-counter" id="seo-ctr-${key}">${(val??'').length}</span></div>`;
|
|
505
|
+
} else {
|
|
506
|
+
fieldsHtml += `<div class="meta-field" data-field-key="${key}" ${field.showWhen?`data-show-when="${field.showWhen}"`:''}><div class="field-label">${escHtml(field.label??key)}</div>`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (field.type==='datetime') {
|
|
510
|
+
fieldsHtml += `<input type="datetime-local" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
|
|
511
|
+
} else if (field.type==='date') {
|
|
512
|
+
fieldsHtml += `<input type="date" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
|
|
513
|
+
} else if (field.type==='select') {
|
|
514
|
+
fieldsHtml += `<select class="field-select" name="${key}">${(field.options??[]).map(o=>`<option value="${escHtml(o)}"${val===o?' selected':''}>${escHtml(o)}</option>`).join('')}</select>`;
|
|
515
|
+
} else if (field.type==='weekdays') {
|
|
516
|
+
const active = Array.isArray(val) ? val : [];
|
|
517
|
+
fieldsHtml += `<div class="weekday-grid" id="wd-grid-${key}">
|
|
518
|
+
${[['Mon','Mo'],['Tue','Di'],['Wed','Mi'],['Thu','Do'],['Fri','Fr'],['Sat','Sa'],['Sun','So']].map(([d,l])=>`<button type="button" class="wd-btn${active.includes(d)?' active':''}" data-day="${d}" data-key="${key}">${l}</button>`).join('')}
|
|
519
|
+
<input type="hidden" name="${key}" id="wd-val-${key}" value="${active.join(',')}" />
|
|
520
|
+
</div>`;
|
|
521
|
+
} else if (field.type==='media') {
|
|
522
|
+
const hasVal = !!(val);
|
|
523
|
+
fieldsHtml += `<div>
|
|
524
|
+
<select class="field-select" name="${key}" id="media-sel-${key}">
|
|
525
|
+
<option value="">— none —</option>
|
|
526
|
+
${mediaData.map(m=>`<option value="${m.id}"${val===m.id?' selected':''}>${escHtml(m.filename)}${m.alt?` (${m.alt})`:''}</option>`).join('')}
|
|
527
|
+
</select>
|
|
528
|
+
<div class="media-drop-zone${hasVal?' has-image':''}" id="media-drop-${key}">
|
|
529
|
+
<img src="${hasVal?`/api/media/${val}/raw`:''}" alt="" id="media-img-${key}" style="${hasVal?'':'display:none'}" />
|
|
530
|
+
<div class="media-drop-label" id="media-drop-label-${key}">↑ drop or click</div>
|
|
531
|
+
</div>
|
|
532
|
+
<div style="margin-top:4px;"><span class="field-readonly" id="media-status-${key}"></span></div>
|
|
533
|
+
</div>`;
|
|
534
|
+
} else if (field.type==='array') {
|
|
535
|
+
const arrVal = Array.isArray(val) ? val.join(', ') : (val??'');
|
|
536
|
+
fieldsHtml += `<div>
|
|
537
|
+
<input type="text" class="tag-input" name="${key}" id="arr-${key}" value="${escHtml(arrVal)}" placeholder="tag1, tag2, …" />
|
|
538
|
+
<div class="tag-preview" id="tags-${key}"></div>
|
|
539
|
+
</div>`;
|
|
540
|
+
} else if (field.type==='relation') {
|
|
541
|
+
const selected = Array.isArray(val) ? val : (val ? [val] : []);
|
|
542
|
+
const entries = relationEntries[field.collection] ?? [];
|
|
543
|
+
const selHtml = selected.length
|
|
544
|
+
? selected.map(id=>{ const e=entries.find(x=>x.id===id); return e?`<span class="rel-tag">${escHtml(e.data?.title??e.slug)}</span>`:''; }).join('')
|
|
545
|
+
: '<span style="font-size:10px;color:var(--muted);">—</span>';
|
|
546
|
+
fieldsHtml += `<div class="rel-picker">
|
|
547
|
+
<div class="rel-selected" id="rel-selected-${key}">${selHtml}</div>
|
|
548
|
+
<input class="rel-search" type="text" placeholder="Filter…" id="rel-search-${key}" />
|
|
549
|
+
<div class="rel-list" id="rel-list-${key}">
|
|
550
|
+
${entries.length ? entries.map(e=>{
|
|
551
|
+
const checked = selected.includes(e.id);
|
|
552
|
+
return `<label class="rel-item" data-key="${key}">
|
|
553
|
+
<input type="checkbox" name="${key}" value="${e.id}"${checked?' checked':''} />
|
|
554
|
+
<span class="rel-check">${checked?'✓':''}</span>
|
|
555
|
+
<span class="rel-name">${escHtml(e.data?.title??e.data?.name??e.slug)}</span>
|
|
556
|
+
</label>`;
|
|
557
|
+
}).join('') : '<div style="padding:10px;font-size:10px;color:var(--muted);">No entries</div>'}
|
|
558
|
+
</div>
|
|
559
|
+
</div>`;
|
|
560
|
+
} else if (field.type==='url') {
|
|
561
|
+
fieldsHtml += `<input type="url" class="field-input" name="${key}" value="${escHtml(val??'')}" placeholder="https://…" />`;
|
|
562
|
+
} else if (field.type==='number') {
|
|
563
|
+
fieldsHtml += `<input type="number" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
|
|
564
|
+
} else if (SEO_TITLE_KEYS.has(key)) {
|
|
565
|
+
fieldsHtml += `<input type="text" class="field-input" name="${key}" id="seo-inp-${key}" value="${escHtml(val??'')}" maxlength="120" />`;
|
|
566
|
+
} else if (SEO_DESC_KEYS.has(key)) {
|
|
567
|
+
fieldsHtml += `<textarea class="field-input" name="${key}" id="seo-inp-${key}" rows="3" maxlength="300" style="resize:vertical;">${escHtml(val??'')}</textarea>`;
|
|
568
|
+
} else {
|
|
569
|
+
fieldsHtml += `<input type="text" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
|
|
570
|
+
}
|
|
571
|
+
fieldsHtml += '</div>';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const versHtml = versionsData.slice(0,8).map((v,i)=>`
|
|
575
|
+
<div class="version-row">
|
|
576
|
+
<div class="v-dot${i===0?' cur':''}"></div>
|
|
577
|
+
<div class="v-hash">${v.id.slice(0,7)}</div>
|
|
578
|
+
<div class="v-time">${new Date(v.created_at).toLocaleDateString()}</div>
|
|
579
|
+
</div>
|
|
580
|
+
`).join('');
|
|
581
|
+
|
|
582
|
+
const hasSeo = !!(seoFields.titleKey || seoFields.descKey);
|
|
583
|
+
const serpHtml = hasSeo ? `<div class="serp-preview" id="serp-preview">
|
|
584
|
+
<div class="serp-url" id="serp-url">${location.host}/${COLLECTION}/<span id="serp-slug-part">${escHtml(IS_NEW?'…':SLUG)}</span></div>
|
|
585
|
+
<div class="serp-title" id="serp-title">${escHtml(document.getElementById('title-input')?.value||'Untitled')}</div>
|
|
586
|
+
<div class="serp-desc" id="serp-desc">—</div>
|
|
587
|
+
</div>` : '';
|
|
588
|
+
|
|
589
|
+
panel.innerHTML = `
|
|
590
|
+
${extraFields.length ? `<div class="meta-section"><div class="meta-label">Fields</div>${fieldsHtml}${serpHtml}</div>` : ''}
|
|
591
|
+
<div class="meta-section">
|
|
592
|
+
<div class="status-bar">
|
|
593
|
+
<div class="status-dot ${status}" id="status-dot"></div>
|
|
594
|
+
<div class="status-lbl ${status}" id="status-lbl">${status==='published'?'Published':'Draft'}</div>
|
|
595
|
+
</div>
|
|
596
|
+
<button type="button" class="btn-publish" id="btn-publish">Publish</button>
|
|
597
|
+
<button type="button" class="btn-draft" id="btn-draft">Save as draft</button>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="meta-section">
|
|
600
|
+
<div class="meta-label">Details</div>
|
|
601
|
+
<div class="meta-field">
|
|
602
|
+
<div class="field-label">Slug</div>
|
|
603
|
+
<input class="field-input" id="slug-input" value="${escHtml(IS_NEW?'':SLUG)}" placeholder="entry-slug" />
|
|
604
|
+
</div>
|
|
605
|
+
<div class="meta-field">
|
|
606
|
+
<div class="field-label">Status</div>
|
|
607
|
+
<select class="field-select" id="status-select">
|
|
608
|
+
<option value="draft" ${status==='draft' ?'selected':''}>Draft</option>
|
|
609
|
+
<option value="published" ${status==='published' ?'selected':''}>Published</option>
|
|
610
|
+
</select>
|
|
611
|
+
</div>
|
|
612
|
+
${updatedAt ? `<div class="meta-field"><div class="field-label">Modified</div><div class="field-readonly">${new Date(updatedAt).toLocaleDateString()}</div></div>` : ''}
|
|
613
|
+
</div>
|
|
614
|
+
${versionsData.length ? `<div class="meta-section"><div class="meta-label">History</div>${versHtml}</div>` : ''}
|
|
615
|
+
`;
|
|
616
|
+
|
|
617
|
+
// Wire up weekday toggles
|
|
618
|
+
panel.querySelectorAll('.wd-btn').forEach(btn=>{
|
|
619
|
+
btn.addEventListener('click',()=>{
|
|
620
|
+
btn.classList.toggle('active');
|
|
621
|
+
const key = btn.dataset.key;
|
|
622
|
+
const hidden = document.getElementById('wd-val-'+key);
|
|
623
|
+
const cur = hidden.value ? hidden.value.split(',') : [];
|
|
624
|
+
const idx = cur.indexOf(btn.dataset.day);
|
|
625
|
+
if (idx===-1) cur.push(btn.dataset.day); else cur.splice(idx,1);
|
|
626
|
+
hidden.value = cur.join(',');
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Wire up tag previews
|
|
631
|
+
panel.querySelectorAll('[id^="arr-"]').forEach(el=>{
|
|
632
|
+
const key = el.id.replace('arr-','');
|
|
633
|
+
renderTags(key);
|
|
634
|
+
el.addEventListener('input',()=>renderTags(key));
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Wire up relation search + selection
|
|
638
|
+
panel.querySelectorAll('.rel-search').forEach(search=>{
|
|
639
|
+
const key = search.id.replace('rel-search-','');
|
|
640
|
+
search.addEventListener('input',()=>{
|
|
641
|
+
const q = search.value.toLowerCase();
|
|
642
|
+
document.querySelectorAll(`#rel-list-${key} .rel-item`).forEach(item=>{
|
|
643
|
+
item.style.display = item.querySelector('.rel-name').textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
panel.querySelectorAll('.rel-item').forEach(item=>{
|
|
648
|
+
item.addEventListener('change',()=>updateRelSelected(item.dataset.key));
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Wire up media drop zones
|
|
652
|
+
panel.querySelectorAll('.media-drop-zone').forEach(zone=>{
|
|
653
|
+
const key = zone.id.replace('media-drop-','');
|
|
654
|
+
zone.addEventListener('click',()=>{ const fi=document.createElement('input');fi.type='file';fi.accept='image/*,video/*,application/pdf';fi.onchange=()=>uploadMediaField(key,fi);fi.click(); });
|
|
655
|
+
zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('drag-over');});
|
|
656
|
+
zone.addEventListener('dragleave',()=>zone.classList.remove('drag-over'));
|
|
657
|
+
zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('drag-over');if(e.dataTransfer.files.length){const fi={files:e.dataTransfer.files};uploadMediaField(key,fi);}});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Publish / draft buttons
|
|
661
|
+
document.getElementById('btn-publish').addEventListener('click',()=>saveEntry('published'));
|
|
662
|
+
document.getElementById('btn-draft').addEventListener('click',()=>saveEntry('draft'));
|
|
663
|
+
|
|
664
|
+
// Slug input → slug preview
|
|
665
|
+
document.getElementById('slug-input').addEventListener('input', e=>{
|
|
666
|
+
e.target.dataset.manual='1';
|
|
667
|
+
document.getElementById('slug-preview').textContent = e.target.value || '…';
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Status select sync
|
|
671
|
+
document.getElementById('status-select').addEventListener('change',e=>{
|
|
672
|
+
updateStatusUI(e.target.value);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// showWhen conditional fields
|
|
676
|
+
initConditional();
|
|
677
|
+
|
|
678
|
+
// SEO char counters + SERP preview live update
|
|
679
|
+
if (hasSeo) {
|
|
680
|
+
function seoCounterClass(len, isTitle) {
|
|
681
|
+
const [lo, hi] = isTitle ? [30, 60] : [120, 160];
|
|
682
|
+
if (len === 0) return '';
|
|
683
|
+
if (len < lo) return 'warn';
|
|
684
|
+
if (len <= hi) return 'ok';
|
|
685
|
+
return 'over';
|
|
686
|
+
}
|
|
687
|
+
function updateSerp() {
|
|
688
|
+
const titleEl = document.getElementById('serp-title');
|
|
689
|
+
const descEl = document.getElementById('serp-desc');
|
|
690
|
+
const slugEl = document.getElementById('serp-slug-part');
|
|
691
|
+
const slugVal = document.getElementById('slug-input')?.value || document.getElementById('slug-preview')?.textContent || '…';
|
|
692
|
+
if (slugEl) slugEl.textContent = slugVal;
|
|
693
|
+
if (titleEl) {
|
|
694
|
+
const seoTitle = seoFields.titleKey ? document.getElementById('seo-inp-'+seoFields.titleKey)?.value : '';
|
|
695
|
+
titleEl.textContent = seoTitle || document.getElementById('title-input')?.value || 'Untitled';
|
|
696
|
+
}
|
|
697
|
+
if (descEl) {
|
|
698
|
+
const seoDesc = seoFields.descKey ? document.getElementById('seo-inp-'+seoFields.descKey)?.value : '';
|
|
699
|
+
descEl.textContent = seoDesc || '—';
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (seoFields.titleKey) {
|
|
703
|
+
const inp = document.getElementById('seo-inp-'+seoFields.titleKey);
|
|
704
|
+
const ctr = document.getElementById('seo-ctr-'+seoFields.titleKey);
|
|
705
|
+
inp?.addEventListener('input',()=>{
|
|
706
|
+
const len = inp.value.length;
|
|
707
|
+
if (ctr) { ctr.textContent=len; ctr.className='seo-counter '+seoCounterClass(len,true); }
|
|
708
|
+
updateSerp();
|
|
709
|
+
});
|
|
710
|
+
if (ctr && inp) { const l=inp.value.length; ctr.textContent=l; ctr.className='seo-counter '+seoCounterClass(l,true); }
|
|
711
|
+
}
|
|
712
|
+
if (seoFields.descKey) {
|
|
713
|
+
const inp = document.getElementById('seo-inp-'+seoFields.descKey);
|
|
714
|
+
const ctr = document.getElementById('seo-ctr-'+seoFields.descKey);
|
|
715
|
+
inp?.addEventListener('input',()=>{
|
|
716
|
+
const len = inp.value.length;
|
|
717
|
+
if (ctr) { ctr.textContent=len; ctr.className='seo-counter '+seoCounterClass(len,false); }
|
|
718
|
+
updateSerp();
|
|
719
|
+
});
|
|
720
|
+
if (ctr && inp) { const l=inp.value.length; ctr.textContent=l; ctr.className='seo-counter '+seoCounterClass(l,false); }
|
|
721
|
+
}
|
|
722
|
+
// Also update SERP when title or slug changes
|
|
723
|
+
document.getElementById('title-input')?.addEventListener('input', updateSerp);
|
|
724
|
+
document.getElementById('slug-input')?.addEventListener('input', updateSerp);
|
|
725
|
+
updateSerp();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function updateStatusUI(status) {
|
|
730
|
+
document.getElementById('status-dot').className = 'status-dot ' + status;
|
|
731
|
+
document.getElementById('status-lbl').className = 'status-lbl ' + status;
|
|
732
|
+
document.getElementById('status-lbl').textContent = status==='published'?'Published':'Draft';
|
|
733
|
+
document.getElementById('status-select').value = status;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderTags(key) {
|
|
737
|
+
const input = document.getElementById('arr-'+key);
|
|
738
|
+
const preview = document.getElementById('tags-'+key);
|
|
739
|
+
if (!preview) return;
|
|
740
|
+
const tags = input.value.split(',').map(t=>t.trim()).filter(Boolean);
|
|
741
|
+
preview.innerHTML = tags.map(t=>`<span class="tag-chip">${escHtml(t)}</span>`).join('');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function updateRelSelected(key) {
|
|
745
|
+
const list = document.getElementById('rel-list-'+key);
|
|
746
|
+
const selected = document.getElementById('rel-selected-'+key);
|
|
747
|
+
if (!list||!selected) return;
|
|
748
|
+
setTimeout(()=>{
|
|
749
|
+
const checked = [...list.querySelectorAll('input[type=checkbox]:checked')];
|
|
750
|
+
selected.innerHTML = checked.length
|
|
751
|
+
? checked.map(cb=>{ const name=cb.closest('.rel-item')?.querySelector('.rel-name')?.textContent??cb.value; return `<span class="rel-tag">${escHtml(name)}</span>`; }).join('')
|
|
752
|
+
: '<span style="font-size:10px;color:var(--muted);">—</span>';
|
|
753
|
+
list.querySelectorAll('.rel-item').forEach(item=>{
|
|
754
|
+
const cb=item.querySelector('input[type=checkbox]');
|
|
755
|
+
const box=item.querySelector('.rel-check');
|
|
756
|
+
if (box) { box.textContent=cb?.checked?'✓':''; box.style.color=cb?.checked?'var(--bg0)':'transparent'; box.style.background=cb?.checked?'var(--accent)':'var(--bg0)'; box.style.borderColor=cb?.checked?'var(--accent)':'var(--line)'; }
|
|
757
|
+
});
|
|
758
|
+
}, 0);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function uploadMediaField(key, input) {
|
|
762
|
+
if (!input.files?.length) return;
|
|
763
|
+
const file = input.files[0];
|
|
764
|
+
const statusEl = document.getElementById('media-status-'+key);
|
|
765
|
+
const sel = document.getElementById('media-sel-'+key);
|
|
766
|
+
const zone = document.getElementById('media-drop-'+key);
|
|
767
|
+
const img = document.getElementById('media-img-'+key);
|
|
768
|
+
statusEl.textContent = '↑ uploading…';
|
|
769
|
+
const fd = new FormData();
|
|
770
|
+
fd.append('file',file); fd.append('alt',''); fd.append('folder','');
|
|
771
|
+
try {
|
|
772
|
+
const res = await fetch('/api/media',{method:'POST',credentials:'include',body:fd});
|
|
773
|
+
const data = await res.json();
|
|
774
|
+
if (data.id) {
|
|
775
|
+
const opt = document.createElement('option');
|
|
776
|
+
opt.value=data.id; opt.textContent=data.filename; opt.selected=true;
|
|
777
|
+
sel.appendChild(opt);
|
|
778
|
+
img.src='/api/media/'+data.id+'/raw'; img.style.display='block';
|
|
779
|
+
zone.classList.add('has-image');
|
|
780
|
+
statusEl.textContent='✓ '+data.filename;
|
|
781
|
+
setTimeout(()=>{statusEl.textContent='';},3000);
|
|
782
|
+
}
|
|
783
|
+
} catch(e) { statusEl.textContent='✗ '+e.message; }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function initConditional() {
|
|
787
|
+
const conditionals = document.querySelectorAll('[data-show-when]');
|
|
788
|
+
if (!conditionals.length) return;
|
|
789
|
+
function evalCond(cond) {
|
|
790
|
+
const [key,rawVal] = cond.split(':');
|
|
791
|
+
const negate = rawVal.startsWith('!');
|
|
792
|
+
const expected = negate ? rawVal.slice(1) : rawVal;
|
|
793
|
+
const ctrl = document.querySelector(`[name="${key}"]`);
|
|
794
|
+
const current = ctrl ? ctrl.value : 'none';
|
|
795
|
+
return negate ? current!==expected : current===expected;
|
|
796
|
+
}
|
|
797
|
+
function refreshAll() { conditionals.forEach(el=>{ el.style.display=evalCond(el.dataset.showWhen)?'':'none'; }); }
|
|
798
|
+
const controlKeys = new Set([...conditionals].map(el=>el.dataset.showWhen.split(':')[0]));
|
|
799
|
+
controlKeys.forEach(key=>{
|
|
800
|
+
const ctrl = document.querySelector(`[name="${key}"]`);
|
|
801
|
+
if (ctrl) ctrl.addEventListener('change',refreshAll);
|
|
802
|
+
});
|
|
803
|
+
refreshAll();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ── Save entry ────────────────────────────────────────────────────
|
|
807
|
+
let currentPath = location.pathname + location.search;
|
|
808
|
+
|
|
809
|
+
async function saveEntry(status, isAutosave=false) {
|
|
810
|
+
const title = document.getElementById('title-input').value;
|
|
811
|
+
syncToHidden();
|
|
812
|
+
const body = document.getElementById('body-input').value;
|
|
813
|
+
const slugInput = document.getElementById('slug-input');
|
|
814
|
+
const slug = isAutosave ? (currentSlug || slugify(title) || 'untitled') : (slugInput.value.trim() || slugify(title) || 'untitled');
|
|
815
|
+
|
|
816
|
+
// Gather extra field values
|
|
817
|
+
const data = { title, body };
|
|
818
|
+
for (const [key, field] of extraFields) {
|
|
819
|
+
if (field.type==='array'||field.type==='weekdays') {
|
|
820
|
+
const raw = document.getElementById('wd-val-'+key)?.value || document.querySelector(`[name="${key}"]`)?.value || '';
|
|
821
|
+
data[key] = raw ? raw.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
822
|
+
} else if (field.type==='relation') {
|
|
823
|
+
data[key] = [...document.querySelectorAll(`[name="${key}"]:checked`)].map(cb=>cb.value);
|
|
824
|
+
} else {
|
|
825
|
+
const el = document.querySelector(`[name="${key}"]`);
|
|
826
|
+
data[key] = el ? el.value : '';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (IS_NEW && !currentSlug) {
|
|
831
|
+
// Create new entry
|
|
832
|
+
const res = await fetch(`/api/collections/${COLLECTION}/entries`,{
|
|
833
|
+
method:'POST', credentials:'include',
|
|
834
|
+
headers:{'Content-Type':'application/json'},
|
|
835
|
+
body: JSON.stringify({ slug, data, status }),
|
|
836
|
+
});
|
|
837
|
+
const json = await res.json();
|
|
838
|
+
if (json.slug || json.id) {
|
|
839
|
+
currentSlug = json.slug ?? slug;
|
|
840
|
+
document.getElementById('slug-preview').textContent = currentSlug;
|
|
841
|
+
slugInput.value = currentSlug;
|
|
842
|
+
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
|
|
843
|
+
history.replaceState(null,'',newUrl);
|
|
844
|
+
currentPath = newUrl;
|
|
845
|
+
setIndicator('saved');
|
|
846
|
+
updateStatusUI(status);
|
|
847
|
+
if (!isAutosave) showFlash();
|
|
848
|
+
} else { setIndicator('error'); }
|
|
849
|
+
} else {
|
|
850
|
+
const targetSlug = currentSlug || SLUG;
|
|
851
|
+
const res = await fetch(`/api/collections/${COLLECTION}/entries/${targetSlug}`,{
|
|
852
|
+
method:'PUT', credentials:'include',
|
|
853
|
+
headers:{'Content-Type':'application/json'},
|
|
854
|
+
body: JSON.stringify({ slug, data, status }),
|
|
855
|
+
});
|
|
856
|
+
if (res.ok) {
|
|
857
|
+
const json = await res.json();
|
|
858
|
+
if (json.slug && json.slug!==currentSlug) {
|
|
859
|
+
currentSlug = json.slug;
|
|
860
|
+
const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
|
|
861
|
+
history.replaceState(null,'',newUrl);
|
|
862
|
+
document.getElementById('slug-preview').textContent = currentSlug;
|
|
863
|
+
slugInput.value = currentSlug;
|
|
864
|
+
}
|
|
865
|
+
setIndicator('saved'); updateStatusUI(status);
|
|
866
|
+
if (!isAutosave) showFlash();
|
|
867
|
+
} else { setIndicator('error'); }
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function slugify(s) {
|
|
872
|
+
return s.toLowerCase().replace(/\s+/g,'-').replace(/[^a-z0-9-]/g,'').slice(0,60);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function showFlash() {
|
|
876
|
+
const f = document.getElementById('saved-flash');
|
|
877
|
+
f.style.display=''; setTimeout(()=>f.style.display='none',2000);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ── Autosave ──────────────────────────────────────────────────────
|
|
881
|
+
const indicator = document.getElementById('autosave-indicator');
|
|
882
|
+
let autosaveTimer = null;
|
|
883
|
+
|
|
884
|
+
function setIndicator(state) {
|
|
885
|
+
const states = { pending:{text:'Unsaved',color:'var(--gold)'}, saving:{text:'Saving…',color:'var(--muted)'}, saved:{text:'✓ Saved',color:'var(--jade)'}, error:{text:'Error',color:'var(--red)'} };
|
|
886
|
+
const s = states[state] ?? {};
|
|
887
|
+
indicator.textContent = s.text ?? '';
|
|
888
|
+
indicator.style.color = s.color ?? '';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function scheduleAutosave() {
|
|
892
|
+
clearTimeout(autosaveTimer);
|
|
893
|
+
setIndicator('pending');
|
|
894
|
+
autosaveTimer = setTimeout(()=>{ setIndicator('saving'); saveEntry(document.getElementById('status-select')?.value??'draft', true); }, 2000);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
window.onTitleInput = function() {
|
|
898
|
+
const slugInput = document.getElementById('slug-input');
|
|
899
|
+
if (!slugInput.dataset.manual) {
|
|
900
|
+
const s = slugify(document.getElementById('title-input').value);
|
|
901
|
+
slugInput.value = s;
|
|
902
|
+
document.getElementById('slug-preview').textContent = s || '…';
|
|
903
|
+
}
|
|
904
|
+
updateWc(); scheduleAutosave(); updatePreview();
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// Cmd+S
|
|
908
|
+
document.addEventListener('keydown', e=>{
|
|
909
|
+
if ((e.metaKey||e.ctrlKey) && e.key==='s') {
|
|
910
|
+
e.preventDefault();
|
|
911
|
+
saveEntry(document.getElementById('status-select')?.value??'draft');
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// ── Block editor ──────────────────────────────────────────────────
|
|
916
|
+
const beEditor = document.getElementById('be-editor');
|
|
917
|
+
const bodyInput = document.getElementById('body-input');
|
|
918
|
+
|
|
919
|
+
function inlineMd(text) {
|
|
920
|
+
return text
|
|
921
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
922
|
+
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
|
923
|
+
.replace(/__(.+?)__/g,'<strong>$1</strong>')
|
|
924
|
+
.replace(/\*(.+?)\*/g,'<em>$1</em>')
|
|
925
|
+
.replace(/_(.+?)_/g,'<em>$1</em>')
|
|
926
|
+
.replace(/`(.+?)`/g,'<code>$1</code>');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function htmlToMd(html) {
|
|
930
|
+
return html
|
|
931
|
+
.replace(/<strong>([\s\S]*?)<\/strong>/gi,'**$1**')
|
|
932
|
+
.replace(/<b>([\s\S]*?)<\/b>/gi,'**$1**')
|
|
933
|
+
.replace(/<em>([\s\S]*?)<\/em>/gi,'_$1_')
|
|
934
|
+
.replace(/<i>([\s\S]*?)<\/i>/gi,'_$1_')
|
|
935
|
+
.replace(/<code>([\s\S]*?)<\/code>/gi,'`$1`')
|
|
936
|
+
.replace(/<br\s*\/?>/gi,' ')
|
|
937
|
+
.replace(/<[^>]+>/g,'')
|
|
938
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/ /g,' ');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function serialize() {
|
|
942
|
+
const lines = [];
|
|
943
|
+
beEditor.querySelectorAll('.be-block').forEach(b=>{
|
|
944
|
+
const type = b.dataset.type||'p';
|
|
945
|
+
if (type==='image') {
|
|
946
|
+
const id = b.dataset.mediaId ?? '';
|
|
947
|
+
const alt = b.querySelector('.be-img-cap')?.value ?? b.dataset.alt ?? '';
|
|
948
|
+
const align = b.dataset.align ?? 'center';
|
|
949
|
+
const suf = (align && align!=='center') ? `{.${align}}` : '';
|
|
950
|
+
lines.push(`${suf}`);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (type==='video') {
|
|
954
|
+
lines.push(`::video[${b.dataset.videoUrl ?? ''}]`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const text = htmlToMd(b.innerHTML);
|
|
958
|
+
if (type==='h1') lines.push('# '+text);
|
|
959
|
+
else if (type==='h2') lines.push('## '+text);
|
|
960
|
+
else if (type==='h3') lines.push('### '+text);
|
|
961
|
+
else if (type==='blockquote') lines.push('> '+text);
|
|
962
|
+
else if (type==='pre') lines.push('```\n'+text+'\n```');
|
|
963
|
+
else if (type==='ul') lines.push('- '+text);
|
|
964
|
+
else if (type==='ol') lines.push('1. '+text);
|
|
965
|
+
else if (type==='hr') lines.push('---');
|
|
966
|
+
else lines.push(text);
|
|
967
|
+
});
|
|
968
|
+
return lines.join('\n\n');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function syncToHidden() { bodyInput.value = serialize(); updateWc(); }
|
|
972
|
+
|
|
973
|
+
const BLK_PH = { h1:'Heading 1',h2:'Heading 2',h3:'Heading 3',blockquote:'Quote…',pre:'Code…',ul:'List item',ol:'List item',p:'Write something…' };
|
|
974
|
+
|
|
975
|
+
function createBlock(type, content) {
|
|
976
|
+
const el = document.createElement('div');
|
|
977
|
+
el.className='be-block'; el.dataset.type=type||'p'; el.dataset.ph=BLK_PH[type]||'Write something…';
|
|
978
|
+
if (type==='hr') { el.contentEditable='false'; }
|
|
979
|
+
else { el.contentEditable='true'; el.innerHTML=content||''; }
|
|
980
|
+
return el;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function parseMd(md) {
|
|
984
|
+
beEditor.innerHTML='';
|
|
985
|
+
if (!md) { beEditor.appendChild(createBlock('p','')); return; }
|
|
986
|
+
md.split(/\n{2,}/).forEach(chunk=>{
|
|
987
|
+
chunk=chunk.trim(); if (!chunk) return;
|
|
988
|
+
// Video block: ::video[url]
|
|
989
|
+
const vidM = chunk.match(/^::video\[(.+)\]$/);
|
|
990
|
+
if (vidM) { beEditor.appendChild(createVideoBlock(vidM[1])); return; }
|
|
991
|
+
// Image block:  or {.align}
|
|
992
|
+
const imgM = chunk.match(/^!\[([^\]]*)\]\(([^)]+)\)(?:\{\.(\w+)\})?$/);
|
|
993
|
+
if (imgM) { beEditor.appendChild(createImgBlock(imgM[2], imgM[1], imgM[3]||'center')); return; }
|
|
994
|
+
let type='p', content=chunk;
|
|
995
|
+
if (chunk.startsWith('# ')) { type='h1'; content=inlineMd(chunk.slice(2)); }
|
|
996
|
+
else if (chunk.startsWith('## ')) { type='h2'; content=inlineMd(chunk.slice(3)); }
|
|
997
|
+
else if (chunk.startsWith('### ')) { type='h3'; content=inlineMd(chunk.slice(4)); }
|
|
998
|
+
else if (chunk.startsWith('> ')) { type='blockquote'; content=inlineMd(chunk.slice(2)); }
|
|
999
|
+
else if (/^```/.test(chunk)) { type='pre'; content=chunk.replace(/^```[^\n]*\n?/,'').replace(/\n?```$/,''); }
|
|
1000
|
+
else if (chunk.startsWith('- ')) { type='ul'; content=inlineMd(chunk.slice(2)); }
|
|
1001
|
+
else if (/^\d+\.\s/.test(chunk)) { type='ol'; content=inlineMd(chunk.replace(/^\d+\.\s/,'')); }
|
|
1002
|
+
else if (chunk==='---') { type='hr'; content=''; }
|
|
1003
|
+
else { type='p'; content=inlineMd(chunk); }
|
|
1004
|
+
beEditor.appendChild(createBlock(type,content));
|
|
1005
|
+
});
|
|
1006
|
+
if (!beEditor.querySelector('.be-block')) beEditor.appendChild(createBlock('p',''));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function focusStart(el) { if (!el||el.contentEditable==='false') return; el.focus(); const r=document.createRange();r.setStart(el,0);r.collapse(true);const s=window.getSelection();s.removeAllRanges();s.addRange(r); }
|
|
1010
|
+
function focusEnd(el) { if (!el||el.contentEditable==='false') return; el.focus(); const r=document.createRange();r.selectNodeContents(el);r.collapse(false);const s=window.getSelection();s.removeAllRanges();s.addRange(r); }
|
|
1011
|
+
|
|
1012
|
+
function getFocusedBlock() {
|
|
1013
|
+
const sel=window.getSelection(); if (!sel||!sel.rangeCount) return null;
|
|
1014
|
+
let node=sel.getRangeAt(0).commonAncestorContainer;
|
|
1015
|
+
while (node&&node!==beEditor) { if (node.classList?.contains('be-block')) return node; node=node.parentNode; }
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
window.changeBlockType = function(type, block) {
|
|
1020
|
+
const b=block||getFocusedBlock(); if (!b) return;
|
|
1021
|
+
b.dataset.type=type; b.dataset.ph=BLK_PH[type]||'Write something…';
|
|
1022
|
+
if (type==='hr') { b.innerHTML=''; b.contentEditable='false'; }
|
|
1023
|
+
else { b.contentEditable='true'; focusEnd(b); }
|
|
1024
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
window.insertHr = function() {
|
|
1028
|
+
const b=getFocusedBlock(); const hr=createBlock('hr',''); const next=createBlock('p','');
|
|
1029
|
+
if (b) { b.after(hr); hr.after(next); } else { beEditor.appendChild(hr); beEditor.appendChild(next); }
|
|
1030
|
+
focusStart(next); syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
window.beCmd = function(cmd) {
|
|
1034
|
+
if (cmd==='code') {
|
|
1035
|
+
const sel=window.getSelection(); if (!sel||!sel.rangeCount) return;
|
|
1036
|
+
const range=sel.getRangeAt(0); const text=range.toString();
|
|
1037
|
+
if (text) { const code=document.createElement('code'); code.textContent=text; range.deleteContents(); range.insertNode(code); }
|
|
1038
|
+
} else { document.execCommand(cmd,false,null); }
|
|
1039
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
beEditor.addEventListener('keydown',e=>{
|
|
1043
|
+
if (bpOpen) {
|
|
1044
|
+
const items=bpFiltered();
|
|
1045
|
+
if (e.key==='ArrowDown') { e.preventDefault(); bpIdx=(bpIdx+1)%(items.length||1); bpRender(); return; }
|
|
1046
|
+
if (e.key==='ArrowUp') { e.preventDefault(); bpIdx=(bpIdx-1+(items.length||1))%(items.length||1); bpRender(); return; }
|
|
1047
|
+
if (e.key==='Enter') { e.preventDefault(); bpInsert(bpIdx); return; }
|
|
1048
|
+
if (e.key==='Escape') { e.preventDefault(); bpClose(); return; }
|
|
1049
|
+
}
|
|
1050
|
+
const b=getFocusedBlock(); if (!b) return;
|
|
1051
|
+
if (e.key==='Enter'&&!e.shiftKey) {
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
const type=b.dataset.type; const newType=(type==='ul'||type==='ol')?type:'p';
|
|
1054
|
+
const next=createBlock(newType,''); b.after(next); focusStart(next);
|
|
1055
|
+
syncToHidden(); scheduleAutosave(); updatePreview(); return;
|
|
1056
|
+
}
|
|
1057
|
+
if (e.key==='Backspace') {
|
|
1058
|
+
const isEmpty=b.innerText.trim()===''; const isFirst=b===beEditor.firstElementChild;
|
|
1059
|
+
if (isEmpty&&b.dataset.type!=='p') { e.preventDefault(); b.dataset.type='p'; b.dataset.ph=BLK_PH.p; b.innerHTML=''; b.contentEditable='true'; focusStart(b); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
|
|
1060
|
+
if (isEmpty&&!isFirst) { e.preventDefault(); const prev=b.previousElementSibling; b.remove(); if (prev&&prev.contentEditable!=='false') focusEnd(prev); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
beEditor.addEventListener('input',()=>{
|
|
1065
|
+
const b=getFocusedBlock(); if (!b||b.dataset.type==='hr') { syncToHidden(); scheduleAutosave(); updatePreview(); return; }
|
|
1066
|
+
if (bpOpen&&bpTargetBlock) {
|
|
1067
|
+
const text=bpTargetBlock.innerText; const typed=text.substring(1);
|
|
1068
|
+
if (typed.includes('\n')) bpClose(); else { bpQuery=typed; bpIdx=0; bpRender(); } return;
|
|
1069
|
+
}
|
|
1070
|
+
const text=b.innerText;
|
|
1071
|
+
if (text==='# ') { b.innerHTML=''; changeBlockType('h1',b); return; }
|
|
1072
|
+
if (text==='## ') { b.innerHTML=''; changeBlockType('h2',b); return; }
|
|
1073
|
+
if (text==='### ') { b.innerHTML=''; changeBlockType('h3',b); return; }
|
|
1074
|
+
if (text==='> ') { b.innerHTML=''; changeBlockType('blockquote',b); return; }
|
|
1075
|
+
if (text==='- ') { b.innerHTML=''; changeBlockType('ul',b); return; }
|
|
1076
|
+
if (text==='---') { const hr=createBlock('hr',''); const next=createBlock('p',''); b.replaceWith(hr); hr.after(next); focusStart(next); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
|
|
1077
|
+
if (text==='/') { bpOpen_(b); return; }
|
|
1078
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
beEditor.addEventListener('paste',e=>{
|
|
1082
|
+
e.preventDefault();
|
|
1083
|
+
const text=(e.clipboardData||window.clipboardData).getData('text/plain').trim();
|
|
1084
|
+
if (text && /^https?:\/\//i.test(text) && /youtube\.com|youtu\.be|vimeo\.com|wistia\.(com|net)|\.(?:mp4|webm|ogg|mov)(\?|$)/i.test(text)) {
|
|
1085
|
+
const b=getFocusedBlock();
|
|
1086
|
+
const block=createVideoBlock(text);
|
|
1087
|
+
if (b) {
|
|
1088
|
+
b.after(block);
|
|
1089
|
+
if (b.innerText.trim()==='') b.remove();
|
|
1090
|
+
if (!block.nextElementSibling||block.nextElementSibling.contentEditable==='false') block.after(createBlock('p',''));
|
|
1091
|
+
} else {
|
|
1092
|
+
beEditor.appendChild(block);
|
|
1093
|
+
beEditor.appendChild(createBlock('p',''));
|
|
1094
|
+
}
|
|
1095
|
+
syncToHidden(); scheduleAutosave(); updatePreview(); return;
|
|
1096
|
+
}
|
|
1097
|
+
if (text) document.execCommand('insertText',false,text);
|
|
1098
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
beEditor.addEventListener('click',e=>{
|
|
1102
|
+
if (e.target===beEditor) {
|
|
1103
|
+
const last=beEditor.lastElementChild;
|
|
1104
|
+
if (last&&last.contentEditable!=='false') focusEnd(last);
|
|
1105
|
+
else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
function updateWc() {
|
|
1110
|
+
const body = bodyInput.value;
|
|
1111
|
+
const words = body.trim() ? body.trim().split(/\s+/).filter(w=>w).length : 0;
|
|
1112
|
+
const chars = body.length;
|
|
1113
|
+
const mins = Math.max(1, Math.round(words / 200));
|
|
1114
|
+
const wcEl = document.getElementById('wc');
|
|
1115
|
+
const detEl = document.getElementById('word-count-display');
|
|
1116
|
+
if (wcEl) wcEl.textContent = words ? `${words.toLocaleString()} w · ${mins} min` : '';
|
|
1117
|
+
if (detEl) detEl.textContent = words ? `${words.toLocaleString()} words · ${chars.toLocaleString()} chars · ~${mins} min read` : '';
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ── View modes ────────────────────────────────────────────────────
|
|
1121
|
+
let viewMode='edit'; let previewSrc='md';
|
|
1122
|
+
const shell=document.getElementById('editor-shell');
|
|
1123
|
+
|
|
1124
|
+
window.setViewMode = function(mode) {
|
|
1125
|
+
viewMode=mode;
|
|
1126
|
+
shell.classList.remove('mode-split','mode-preview');
|
|
1127
|
+
if (mode!=='edit') shell.classList.add('mode-'+mode);
|
|
1128
|
+
['edit','split','preview'].forEach(m=>document.getElementById('vbtn-'+m).classList.toggle('active',m===mode));
|
|
1129
|
+
if (mode!=='edit') updatePreview();
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
window.setPreviewSrc = function(src) {
|
|
1133
|
+
previewSrc=src;
|
|
1134
|
+
document.getElementById('src-md').classList.toggle('active',src==='md');
|
|
1135
|
+
document.getElementById('src-site').classList.toggle('active',src==='site');
|
|
1136
|
+
document.getElementById('preview-md-pane').style.display = src==='md' ? '' : 'none';
|
|
1137
|
+
document.getElementById('preview-iframe').style.display = src==='site' ? '' : 'none';
|
|
1138
|
+
updatePreview();
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
function updatePreview() {
|
|
1142
|
+
if (viewMode==='edit') return;
|
|
1143
|
+
if (previewSrc==='md') renderMarkdown(); else renderIframe();
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function renderMarkdown() {
|
|
1147
|
+
const title=document.getElementById('title-input').value;
|
|
1148
|
+
const slug=document.getElementById('slug-preview').textContent;
|
|
1149
|
+
document.getElementById('preview-title').textContent=title||'Untitled';
|
|
1150
|
+
document.getElementById('preview-slug-line').innerHTML='/'+COLLECTION+'/<span>'+slug+'</span>';
|
|
1151
|
+
if (window.marked) {
|
|
1152
|
+
// Expand image blocks: {.align} → <img> with alignment style
|
|
1153
|
+
const prepped = (bodyInput.value||'').replace(
|
|
1154
|
+
/!\[([^\]]*)\]\(([0-9a-f-]{32,36})\)(?:\{\.(\w+)\})?/g,
|
|
1155
|
+
(_,alt,id,align)=>{
|
|
1156
|
+
const st = align==='left' ? 'float:left;max-width:48%;margin:4px 20px 12px 0;'
|
|
1157
|
+
: align==='right' ? 'float:right;max-width:48%;margin:4px 0 12px 20px;'
|
|
1158
|
+
: align==='full' ? 'width:100%;display:block;'
|
|
1159
|
+
: 'display:block;margin:0 auto;';
|
|
1160
|
+
return `<img src="/api/media/${id}/raw" alt="${alt}" style="border-radius:6px;max-width:100%;${st}">`;
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
const prepped2 = prepped.replace(/::video\[([^\]]+)\]/g, (_,url)=>{
|
|
1164
|
+
const embed = parseVideoUrl(url);
|
|
1165
|
+
if (!embed) return `<video src="${url}" controls style="width:100%;border-radius:8px;margin:16px 0;"></video>`;
|
|
1166
|
+
return `<div style="position:relative;padding-bottom:56.25%;height:0;margin:16px 0;overflow:hidden;border-radius:8px;"><iframe src="${embed}" frameborder="0" allowfullscreen allow="autoplay;encrypted-media" style="position:absolute;top:0;left:0;width:100%;height:100%;"></iframe></div>`;
|
|
1167
|
+
});
|
|
1168
|
+
document.getElementById('preview-body').innerHTML=marked.parse(prepped2);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function renderIframe() {
|
|
1173
|
+
const slug=document.getElementById('slug-preview').textContent||currentSlug;
|
|
1174
|
+
document.getElementById('preview-iframe').src=window.location.origin+'/'+COLLECTION+'/'+slug;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ── Block picker ──────────────────────────────────────────────────
|
|
1178
|
+
const BP_BLOCKS = [
|
|
1179
|
+
{label:'Heading 1', icon:'H1', hint:'#', type:'h1'},
|
|
1180
|
+
{label:'Heading 2', icon:'H2', hint:'##', type:'h2'},
|
|
1181
|
+
{label:'Heading 3', icon:'H3', hint:'###', type:'h3'},
|
|
1182
|
+
{label:'Quote', icon:'❝', hint:'>', type:'blockquote'},
|
|
1183
|
+
{label:'Code block', icon:'{}', hint:'```', type:'pre'},
|
|
1184
|
+
{label:'List', icon:'·', hint:'-', type:'ul'},
|
|
1185
|
+
{label:'Numbered', icon:'1.', hint:'1.', type:'ol'},
|
|
1186
|
+
{label:'Divider', icon:'—', hint:'---', type:'hr'},
|
|
1187
|
+
{label:'Image', icon:'⊡', hint:'img', type:'image'},
|
|
1188
|
+
{label:'Video', icon:'▶', hint:'vid', type:'video'},
|
|
1189
|
+
];
|
|
1190
|
+
let bpOpen=false, bpIdx=0, bpQuery='', bpTargetBlock=null;
|
|
1191
|
+
const bpEl=document.getElementById('block-picker');
|
|
1192
|
+
const bpListEl=document.getElementById('bp-list');
|
|
1193
|
+
|
|
1194
|
+
function bpFiltered() {
|
|
1195
|
+
if (!bpQuery) return BP_BLOCKS;
|
|
1196
|
+
const q=bpQuery.toLowerCase();
|
|
1197
|
+
return BP_BLOCKS.filter(b=>b.label.toLowerCase().includes(q)||b.hint.includes(q));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function bpRender() {
|
|
1201
|
+
bpListEl.innerHTML='';
|
|
1202
|
+
const items=bpFiltered();
|
|
1203
|
+
if (!items.length) { bpListEl.innerHTML='<div style="padding:12px;font-size:11px;color:var(--muted);text-align:center;">No blocks</div>'; return; }
|
|
1204
|
+
items.forEach((b,i)=>{
|
|
1205
|
+
const row=document.createElement('div');
|
|
1206
|
+
row.className='bp-item'+(i===bpIdx?' active':'');
|
|
1207
|
+
row.innerHTML=`<div class="bp-icon">${b.icon}</div><div class="bp-label">${b.label}</div><div class="bp-hint">${b.hint}</div>`;
|
|
1208
|
+
row.addEventListener('mousedown',ev=>{ev.preventDefault();bpInsert(i);});
|
|
1209
|
+
row.addEventListener('mouseover',()=>{bpIdx=i;bpRender();});
|
|
1210
|
+
bpListEl.appendChild(row);
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function bpOpen_(block) {
|
|
1215
|
+
bpTargetBlock=block; bpQuery=''; bpIdx=0; bpOpen=true;
|
|
1216
|
+
const rect=block.getBoundingClientRect();
|
|
1217
|
+
bpEl.style.top=Math.min(rect.bottom+6,window.innerHeight-280)+'px';
|
|
1218
|
+
bpEl.style.left=Math.min(rect.left,window.innerWidth-240)+'px';
|
|
1219
|
+
bpEl.style.display='block'; bpRender();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function bpClose() { bpOpen=false; bpEl.style.display='none'; }
|
|
1223
|
+
|
|
1224
|
+
function bpInsert(relIdx) {
|
|
1225
|
+
const items=bpFiltered(); const blk=items[relIdx];
|
|
1226
|
+
if (!blk||!bpTargetBlock) { bpClose(); return; }
|
|
1227
|
+
if (blk.type==='image') {
|
|
1228
|
+
pickerAnchor = bpTargetBlock;
|
|
1229
|
+
bpTargetBlock.innerHTML=''; bpClose(); openImgPicker(); return;
|
|
1230
|
+
}
|
|
1231
|
+
if (blk.type==='video') {
|
|
1232
|
+
vidPickerAnchor = bpTargetBlock;
|
|
1233
|
+
bpTargetBlock.innerHTML=''; bpClose(); openVideoPicker(); return;
|
|
1234
|
+
}
|
|
1235
|
+
bpTargetBlock.innerHTML='';
|
|
1236
|
+
if (blk.type==='hr') {
|
|
1237
|
+
const hr=createBlock('hr',''); const next=createBlock('p','');
|
|
1238
|
+
bpTargetBlock.replaceWith(hr); hr.after(next); focusStart(next);
|
|
1239
|
+
} else { changeBlockType(blk.type,bpTargetBlock); }
|
|
1240
|
+
syncToHidden(); scheduleAutosave(); updatePreview(); bpClose();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
document.addEventListener('mousedown',e=>{if(bpOpen&&!bpEl.contains(e.target))bpClose();});
|
|
1244
|
+
|
|
1245
|
+
// ── Image blocks ──────────────────────────────────────────────────
|
|
1246
|
+
let pickerAnchor = null;
|
|
1247
|
+
|
|
1248
|
+
function refreshAlignBtns(block, align) {
|
|
1249
|
+
block.querySelectorAll('.img-ab[data-align]').forEach(b=>b.classList.toggle('on', b.dataset.align===align));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function createImgBlock(mediaId, alt, align) {
|
|
1253
|
+
alt = alt ?? '';
|
|
1254
|
+
align = align ?? 'center';
|
|
1255
|
+
const el = document.createElement('div');
|
|
1256
|
+
el.className='be-block'; el.dataset.type='image';
|
|
1257
|
+
el.dataset.mediaId=mediaId; el.dataset.alt=alt; el.dataset.align=align;
|
|
1258
|
+
el.contentEditable='false';
|
|
1259
|
+
el.innerHTML = `
|
|
1260
|
+
<div class="be-img-ctrl">
|
|
1261
|
+
<button class="img-ab" data-align="left" title="Float left" onclick="imgAlign(this)">◧ left</button>
|
|
1262
|
+
<button class="img-ab" data-align="center" title="Center" onclick="imgAlign(this)">▣ center</button>
|
|
1263
|
+
<button class="img-ab" data-align="right" title="Float right" onclick="imgAlign(this)">◨ right</button>
|
|
1264
|
+
<button class="img-ab" data-align="full" title="Full width" onclick="imgAlign(this)">▭ full</button>
|
|
1265
|
+
<div class="img-ctrl-sep"></div>
|
|
1266
|
+
<button class="img-ab del" title="Remove image" onclick="imgDelete(this)">✕</button>
|
|
1267
|
+
</div>
|
|
1268
|
+
<img class="be-img" src="/api/media/${mediaId}/raw" alt="">
|
|
1269
|
+
<input class="be-img-cap" type="text" placeholder="Caption / alt text…" value="">
|
|
1270
|
+
`;
|
|
1271
|
+
el.querySelector('.be-img').alt = alt;
|
|
1272
|
+
el.querySelector('.be-img-cap').value = alt;
|
|
1273
|
+
refreshAlignBtns(el, align);
|
|
1274
|
+
|
|
1275
|
+
el.querySelector('.be-img-cap').addEventListener('input', function() {
|
|
1276
|
+
el.dataset.alt = this.value;
|
|
1277
|
+
el.querySelector('.be-img').alt = this.value;
|
|
1278
|
+
syncToHidden(); scheduleAutosave();
|
|
1279
|
+
});
|
|
1280
|
+
el.querySelector('.be-img-cap').addEventListener('click', e=>e.stopPropagation());
|
|
1281
|
+
|
|
1282
|
+
el.addEventListener('click', e=>{
|
|
1283
|
+
if (e.target.closest('.img-ab')) return;
|
|
1284
|
+
e.stopPropagation();
|
|
1285
|
+
beEditor.querySelectorAll('.be-block[data-type="image"]').forEach(b=>b.classList.remove('img-sel'));
|
|
1286
|
+
el.classList.add('img-sel');
|
|
1287
|
+
});
|
|
1288
|
+
return el;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
window.imgAlign = function(btn) {
|
|
1292
|
+
const block=btn.closest('.be-block[data-type="image"]'); if(!block) return;
|
|
1293
|
+
block.dataset.align=btn.dataset.align;
|
|
1294
|
+
refreshAlignBtns(block, btn.dataset.align);
|
|
1295
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
window.imgDelete = function(btn) {
|
|
1299
|
+
const block=btn.closest('.be-block[data-type="image"]'); if(!block) return;
|
|
1300
|
+
const prev=block.previousElementSibling, next=block.nextElementSibling;
|
|
1301
|
+
block.remove();
|
|
1302
|
+
if (next&&next.contentEditable!=='false') focusStart(next);
|
|
1303
|
+
else if (prev&&prev.contentEditable!=='false') focusEnd(prev);
|
|
1304
|
+
else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
|
|
1305
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
// Deselect image blocks on outside click
|
|
1309
|
+
document.addEventListener('click', e=>{
|
|
1310
|
+
if (!e.target.closest('.be-block[data-type="image"]'))
|
|
1311
|
+
beEditor.querySelectorAll('.be-block[data-type="image"]').forEach(b=>b.classList.remove('img-sel'));
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// ── Image picker ──────────────────────────────────────────────────
|
|
1315
|
+
window.openImgPicker = function() {
|
|
1316
|
+
pickerAnchor = getFocusedBlock();
|
|
1317
|
+
document.getElementById('img-pick-overlay').classList.add('open');
|
|
1318
|
+
renderPickerGrid();
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
window.closeImgPicker = function() {
|
|
1322
|
+
document.getElementById('img-pick-overlay').classList.remove('open');
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
function renderPickerGrid() {
|
|
1326
|
+
const grid=document.getElementById('img-pick-grid');
|
|
1327
|
+
const images=mediaData.filter(m=>m.mime_type?.startsWith('image/'));
|
|
1328
|
+
if (!images.length) {
|
|
1329
|
+
grid.innerHTML='<div class="img-pick-empty">No images yet.<br>Upload one to get started.</div>'; return;
|
|
1330
|
+
}
|
|
1331
|
+
grid.innerHTML=images.map(m=>`
|
|
1332
|
+
<div class="img-pick-thumb" onclick="pickInsert('${m.id}','${escHtml(m.alt??m.filename)}')">
|
|
1333
|
+
<img src="/api/media/${m.id}/raw" alt="${escHtml(m.alt??m.filename)}" loading="lazy">
|
|
1334
|
+
<div class="img-pick-name">${escHtml(m.filename)}</div>
|
|
1335
|
+
</div>`).join('');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
window.pickInsert = function(mediaId, alt) {
|
|
1339
|
+
closeImgPicker();
|
|
1340
|
+
doInsertImg(mediaId, alt||'');
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
function doInsertImg(mediaId, alt) {
|
|
1344
|
+
const block=createImgBlock(mediaId, alt, 'center');
|
|
1345
|
+
const anchor=pickerAnchor;
|
|
1346
|
+
if (anchor) {
|
|
1347
|
+
anchor.after(block);
|
|
1348
|
+
if (!block.nextElementSibling||block.nextElementSibling.dataset.type==='image')
|
|
1349
|
+
block.after(createBlock('p',''));
|
|
1350
|
+
} else {
|
|
1351
|
+
beEditor.appendChild(block);
|
|
1352
|
+
beEditor.appendChild(createBlock('p',''));
|
|
1353
|
+
}
|
|
1354
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Upload from file (native picker — supports iCloud Drive, Dropbox, Google Drive via OS)
|
|
1358
|
+
document.getElementById('img-file-inp').addEventListener('change', async function() {
|
|
1359
|
+
const file=this.files[0]; if (!file) return;
|
|
1360
|
+
this.value='';
|
|
1361
|
+
const btn=document.querySelector('.img-pick-upload');
|
|
1362
|
+
const orig=btn.textContent; btn.textContent='Uploading…'; btn.disabled=true;
|
|
1363
|
+
const fd=new FormData(); fd.append('file',file); fd.append('alt',''); fd.append('folder','');
|
|
1364
|
+
try {
|
|
1365
|
+
const res=await fetch('/api/media',{method:'POST',credentials:'include',body:fd});
|
|
1366
|
+
if (res.ok) {
|
|
1367
|
+
const media=await res.json();
|
|
1368
|
+
mediaData.unshift(media);
|
|
1369
|
+
closeImgPicker();
|
|
1370
|
+
doInsertImg(media.id, media.alt??'');
|
|
1371
|
+
} else { alert('Upload failed'); }
|
|
1372
|
+
} catch { alert('Upload error'); }
|
|
1373
|
+
finally { btn.textContent=orig; btn.disabled=false; }
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
window.imgPickTab = function(tab, btn) {
|
|
1377
|
+
document.querySelectorAll('.img-pick-tab').forEach(b=>b.classList.remove('active'));
|
|
1378
|
+
document.querySelectorAll('.img-pick-pane').forEach(p=>p.classList.remove('active'));
|
|
1379
|
+
btn.classList.add('active');
|
|
1380
|
+
document.getElementById('img-pick-pane-'+tab).classList.add('active');
|
|
1381
|
+
if (tab==='url') setTimeout(()=>document.getElementById('img-url-inp').focus(), 40);
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
window.importFromUrl = async function() {
|
|
1385
|
+
const inp = document.getElementById('img-url-inp');
|
|
1386
|
+
const url = inp.value.trim();
|
|
1387
|
+
if (!url) return;
|
|
1388
|
+
const btn = document.getElementById('img-url-btn');
|
|
1389
|
+
const status = document.getElementById('img-url-status');
|
|
1390
|
+
btn.disabled=true; btn.textContent='Wird geladen…'; status.textContent='';
|
|
1391
|
+
try {
|
|
1392
|
+
const res = await fetch('/api/media/import-url', {
|
|
1393
|
+
method:'POST', credentials:'include',
|
|
1394
|
+
headers:{'Content-Type':'application/json'},
|
|
1395
|
+
body: JSON.stringify({ url, alt:'', folder:'' })
|
|
1396
|
+
});
|
|
1397
|
+
if (!res.ok) { const e=await res.json().catch(()=>({})); status.textContent='Fehler: '+(e.error||res.status); return; }
|
|
1398
|
+
const media = await res.json();
|
|
1399
|
+
mediaData.unshift(media);
|
|
1400
|
+
closeImgPicker();
|
|
1401
|
+
doInsertImg(media.id, media.alt??'');
|
|
1402
|
+
inp.value='';
|
|
1403
|
+
} catch(e) {
|
|
1404
|
+
status.textContent='Netzwerkfehler: '+e.message;
|
|
1405
|
+
} finally {
|
|
1406
|
+
btn.disabled=false; btn.textContent='Importieren';
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
document.getElementById('img-url-inp').addEventListener('keydown', e=>{
|
|
1411
|
+
if (e.key==='Enter') { e.preventDefault(); importFromUrl(); }
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
function escHtml(str) {
|
|
1415
|
+
return String(str??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// ── Video blocks ──────────────────────────────────────────────────
|
|
1419
|
+
let vidPickerAnchor = null;
|
|
1420
|
+
|
|
1421
|
+
function parseVideoUrl(url) {
|
|
1422
|
+
url = (url||'').trim();
|
|
1423
|
+
let m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/);
|
|
1424
|
+
if (m) return `https://www.youtube.com/embed/${m[1]}?rel=0`;
|
|
1425
|
+
m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
|
|
1426
|
+
if (m) return `https://player.vimeo.com/video/${m[1]}`;
|
|
1427
|
+
m = url.match(/wistia\.(?:com|net)\/(?:medias|embed\/iframe)\/(\w+)/);
|
|
1428
|
+
if (m) return `https://fast.wistia.net/embed/iframe/${m[1]}`;
|
|
1429
|
+
return null; // direct file URL — use <video> tag
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function createVideoBlock(url) {
|
|
1433
|
+
const embed = parseVideoUrl(url);
|
|
1434
|
+
const isDirect = /\.(mp4|webm|ogg|mov)(\?.*)?$/i.test(url);
|
|
1435
|
+
const el = document.createElement('div');
|
|
1436
|
+
el.className='be-block'; el.dataset.type='video';
|
|
1437
|
+
el.dataset.videoUrl = url;
|
|
1438
|
+
el.contentEditable='false';
|
|
1439
|
+
let mediaHtml;
|
|
1440
|
+
if (embed) {
|
|
1441
|
+
mediaHtml = `<iframe src="${escHtml(embed)}" frameborder="0" allowfullscreen allow="autoplay; encrypted-media"></iframe><div class="be-video-overlay"></div>`;
|
|
1442
|
+
} else if (isDirect) {
|
|
1443
|
+
mediaHtml = `<video src="${escHtml(url)}" controls></video>`;
|
|
1444
|
+
} else {
|
|
1445
|
+
mediaHtml = `<div style="padding:20px;color:var(--muted);font-size:11px;font-family:var(--mono);text-align:center;">Unsupported video URL</div>`;
|
|
1446
|
+
}
|
|
1447
|
+
el.innerHTML = `
|
|
1448
|
+
<div class="be-video-wrap">${mediaHtml}</div>
|
|
1449
|
+
<div class="be-vid-ctrl">
|
|
1450
|
+
<input class="be-vid-url" type="text" value="${escHtml(url)}" readonly tabindex="-1">
|
|
1451
|
+
<button class="img-ab del" onclick="vidDelete(this)" title="Remove video">✕ remove</button>
|
|
1452
|
+
</div>`;
|
|
1453
|
+
el.addEventListener('click', e=>{
|
|
1454
|
+
if (e.target.closest('.img-ab')) return;
|
|
1455
|
+
e.stopPropagation();
|
|
1456
|
+
beEditor.querySelectorAll('.be-block[data-type="video"]').forEach(b=>b.classList.remove('vid-sel'));
|
|
1457
|
+
el.classList.add('vid-sel');
|
|
1458
|
+
});
|
|
1459
|
+
return el;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
window.vidDelete = function(btn) {
|
|
1463
|
+
const block=btn.closest('.be-block[data-type="video"]'); if(!block) return;
|
|
1464
|
+
const prev=block.previousElementSibling, next=block.nextElementSibling;
|
|
1465
|
+
block.remove();
|
|
1466
|
+
if (next&&next.contentEditable!=='false') focusStart(next);
|
|
1467
|
+
else if (prev&&prev.contentEditable!=='false') focusEnd(prev);
|
|
1468
|
+
else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
|
|
1469
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
document.addEventListener('click', e=>{
|
|
1473
|
+
if (!e.target.closest('.be-block[data-type="video"]'))
|
|
1474
|
+
beEditor.querySelectorAll('.be-block[data-type="video"]').forEach(b=>b.classList.remove('vid-sel'));
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
window.openVideoPicker = function() {
|
|
1478
|
+
vidPickerAnchor = getFocusedBlock();
|
|
1479
|
+
const inp = document.getElementById('vid-url-inp');
|
|
1480
|
+
inp.value = '';
|
|
1481
|
+
document.getElementById('vid-url-overlay').classList.add('open');
|
|
1482
|
+
setTimeout(()=>inp.focus(), 60);
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
window.closeVideoPicker = function() {
|
|
1486
|
+
document.getElementById('vid-url-overlay').classList.remove('open');
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
window.confirmVideoUrl = function() {
|
|
1490
|
+
const url = document.getElementById('vid-url-inp').value.trim();
|
|
1491
|
+
if (!url) return;
|
|
1492
|
+
closeVideoPicker();
|
|
1493
|
+
const block = createVideoBlock(url);
|
|
1494
|
+
const anchor = vidPickerAnchor;
|
|
1495
|
+
if (anchor) {
|
|
1496
|
+
anchor.after(block);
|
|
1497
|
+
if (!block.nextElementSibling||block.nextElementSibling.contentEditable==='false')
|
|
1498
|
+
block.after(createBlock('p',''));
|
|
1499
|
+
if (anchor.innerText.trim()==='') anchor.remove();
|
|
1500
|
+
} else {
|
|
1501
|
+
beEditor.appendChild(block);
|
|
1502
|
+
beEditor.appendChild(createBlock('p',''));
|
|
1503
|
+
}
|
|
1504
|
+
syncToHidden(); scheduleAutosave(); updatePreview();
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
document.getElementById('vid-url-inp').addEventListener('keydown', e=>{
|
|
1508
|
+
if (e.key==='Enter') { e.preventDefault(); confirmVideoUrl(); }
|
|
1509
|
+
if (e.key==='Escape') { e.preventDefault(); closeVideoPicker(); }
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// ── Init ──────────────────────────────────────────────────────────
|
|
1513
|
+
renderMetaPanel();
|
|
1514
|
+
parseMd(IS_NEW ? '' : (entryData?.data?.body ?? ''));
|
|
1515
|
+
syncToHidden(); // serializes blocks → bodyInput, then calls updateWc()
|
|
1516
|
+
updatePreview();
|
|
1517
|
+
if (!IS_NEW) setIndicator('saved');
|
|
1518
|
+
</script>
|
|
1519
|
+
<!-- Video URL dialog -->
|
|
1520
|
+
<div id="vid-url-overlay" class="vid-url-overlay" onclick="if(event.target===this)closeVideoPicker()">
|
|
1521
|
+
<div class="vid-url-box">
|
|
1522
|
+
<div class="vid-url-title">Insert Video</div>
|
|
1523
|
+
<input id="vid-url-inp" class="vid-url-inp" type="url" placeholder="https://youtu.be/… or https://vimeo.com/… or .mp4 URL" />
|
|
1524
|
+
<div class="vid-url-hint">YouTube · Vimeo · Wistia · direct MP4/WebM URL</div>
|
|
1525
|
+
<div class="vid-url-actions">
|
|
1526
|
+
<button class="vid-url-cancel" onclick="closeVideoPicker()">Cancel</button>
|
|
1527
|
+
<button class="vid-url-ok" onclick="confirmVideoUrl()">Insert</button>
|
|
1528
|
+
</div>
|
|
1529
|
+
</div>
|
|
1530
|
+
</div>
|
|
1531
|
+
|
|
1532
|
+
<!-- Image picker sheet -->
|
|
1533
|
+
<div id="img-pick-overlay" class="img-pick-overlay" onclick="if(event.target===this)closeImgPicker()">
|
|
1534
|
+
<div class="img-pick-sheet">
|
|
1535
|
+
<div class="img-pick-head">
|
|
1536
|
+
<span class="img-pick-title">Insert Image</span>
|
|
1537
|
+
<button class="img-pick-upload" onclick="document.getElementById('img-file-inp').click()">↑ Upload</button>
|
|
1538
|
+
<button class="img-pick-close" onclick="closeImgPicker()">×</button>
|
|
1539
|
+
</div>
|
|
1540
|
+
<div class="img-pick-tabs">
|
|
1541
|
+
<button class="img-pick-tab active" onclick="imgPickTab('library',this)">Library</button>
|
|
1542
|
+
<button class="img-pick-tab" onclick="imgPickTab('url',this)">From URL</button>
|
|
1543
|
+
</div>
|
|
1544
|
+
<div id="img-pick-pane-library" class="img-pick-pane active">
|
|
1545
|
+
<div class="img-pick-body">
|
|
1546
|
+
<div id="img-pick-grid" class="img-pick-grid"></div>
|
|
1547
|
+
</div>
|
|
1548
|
+
</div>
|
|
1549
|
+
<div id="img-pick-pane-url" class="img-pick-pane">
|
|
1550
|
+
<div class="img-url-pane">
|
|
1551
|
+
<input id="img-url-inp" class="img-url-inp" type="url" placeholder="https://…" />
|
|
1552
|
+
<div class="img-url-hint">
|
|
1553
|
+
<strong>Dropbox</strong> — Teilen → Link kopieren<br>
|
|
1554
|
+
<strong>Google Drive</strong> — Freigeben → Link abrufen<br>
|
|
1555
|
+
<strong>OneDrive</strong> — Teilen → Link kopieren<br>
|
|
1556
|
+
<strong>Jede öffentliche Bild-URL</strong>
|
|
1557
|
+
</div>
|
|
1558
|
+
<div id="img-url-status" class="img-url-status"></div>
|
|
1559
|
+
<button id="img-url-btn" class="img-url-btn" onclick="importFromUrl()">Importieren</button>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
</div>
|
|
1563
|
+
</div>
|
|
1564
|
+
<input type="file" id="img-file-inp" accept="image/*" style="display:none">
|
|
1565
|
+
|
|
1566
|
+
<script src="/sidebar.js"></script>
|
|
1567
|
+
<script src="/router.js"></script>
|
|
1568
|
+
</body>
|
|
1569
|
+
</html>
|