@lark-apaas/coding-registry-buildless 0.1.2-alpha.0 → 0.1.2-beta.1
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/components/android-frame.jsx +0 -1
- package/components/animations.jsx +179 -86
- package/components/browser-window.jsx +1 -1
- package/components/design-canvas.jsx +221 -97
- package/components/ios-frame.jsx +5 -5
- package/components/macos-window.jsx +1 -1
- package/components/tweaks-panel.jsx +7 -0
- package/package.json +1 -1
- package/scripts/deck-stage.js +563 -371
package/scripts/deck-stage.js
CHANGED
|
@@ -4,25 +4,29 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Handles:
|
|
6
6
|
* (a) speaker notes — reads <script type="application/json" id="speaker-notes">
|
|
7
|
-
*
|
|
7
|
+
* (a JSON array of per-slide note strings), shows the current slide's
|
|
8
|
+
* note in an editable bottom dock (edit mode only; drag its grip to
|
|
9
|
+
* resize, height persisted to localStorage), posts miaoda:deck:slide-changed
|
|
10
|
+
* to the parent on nav, and posts miaoda:deck:notes-changed (the full
|
|
11
|
+
* array) when a note is edited so the host can persist it. The dock is
|
|
12
|
+
* hidden while presenting / in preview / on narrow / noscale / no-rail.
|
|
8
13
|
* (b) keyboard navigation — ←/→, PgUp/PgDn, Space, Home/End, number keys.
|
|
9
14
|
* On touch devices, tapping the left/right half of the stage goes
|
|
10
15
|
* prev/next — taps on links, buttons and other interactive slide
|
|
11
16
|
* content are left alone.
|
|
12
|
-
* (c) press R to reset to slide 0
|
|
13
|
-
* (d)
|
|
14
|
-
* (e) auto-scaling — inner canvas is a fixed design size (default 1920×1080)
|
|
17
|
+
* (c) press R to reset to slide 0.
|
|
18
|
+
* (d) auto-scaling — inner canvas is a fixed design size (default 1920×1080)
|
|
15
19
|
* scaled with `transform: scale()` to fit the viewport, letterboxed.
|
|
16
20
|
* Set the `noscale` attribute to render at authored size (1:1) — the
|
|
17
21
|
* PPTX exporter sets this so its DOM capture sees unscaled geometry.
|
|
18
|
-
* (
|
|
22
|
+
* (e) print — `@media print` lays every slide out as its own page at the
|
|
19
23
|
* design size, so the browser's Print → Save as PDF produces a clean
|
|
20
24
|
* one-page-per-slide PDF with no extra setup.
|
|
21
|
-
* (
|
|
25
|
+
* (f) thumbnail rail — resizable left-hand column of per-slide thumbnails
|
|
22
26
|
* (static clones). Click to navigate; ↑/↓ with a thumbnail focused to
|
|
23
27
|
* step between slides; drag to reorder; right-click for
|
|
24
|
-
* Skip / Move up / Move down / Delete
|
|
25
|
-
*
|
|
28
|
+
* Skip / Move up / Move down / Delete. Drag the rail's right edge
|
|
29
|
+
* to resize; width persists to
|
|
26
30
|
* localStorage. Skipped slides carry `data-deck-skip`, are dimmed in
|
|
27
31
|
* the rail, omitted from prev/next navigation, and hidden at print.
|
|
28
32
|
* The rail is suppressed in presenting mode, in the host's Preview
|
|
@@ -83,9 +87,15 @@
|
|
|
83
87
|
* - Do NOT set position/inset/width/height on the slide <section> elements —
|
|
84
88
|
* the component absolutely positions every slotted child for you.
|
|
85
89
|
* - Entrance animations: make the visible end-state the base style and
|
|
86
|
-
* animate *from* hidden,
|
|
87
|
-
*
|
|
88
|
-
*
|
|
90
|
+
* animate *from* hidden. The thumbnail rail, print, and reduced-motion
|
|
91
|
+
* render each slide's static base state and never fire the animation, so
|
|
92
|
+
* a hidden base (opacity:0 on the rule itself) renders blank there while
|
|
93
|
+
* the live slide looks fine. Keep the hidden state only in the keyframes
|
|
94
|
+
* `from`, and gate the animation on [data-deck-active] and the motion
|
|
95
|
+
* query, e.g.:
|
|
96
|
+
* .fade-up { opacity: 1 }
|
|
97
|
+
* @media (prefers-reduced-motion:no-preference){ [data-deck-active] .fade-up{ animation: fadeUp .5s both } }
|
|
98
|
+
* @keyframes fadeUp { from{ opacity:0; transform:translateY(24px) } to{ opacity:1; transform:none } }
|
|
89
99
|
* Avoid infinite decorative loops on slide content.
|
|
90
100
|
*/
|
|
91
101
|
/* END USAGE */
|
|
@@ -94,7 +104,12 @@
|
|
|
94
104
|
if (typeof window === 'undefined') return;
|
|
95
105
|
const DESIGN_W_DEFAULT = 1920;
|
|
96
106
|
const DESIGN_H_DEFAULT = 1080;
|
|
97
|
-
|
|
107
|
+
// Safe margin around the scaled slide in edit/preview (non-presenting) mode,
|
|
108
|
+
// giving the canvas a floating "card" look with a drop-shadow.
|
|
109
|
+
const DECK_MARGIN = 20;
|
|
110
|
+
// TEMP: presenter-notes dock is hidden for now. Flip back to false to restore
|
|
111
|
+
// the editable speaker-notes panel (all notes logic below stays intact).
|
|
112
|
+
const NOTES_DISABLED = true;
|
|
98
113
|
const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
|
|
99
114
|
const FINE_POINTER_MQ = matchMedia('(hover: hover) and (pointer: fine)');
|
|
100
115
|
const NARROW_MQ = matchMedia('(max-width: 640px)');
|
|
@@ -123,8 +138,7 @@
|
|
|
123
138
|
position: fixed;
|
|
124
139
|
inset: 0;
|
|
125
140
|
display: block;
|
|
126
|
-
background: #
|
|
127
|
-
color: #fff;
|
|
141
|
+
background: #F8F9FA;
|
|
128
142
|
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
129
143
|
overflow: hidden;
|
|
130
144
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -136,7 +150,8 @@
|
|
|
136
150
|
* Only the stage/rail hide — the black :host background stays, so the
|
|
137
151
|
* iframe doesn't flash the page's default white. */
|
|
138
152
|
:host([data-fonts-pending]) .stage,
|
|
139
|
-
:host([data-fonts-pending]) .rail
|
|
153
|
+
:host([data-fonts-pending]) .rail,
|
|
154
|
+
:host([data-fonts-pending]) .notes { opacity: 0; pointer-events: none; }
|
|
140
155
|
|
|
141
156
|
.stage {
|
|
142
157
|
position: absolute;
|
|
@@ -144,8 +159,9 @@
|
|
|
144
159
|
display: flex;
|
|
145
160
|
align-items: center;
|
|
146
161
|
justify-content: center;
|
|
147
|
-
background:
|
|
162
|
+
background: transparent;
|
|
148
163
|
}
|
|
164
|
+
:host(:not([data-windowed])) .stage { background: #000; }
|
|
149
165
|
|
|
150
166
|
.canvas {
|
|
151
167
|
position: relative;
|
|
@@ -154,6 +170,13 @@
|
|
|
154
170
|
background: #fff;
|
|
155
171
|
will-change: transform;
|
|
156
172
|
}
|
|
173
|
+
/* Floating card drop-shadow — only when the stage is windowed (20px
|
|
174
|
+
margin), i.e. edit/preview. Full-bleed presenting / noscale / _snthumb
|
|
175
|
+
carry no shadow (_fit toggles data-windowed). */
|
|
176
|
+
:host([data-windowed]) .canvas {
|
|
177
|
+
border: 0.882px solid #DEE0E3;
|
|
178
|
+
box-shadow: 0 0 7.052px 0 rgba(31, 35, 41, 0.06), 0 0 15.867px 0 rgba(31, 35, 41, 0.04), 0 0 22.919px 7.052px rgba(31, 35, 41, 0.04);
|
|
179
|
+
}
|
|
157
180
|
|
|
158
181
|
/* Slides live in light DOM (via <slot>) so authored CSS still applies.
|
|
159
182
|
We absolutely position each slotted child to stack them. */
|
|
@@ -174,104 +197,6 @@
|
|
|
174
197
|
visibility: visible;
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
.overlay {
|
|
178
|
-
position: fixed;
|
|
179
|
-
left: 50%;
|
|
180
|
-
bottom: 22px;
|
|
181
|
-
transform: translate(-50%, 6px) scale(0.92);
|
|
182
|
-
filter: blur(6px);
|
|
183
|
-
display: flex;
|
|
184
|
-
align-items: center;
|
|
185
|
-
gap: 4px;
|
|
186
|
-
padding: 4px;
|
|
187
|
-
background: #000;
|
|
188
|
-
color: #fff;
|
|
189
|
-
border-radius: 999px;
|
|
190
|
-
font-size: 12px;
|
|
191
|
-
font-feature-settings: "tnum" 1;
|
|
192
|
-
letter-spacing: 0.01em;
|
|
193
|
-
opacity: 0;
|
|
194
|
-
pointer-events: none;
|
|
195
|
-
transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease;
|
|
196
|
-
transform-origin: center bottom;
|
|
197
|
-
z-index: 2147483000;
|
|
198
|
-
user-select: none;
|
|
199
|
-
}
|
|
200
|
-
.overlay[data-visible] {
|
|
201
|
-
opacity: 1;
|
|
202
|
-
pointer-events: auto;
|
|
203
|
-
transform: translate(-50%, 0) scale(1);
|
|
204
|
-
filter: blur(0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.btn {
|
|
208
|
-
appearance: none;
|
|
209
|
-
-webkit-appearance: none;
|
|
210
|
-
background: transparent;
|
|
211
|
-
border: 0;
|
|
212
|
-
margin: 0;
|
|
213
|
-
padding: 0;
|
|
214
|
-
color: inherit;
|
|
215
|
-
font: inherit;
|
|
216
|
-
cursor: default;
|
|
217
|
-
display: inline-flex;
|
|
218
|
-
align-items: center;
|
|
219
|
-
justify-content: center;
|
|
220
|
-
height: 28px;
|
|
221
|
-
min-width: 28px;
|
|
222
|
-
border-radius: 999px;
|
|
223
|
-
color: rgba(255,255,255,0.72);
|
|
224
|
-
transition: background 140ms ease, color 140ms ease;
|
|
225
|
-
-webkit-tap-highlight-color: transparent;
|
|
226
|
-
}
|
|
227
|
-
.btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
|
228
|
-
.btn:active { background: rgba(255,255,255,0.18); }
|
|
229
|
-
.btn:focus { outline: none; }
|
|
230
|
-
.btn:focus-visible { outline: none; }
|
|
231
|
-
.btn::-moz-focus-inner { border: 0; }
|
|
232
|
-
.btn svg { width: 14px; height: 14px; display: block; }
|
|
233
|
-
.btn.reset {
|
|
234
|
-
font-size: 11px;
|
|
235
|
-
font-weight: 500;
|
|
236
|
-
letter-spacing: 0.02em;
|
|
237
|
-
padding: 0 10px 0 12px;
|
|
238
|
-
gap: 6px;
|
|
239
|
-
color: rgba(255,255,255,0.72);
|
|
240
|
-
}
|
|
241
|
-
.btn.reset .kbd {
|
|
242
|
-
display: inline-flex;
|
|
243
|
-
align-items: center;
|
|
244
|
-
justify-content: center;
|
|
245
|
-
min-width: 16px;
|
|
246
|
-
height: 16px;
|
|
247
|
-
padding: 0 4px;
|
|
248
|
-
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
249
|
-
font-size: 10px;
|
|
250
|
-
line-height: 1;
|
|
251
|
-
color: rgba(255,255,255,0.88);
|
|
252
|
-
background: rgba(255,255,255,0.12);
|
|
253
|
-
border-radius: 4px;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.count {
|
|
257
|
-
font-variant-numeric: tabular-nums;
|
|
258
|
-
color: #fff;
|
|
259
|
-
font-weight: 500;
|
|
260
|
-
padding: 0 8px;
|
|
261
|
-
min-width: 42px;
|
|
262
|
-
text-align: center;
|
|
263
|
-
font-size: 12px;
|
|
264
|
-
}
|
|
265
|
-
.count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; }
|
|
266
|
-
.count .total { color: rgba(255,255,255,0.55); }
|
|
267
|
-
|
|
268
|
-
.divider {
|
|
269
|
-
width: 1px;
|
|
270
|
-
height: 14px;
|
|
271
|
-
background: rgba(255,255,255,0.18);
|
|
272
|
-
margin: 0 2px;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
200
|
/* ── Thumbnail rail ──────────────────────────────────────────────────
|
|
276
201
|
Fixed column on the left; each thumbnail is a static deep-clone of
|
|
277
202
|
the light-DOM slide scaled into a 16:9 (or design-aspect) frame. The
|
|
@@ -287,11 +212,12 @@
|
|
|
287
212
|
border-right: 0.5px solid #DEE0E3;
|
|
288
213
|
overflow-y: auto;
|
|
289
214
|
overflow-x: hidden;
|
|
290
|
-
|
|
215
|
+
transform: translate3d(0, 0, 0);
|
|
216
|
+
padding: 20px 15px 20px 10px;
|
|
291
217
|
box-sizing: border-box;
|
|
292
218
|
display: flex;
|
|
293
219
|
flex-direction: column;
|
|
294
|
-
gap:
|
|
220
|
+
gap: 18px;
|
|
295
221
|
z-index: 2147482500;
|
|
296
222
|
scrollbar-width: thin;
|
|
297
223
|
scrollbar-color: rgba(0,0,0,0.18) transparent;
|
|
@@ -319,36 +245,30 @@
|
|
|
319
245
|
popping. Transitions are gated on :host([data-rail-anim]) — set only
|
|
320
246
|
for the 200ms around the toggle — so window-resize and rail-width
|
|
321
247
|
drag (which also call _fit) don't lag behind the cursor. */
|
|
322
|
-
.rail[data-user-hidden] { transform:
|
|
248
|
+
.rail[data-user-hidden] { transform: translate3d(-100%, 0, 0); }
|
|
323
249
|
:host([data-rail-anim]) .rail { transition: transform 200ms cubic-bezier(.3,.7,.4,1); }
|
|
324
250
|
:host([data-rail-anim]) .stage { transition: left 200ms cubic-bezier(.3,.7,.4,1); }
|
|
325
251
|
:host([data-rail-anim]) .canvas { transition: transform 200ms cubic-bezier(.3,.7,.4,1); }
|
|
326
|
-
|
|
327
|
-
.overlay opacity/transform/filter transitions so visibility changes
|
|
328
|
-
during the 200ms toggle window still fade instead of popping. */
|
|
329
|
-
:host([data-rail-anim]) .overlay {
|
|
330
|
-
transition: margin-left 200ms cubic-bezier(.3,.7,.4,1),
|
|
331
|
-
opacity 260ms ease,
|
|
332
|
-
transform 260ms cubic-bezier(.2,.8,.2,1),
|
|
333
|
-
filter 260ms ease;
|
|
334
|
-
}
|
|
252
|
+
:host([data-rail-anim]) .notes { transition: left 200ms cubic-bezier(.3,.7,.4,1); }
|
|
335
253
|
|
|
336
254
|
.thumb {
|
|
337
255
|
position: relative;
|
|
338
256
|
display: flex;
|
|
339
257
|
align-items: flex-start;
|
|
340
|
-
gap:
|
|
258
|
+
gap: 9px;
|
|
341
259
|
cursor: pointer;
|
|
342
260
|
user-select: none;
|
|
343
261
|
}
|
|
262
|
+
.thumb[data-dragging] { cursor: grabbing; }
|
|
344
263
|
.thumb .num {
|
|
345
|
-
width:
|
|
264
|
+
width: 14px;
|
|
346
265
|
flex-shrink: 0;
|
|
347
266
|
font-size: 12px;
|
|
267
|
+
line-height: 1;
|
|
348
268
|
font-weight: 400;
|
|
349
|
-
text-align:
|
|
269
|
+
text-align: center;
|
|
350
270
|
color: #646A73;
|
|
351
|
-
padding-top:
|
|
271
|
+
padding-top: 8px;
|
|
352
272
|
font-variant-numeric: tabular-nums;
|
|
353
273
|
}
|
|
354
274
|
.thumb .frame {
|
|
@@ -357,18 +277,32 @@
|
|
|
357
277
|
min-width: 0;
|
|
358
278
|
aspect-ratio: var(--deck-aspect);
|
|
359
279
|
background: #fff;
|
|
360
|
-
border-radius:
|
|
280
|
+
border-radius: 6px;
|
|
281
|
+
/* Default hairline as a box-shadow, not a border: the frame is the
|
|
282
|
+
absolutely-positioned origin for the cloned slide, so a real border
|
|
283
|
+
would shift the clone; box-shadow doesn't affect layout, so
|
|
284
|
+
_scaleThumbs' frame.offsetWidth read stays correct. */
|
|
285
|
+
box-shadow: 0 0 0 1px #DEE0E3;
|
|
361
286
|
outline: 2px solid transparent;
|
|
362
|
-
outline-offset:
|
|
287
|
+
outline-offset: 2px;
|
|
363
288
|
overflow: hidden;
|
|
364
|
-
transition: outline-color 120ms ease;
|
|
289
|
+
transition: outline-color 120ms ease, box-shadow 120ms ease;
|
|
365
290
|
}
|
|
366
|
-
.thumb:hover .frame { outline-color:
|
|
291
|
+
.thumb:hover .frame { outline-color: #1f23290d; }
|
|
367
292
|
.thumb { outline: none; }
|
|
368
|
-
.thumb:focus-visible .frame { outline-color: #
|
|
369
|
-
.thumb[data-current] .num { color: #1F2329; }
|
|
293
|
+
.thumb:focus-visible .frame { outline-color: #1f23290d; }
|
|
294
|
+
.thumb[data-current] .num { color: #1F2329; font-weight: 500; }
|
|
370
295
|
.thumb[data-current] .frame { outline-color: #1F2329; }
|
|
371
|
-
|
|
296
|
+
/* Elevated "picked up" look on the *stationary* source while dragging
|
|
297
|
+
(outline hidden, card lifted); the ::before drop-line marks the landing
|
|
298
|
+
position. The floating ghost under the cursor is a separate custom
|
|
299
|
+
drag-image built in _makeDragGhost — the browser's native snapshot of
|
|
300
|
+
.thumb clipped this frame's outside-offset ring and carried the number
|
|
301
|
+
gutter, so we no longer let it draw the ghost. */
|
|
302
|
+
.thumb[data-dragging] .frame {
|
|
303
|
+
outline-color: transparent;
|
|
304
|
+
box-shadow: 0 8px 24px rgba(31,35,41,0.22), 0 0 0 1px rgba(31,35,41,0.08);
|
|
305
|
+
}
|
|
372
306
|
.thumb::before {
|
|
373
307
|
content: '';
|
|
374
308
|
position: absolute;
|
|
@@ -382,32 +316,30 @@
|
|
|
382
316
|
}
|
|
383
317
|
.thumb[data-drop="before"]::before { top: -8px; opacity: 1; }
|
|
384
318
|
.thumb[data-drop="after"]::before { bottom: -8px; opacity: 1; }
|
|
385
|
-
.thumb
|
|
386
|
-
|
|
387
|
-
content: '已跳过';
|
|
319
|
+
.thumb .skip-icon {
|
|
320
|
+
display: none;
|
|
388
321
|
position: absolute;
|
|
389
322
|
inset: 0;
|
|
390
|
-
display: flex;
|
|
391
323
|
align-items: center;
|
|
392
324
|
justify-content: center;
|
|
393
|
-
background: rgba(
|
|
394
|
-
|
|
395
|
-
font-size: 10px;
|
|
396
|
-
font-weight: 500;
|
|
397
|
-
letter-spacing: 0.04em;
|
|
325
|
+
background: rgba(255,255,255,0.6);
|
|
326
|
+
z-index: 1;
|
|
398
327
|
}
|
|
328
|
+
.thumb[data-skip] .skip-icon { display: flex; }
|
|
399
329
|
|
|
400
330
|
.ctxmenu {
|
|
401
331
|
position: fixed;
|
|
402
|
-
min-width:
|
|
332
|
+
min-width: 160px;
|
|
403
333
|
padding: 4px;
|
|
404
|
-
background: #
|
|
405
|
-
|
|
406
|
-
border
|
|
407
|
-
|
|
334
|
+
background: #fff;
|
|
335
|
+
color: #1F2329;
|
|
336
|
+
border: 0.5px solid #D2D5D8;
|
|
337
|
+
border-radius: 6px;
|
|
338
|
+
box-shadow: 0 4px 8px -8px rgba(0, 0, 0, 0.06), 0 6px 12px 0 rgba(0, 0, 0, 0.04), 0 8px 24px 8px rgba(0, 0, 0, 0.04);
|
|
408
339
|
z-index: 2147483100;
|
|
409
340
|
display: none;
|
|
410
|
-
font-size:
|
|
341
|
+
font-size: 14px;
|
|
342
|
+
line-height: 22px;
|
|
411
343
|
}
|
|
412
344
|
.ctxmenu[data-open] { display: block; }
|
|
413
345
|
.ctxmenu button {
|
|
@@ -416,19 +348,19 @@
|
|
|
416
348
|
appearance: none;
|
|
417
349
|
border: 0;
|
|
418
350
|
background: transparent;
|
|
419
|
-
color: #
|
|
351
|
+
color: #1F2329;
|
|
420
352
|
font: inherit;
|
|
421
353
|
text-align: left;
|
|
422
|
-
padding: 6px
|
|
354
|
+
padding: 6px 8px;
|
|
423
355
|
border-radius: 4px;
|
|
424
356
|
cursor: pointer;
|
|
425
357
|
}
|
|
426
|
-
.ctxmenu button:hover:not(:disabled) { background:
|
|
358
|
+
.ctxmenu button:hover:not(:disabled) { background: #F2F3F5; }
|
|
427
359
|
.ctxmenu button:disabled { opacity: 0.35; cursor: default; }
|
|
428
360
|
.ctxmenu hr {
|
|
429
361
|
border: 0;
|
|
430
|
-
border-top:
|
|
431
|
-
margin: 4px
|
|
362
|
+
border-top: 0.5px solid #D2D5D8;
|
|
363
|
+
margin: 4px -4px;
|
|
432
364
|
}
|
|
433
365
|
|
|
434
366
|
.rail-resize {
|
|
@@ -448,67 +380,65 @@
|
|
|
448
380
|
.rail[data-presenting] + .rail-resize,
|
|
449
381
|
.rail[data-user-hidden] + .rail-resize { display: none; }
|
|
450
382
|
|
|
451
|
-
/*
|
|
452
|
-
|
|
453
|
-
|
|
383
|
+
/* ── Speaker-notes dock ──────────────────────────────────────────────
|
|
384
|
+
Bottom panel spanning the non-rail width in edit mode: a resize grip
|
|
385
|
+
(drag to set height, persisted to localStorage) + an editable textarea
|
|
386
|
+
bound to the current slide's note. Hidden while presenting / preview /
|
|
387
|
+
narrow / noscale / no-rail / print — same editing-chrome gating as the
|
|
388
|
+
rail. Its left edge is set to the live rail width by _fit. */
|
|
389
|
+
.notes {
|
|
454
390
|
position: fixed;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
max-width: calc(100vw - 32px);
|
|
466
|
-
background: #2a2a2a;
|
|
467
|
-
color: #e8e8e8;
|
|
468
|
-
border: 1px solid rgba(255,255,255,0.12);
|
|
469
|
-
border-radius: 12px;
|
|
470
|
-
box-shadow: 0 12px 32px rgba(0,0,0,0.5);
|
|
471
|
-
overflow: hidden;
|
|
472
|
-
font-family: inherit;
|
|
473
|
-
animation: deck-confirm-in 0.18s ease;
|
|
474
|
-
}
|
|
475
|
-
@keyframes deck-confirm-in {
|
|
476
|
-
from { opacity: 0; transform: scale(0.96); }
|
|
477
|
-
to { opacity: 1; transform: scale(1); }
|
|
478
|
-
}
|
|
479
|
-
.confirm .body { padding: 20px 20px 16px; }
|
|
480
|
-
.confirm .title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
481
|
-
.confirm .msg { font-size: 13px; line-height: 1.5; color: rgba(255,255,255,0.65); }
|
|
482
|
-
.confirm .footer {
|
|
483
|
-
padding: 14px 20px;
|
|
484
|
-
background: #1f1f1f;
|
|
485
|
-
border-top: 1px solid rgba(255,255,255,0.08);
|
|
391
|
+
left: var(--deck-rail-w, 192px);
|
|
392
|
+
right: 20px;
|
|
393
|
+
bottom: 0;
|
|
394
|
+
height: var(--deck-notes-h, 160px);
|
|
395
|
+
background: #fff;
|
|
396
|
+
border: 0.5px solid #D2D5D8;
|
|
397
|
+
border-bottom: none;
|
|
398
|
+
border-radius: 12px 12px 0 0;
|
|
399
|
+
box-shadow: 0 2px 4px -4px rgba(31, 35, 41, 0.02), 0 4px 8px 0 rgba(31, 35, 41, 0.02), 0 4px 16px 4px rgba(31, 35, 41, 0.03);
|
|
400
|
+
box-sizing: border-box;
|
|
486
401
|
display: flex;
|
|
487
|
-
|
|
488
|
-
|
|
402
|
+
flex-direction: column;
|
|
403
|
+
z-index: 2147482400;
|
|
489
404
|
}
|
|
490
|
-
.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
405
|
+
:host([no-rail]) .notes,
|
|
406
|
+
:host([noscale]) .notes { display: none; }
|
|
407
|
+
.notes[data-hidden] { display: none; }
|
|
408
|
+
@media (max-width: 640px) { .notes { display: none; } }
|
|
409
|
+
.notes-handle {
|
|
410
|
+
flex-shrink: 0;
|
|
411
|
+
height: 20px;
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
justify-content: center;
|
|
415
|
+
cursor: row-resize;
|
|
416
|
+
color: #8F959E;
|
|
417
|
+
font-size: 18px;
|
|
418
|
+
line-height: 20px;
|
|
419
|
+
letter-spacing: 2px;
|
|
420
|
+
user-select: none;
|
|
421
|
+
touch-action: none;
|
|
498
422
|
}
|
|
499
|
-
.
|
|
500
|
-
|
|
423
|
+
.notes-handle:hover,
|
|
424
|
+
.notes-handle[data-dragging] { color: #646A73; }
|
|
425
|
+
.notes-body {
|
|
426
|
+
flex: 1;
|
|
427
|
+
min-height: 0;
|
|
428
|
+
width: 100%;
|
|
429
|
+
box-sizing: border-box;
|
|
501
430
|
border: 0;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
431
|
+
outline: none;
|
|
432
|
+
resize: none;
|
|
433
|
+
background: transparent;
|
|
434
|
+
color: #1F2329;
|
|
435
|
+
font: inherit;
|
|
436
|
+
font-size: 14px;
|
|
437
|
+
line-height: 1.6;
|
|
438
|
+
padding: 4px 24px 12px;
|
|
439
|
+
overflow-y: auto;
|
|
510
440
|
}
|
|
511
|
-
.
|
|
441
|
+
.notes-body::placeholder { color: #8F959E; }
|
|
512
442
|
|
|
513
443
|
/* ── Print: one page per slide, no chrome ────────────────────────────
|
|
514
444
|
The screen layout stacks every slide at inset:0 inside a scaled
|
|
@@ -532,6 +462,9 @@
|
|
|
532
462
|
height: auto !important;
|
|
533
463
|
background: none;
|
|
534
464
|
will-change: auto;
|
|
465
|
+
/* !important: :host([data-windowed]) .canvas (0,2,0) outranks a bare
|
|
466
|
+
.canvas rule inside this media query. */
|
|
467
|
+
box-shadow: none !important;
|
|
535
468
|
}
|
|
536
469
|
::slotted(*) {
|
|
537
470
|
position: relative !important;
|
|
@@ -557,7 +490,7 @@
|
|
|
557
490
|
page-break-after: auto;
|
|
558
491
|
}
|
|
559
492
|
::slotted([data-deck-skip]) { display: none !important; }
|
|
560
|
-
.
|
|
493
|
+
.rail, .rail-resize, .ctxmenu, .notes { display: none !important; }
|
|
561
494
|
}
|
|
562
495
|
`;
|
|
563
496
|
|
|
@@ -570,16 +503,15 @@
|
|
|
570
503
|
this._index = 0;
|
|
571
504
|
this._slides = [];
|
|
572
505
|
this._notes = [];
|
|
573
|
-
this._hideTimer = null;
|
|
574
|
-
this._mouseIdleTimer = null;
|
|
575
506
|
this._menuIndex = -1;
|
|
507
|
+
this._notesPersistTimer = null;
|
|
576
508
|
|
|
577
509
|
this._onKey = this._onKey.bind(this);
|
|
578
510
|
this._onResize = this._onResize.bind(this);
|
|
579
511
|
this._onSlotChange = this._onSlotChange.bind(this);
|
|
580
|
-
this._onMouseMove = this._onMouseMove.bind(this);
|
|
581
512
|
this._onTap = this._onTap.bind(this);
|
|
582
513
|
this._onMessage = this._onMessage.bind(this);
|
|
514
|
+
this._onHashChange = this._onHashChange.bind(this);
|
|
583
515
|
// Capture-phase close so a click anywhere dismisses the menu, but
|
|
584
516
|
// ignore clicks that land inside the menu itself — otherwise the
|
|
585
517
|
// capture handler runs before the menu's own (bubble) handler and
|
|
@@ -598,17 +530,22 @@
|
|
|
598
530
|
}
|
|
599
531
|
|
|
600
532
|
connectedCallback() {
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
// (
|
|
604
|
-
|
|
533
|
+
// Capture modes that render a clean full-bleed single slide, both routed
|
|
534
|
+
// through the same no-rail path: the host's pre-screenshot ?thumbnail=1
|
|
535
|
+
// (product thumbnail — slide 1) and the presenter-view popup's
|
|
536
|
+
// ?_snthumb=...#N (prev/cur/next thumbnails). Both drop the rail (wrong
|
|
537
|
+
// scale, offsets the stage into a gutter) and render full-bleed — no
|
|
538
|
+
// windowed margin/shadow, see _fit. A bare load with no #N hash starts at
|
|
539
|
+
// slide 1, so ?thumbnail=1 alone yields a clean first-slide frame.
|
|
540
|
+
this._snthumb = /[?&](_snthumb|thumbnail)=/.test(location.search);
|
|
541
|
+
if (this._snthumb) this.setAttribute('no-rail', '');
|
|
605
542
|
this._render();
|
|
606
543
|
this._loadNotes();
|
|
607
544
|
this._syncPrintPageRule();
|
|
608
545
|
window.addEventListener('keydown', this._onKey);
|
|
609
546
|
window.addEventListener('resize', this._onResize);
|
|
610
|
-
window.addEventListener('mousemove', this._onMouseMove, { passive: true });
|
|
611
547
|
window.addEventListener('message', this._onMessage);
|
|
548
|
+
window.addEventListener('hashchange', this._onHashChange);
|
|
612
549
|
window.addEventListener('click', this._onDocClick, true);
|
|
613
550
|
this.addEventListener('click', this._onTap);
|
|
614
551
|
// Print lays every slide out as its own page, so [data-deck-active]-
|
|
@@ -625,7 +562,7 @@
|
|
|
625
562
|
this._slides.forEach((s) => s.setAttribute('data-deck-active', ''));
|
|
626
563
|
};
|
|
627
564
|
this._onAfterPrint = () => {
|
|
628
|
-
this._applyIndex({
|
|
565
|
+
this._applyIndex({ broadcast: false });
|
|
629
566
|
if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
|
|
630
567
|
};
|
|
631
568
|
window.addEventListener('beforeprint', this._onBeforePrint);
|
|
@@ -648,6 +585,8 @@
|
|
|
648
585
|
new Promise((r) => setTimeout(r, 2000)),
|
|
649
586
|
]).then(reveal, reveal);
|
|
650
587
|
});
|
|
588
|
+
// Announce to the host that this product IS a deck scene.
|
|
589
|
+
try { window.parent.postMessage({ type: 'miaoda:deck:available' }, '*'); } catch (e) {}
|
|
651
590
|
}
|
|
652
591
|
|
|
653
592
|
_enableRail() {
|
|
@@ -724,26 +663,86 @@
|
|
|
724
663
|
this._syncRailHidden();
|
|
725
664
|
this._renderRail();
|
|
726
665
|
this._fit();
|
|
666
|
+
// The notes dock is editing chrome too — show it and load the current
|
|
667
|
+
// slide's note now that the rail feature is enabled.
|
|
668
|
+
this._syncNotesHidden();
|
|
669
|
+
this._syncNotesBody();
|
|
727
670
|
}
|
|
728
671
|
|
|
729
672
|
/** Snapshot document stylesheets into a constructable sheet that each
|
|
730
673
|
* thumbnail's nested shadow root adopts — so author CSS styles the
|
|
731
674
|
* cloned slide content without touching this component's chrome.
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
675
|
+
* Re-callable: the constructable sheet is reused via replaceSync so
|
|
676
|
+
* every already-adopted shadow root picks up the fresh CSS.
|
|
677
|
+
*
|
|
678
|
+
* Cross-origin sheets (a CDN styles.css) throw SecurityError on
|
|
679
|
+
* .cssRules — the rules style the light DOM but CSSOM won't expose them,
|
|
680
|
+
* so the snapshot comes back empty and thumbnail clones render unstyled.
|
|
681
|
+
* Re-fetch those sheets' source and re-apply when it lands; the shared
|
|
682
|
+
* adopted sheet means one replaceSync restyles every clone (no re-clone).
|
|
683
|
+
* The fetch is CORS-gated — a CDN without Access-Control-Allow-Origin
|
|
684
|
+
* fails it and we degrade to an unstyled clone, no worse than before. */
|
|
735
685
|
_snapshotAuthorCss() {
|
|
736
|
-
// :
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
686
|
+
// Generation guard: a stale fetch (from a snapshot a newer call has
|
|
687
|
+
// superseded) must not clobber the current adopted CSS.
|
|
688
|
+
const gen = (this._cssGen = (this._cssGen || 0) + 1);
|
|
689
|
+
if (!this._xoCssCache) this._xoCssCache = new Map(); // href → fetched text ('' = failed)
|
|
690
|
+
const pending = [];
|
|
691
|
+
this._applyAuthorCss(this._collectAuthorCss(pending));
|
|
692
|
+
if (!pending.length) return;
|
|
693
|
+
Promise.all(pending.map((href) =>
|
|
694
|
+
fetch(href).then((r) => (r.ok ? r.text() : '')).catch(() => '')
|
|
695
|
+
.then((text) => { this._xoCssCache.set(href, text); })
|
|
696
|
+
)).then(() => {
|
|
697
|
+
if (this._cssGen !== gen) return;
|
|
698
|
+
this._applyAuthorCss(this._collectAuthorCss(null));
|
|
699
|
+
if (this._adoptedSheet) {
|
|
700
|
+
// Shared sheet already restyled every clone — just re-mirror vars.
|
|
701
|
+
const cs = getComputedStyle(this);
|
|
702
|
+
(this._thumbs || []).forEach((t) => { if (t.host) this._syncThumbHostAttrs(t.host, cs); });
|
|
703
|
+
} else {
|
|
704
|
+
// Fallback (no constructable sheets): each clone baked its own
|
|
705
|
+
// <style> at materialize, so re-clone to pick up the fetched CSS.
|
|
706
|
+
(this._thumbs || []).forEach((t) => { if (t.host) this._refreshThumb(t.slide); });
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Concatenate every document stylesheet's rules in document order. A
|
|
712
|
+
* cross-origin sheet whose .cssRules throws is unreadable via CSSOM:
|
|
713
|
+
* substitute its re-fetched source if cached, else push its href to
|
|
714
|
+
* pendingOut (when provided) and leave a placeholder until the fetch lands. */
|
|
715
|
+
_collectAuthorCss(pendingOut) {
|
|
716
|
+
return Array.from(document.styleSheets).map((sh) => {
|
|
743
717
|
try {
|
|
744
718
|
return Array.from(sh.cssRules).map((r) => r.cssText).join('\n');
|
|
745
|
-
} catch (e) {
|
|
746
|
-
|
|
719
|
+
} catch (e) {
|
|
720
|
+
const href = sh.href;
|
|
721
|
+
if (!href) return '';
|
|
722
|
+
if (this._xoCssCache.has(href)) return this._xoCssCache.get(href);
|
|
723
|
+
if (pendingOut) pendingOut.push(href);
|
|
724
|
+
return '';
|
|
725
|
+
}
|
|
726
|
+
}).join('\n');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Rewrite raw author CSS so its document-scoped selectors match inside a
|
|
730
|
+
* thumbnail's shadow root, then (re)load it into the shared adopted sheet.
|
|
731
|
+
* :root in an adopted sheet inside a shadow root matches nothing (only the
|
|
732
|
+
* document root qualifies), so author rules like
|
|
733
|
+
* `:root[data-voice="modern"] .serif` never reach the clones. Rewrite
|
|
734
|
+
* :root → :host and mirror <html>'s data-*, class and lang onto each thumb
|
|
735
|
+
* host (see _syncThumbHostAttrs) so the same selectors match inside it.
|
|
736
|
+
* The <deck-stage> type selector gets the same treatment: from a clone's
|
|
737
|
+
* point of view inside an isolated thumb shadow root there is no real
|
|
738
|
+
* <deck-stage> ancestor, so a rule like `deck-stage section { background }`
|
|
739
|
+
* (the common per-slide default-background idiom) would never match and
|
|
740
|
+
* the clone renders with a transparent background — through which the
|
|
741
|
+
* page-level html/body background (rewritten to :host above) leaks. The
|
|
742
|
+
* thumb's own host IS the deck-stage from the clone's perspective, so
|
|
743
|
+
* alias deck-stage → :host to restore the match. */
|
|
744
|
+
_applyAuthorCss(rawCss) {
|
|
745
|
+
const authorCss = rawCss
|
|
747
746
|
// The shadow host is featureless outside the functional :host(...)
|
|
748
747
|
// form, so any compound on :root — [attr], .class, #id, :pseudo —
|
|
749
748
|
// must become :host(<compound>) not :host<compound>. Same for the
|
|
@@ -753,7 +752,12 @@
|
|
|
753
752
|
.replace(/:root((?:\[[^\]]*\]|[.#][-\w]+|:[-\w]+(?:\([^)]*\))?)+)/g, ':host($1)')
|
|
754
753
|
.replace(/:root\b/g, ':host')
|
|
755
754
|
.replace(/(^|[\s,>~+(}])html((?:\[[^\]]*\]|[.#][-\w]+|:[-\w]+(?:\([^)]*\))?)+)(?![-\w])/g, '$1:host($2)')
|
|
756
|
-
.replace(/(^|[\s,>~+(}])html(?![-\w])/g, '$1:host')
|
|
755
|
+
.replace(/(^|[\s,>~+(}])html(?![-\w])/g, '$1:host')
|
|
756
|
+
// Same rewrite for the deck-stage type selector (see the doc comment).
|
|
757
|
+
// The negative-lookahead (?![-\w]) keeps `.deck-stage-title` and the
|
|
758
|
+
// `--deck-stage-*` custom-property names untouched.
|
|
759
|
+
.replace(/(^|[\s,>~+(}])deck-stage((?:\[[^\]]*\]|[.#][-\w]+|:[-\w]+(?:\([^)]*\))?)+)(?![-\w])/g, '$1:host($2)')
|
|
760
|
+
.replace(/(^|[\s,>~+(}])deck-stage(?![-\w])/g, '$1:host');
|
|
757
761
|
// Every custom property the author references. _syncThumbHostAttrs
|
|
758
762
|
// mirrors each one's *computed* value at <deck-stage> onto the
|
|
759
763
|
// thumb host so the live value wins over the :host default above
|
|
@@ -814,18 +818,18 @@
|
|
|
814
818
|
disconnectedCallback() {
|
|
815
819
|
window.removeEventListener('keydown', this._onKey);
|
|
816
820
|
window.removeEventListener('resize', this._onResize);
|
|
817
|
-
window.removeEventListener('mousemove', this._onMouseMove);
|
|
818
821
|
window.removeEventListener('message', this._onMessage);
|
|
822
|
+
window.removeEventListener('hashchange', this._onHashChange);
|
|
819
823
|
window.removeEventListener('click', this._onDocClick, true);
|
|
820
824
|
window.removeEventListener('beforeprint', this._onBeforePrint);
|
|
821
825
|
window.removeEventListener('afterprint', this._onAfterPrint);
|
|
822
826
|
if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
|
|
827
|
+
if (this._dragGhost) { this._dragGhost.remove(); this._dragGhost = null; }
|
|
823
828
|
this.removeEventListener('click', this._onTap);
|
|
824
|
-
if (this._hideTimer) clearTimeout(this._hideTimer);
|
|
825
|
-
if (this._mouseIdleTimer) clearTimeout(this._mouseIdleTimer);
|
|
826
829
|
if (this._liveTimer) clearTimeout(this._liveTimer);
|
|
827
830
|
if (this._tweakTimer) clearTimeout(this._tweakTimer);
|
|
828
831
|
if (this._railAnimTimer) clearTimeout(this._railAnimTimer);
|
|
832
|
+
if (this._notesPersistTimer) clearTimeout(this._notesPersistTimer);
|
|
829
833
|
if (this._scaleRaf) cancelAnimationFrame(this._scaleRaf);
|
|
830
834
|
if (this._liveObserver) this._liveObserver.disconnect();
|
|
831
835
|
if (this._railObserver) this._railObserver.disconnect();
|
|
@@ -866,28 +870,6 @@
|
|
|
866
870
|
canvas.appendChild(slot);
|
|
867
871
|
stage.appendChild(canvas);
|
|
868
872
|
|
|
869
|
-
// Overlay: compact, solid black, with clickable controls.
|
|
870
|
-
const overlay = document.createElement('div');
|
|
871
|
-
overlay.className = 'overlay export-hidden';
|
|
872
|
-
overlay.setAttribute('role', 'toolbar');
|
|
873
|
-
overlay.setAttribute('aria-label', 'Deck controls');
|
|
874
|
-
overlay.setAttribute('data-miaoda-chrome', '');
|
|
875
|
-
overlay.innerHTML = `
|
|
876
|
-
<button class="btn prev" type="button" aria-label="Previous slide" title="上一页(←)">
|
|
877
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3L5 8l5 5"/></svg>
|
|
878
|
-
</button>
|
|
879
|
-
<span class="count" aria-live="polite"><span class="current">1</span><span class="sep">/</span><span class="total">1</span></span>
|
|
880
|
-
<button class="btn next" type="button" aria-label="Next slide" title="下一页(→)">
|
|
881
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
882
|
-
</button>
|
|
883
|
-
<span class="divider"></span>
|
|
884
|
-
<button class="btn reset" type="button" aria-label="Reset to first slide" title="重置(R)">重置<span class="kbd">R</span></button>
|
|
885
|
-
`;
|
|
886
|
-
|
|
887
|
-
overlay.querySelector('.prev').addEventListener('click', () => this._advance(-1, 'click'));
|
|
888
|
-
overlay.querySelector('.next').addEventListener('click', () => this._advance(1, 'click'));
|
|
889
|
-
overlay.querySelector('.reset').addEventListener('click', () => this._go(0, 'click'));
|
|
890
|
-
|
|
891
873
|
// Thumbnail rail + context menu. Thumbnails are populated in
|
|
892
874
|
// _renderRail() after _collectSlides().
|
|
893
875
|
const rail = document.createElement('div');
|
|
@@ -900,23 +882,51 @@
|
|
|
900
882
|
// (ramped by edge proximity) is enough — no rAF loop needed.
|
|
901
883
|
rail.addEventListener('dragover', (e) => {
|
|
902
884
|
if (this._dragFrom == null) return;
|
|
885
|
+
// preventDefault makes the whole rail — including the gaps between
|
|
886
|
+
// thumbs, where the drop-line actually renders — a valid drop target.
|
|
887
|
+
// Without it a release in a gap is rejected and the slide snaps back
|
|
888
|
+
// even though the indicator was showing.
|
|
889
|
+
e.preventDefault();
|
|
890
|
+
e.dataTransfer.dropEffect = 'move';
|
|
903
891
|
const r = rail.getBoundingClientRect();
|
|
904
892
|
const EDGE = 40;
|
|
905
893
|
const dt = e.clientY - r.top;
|
|
906
894
|
const db = r.bottom - e.clientY;
|
|
907
895
|
if (dt < EDGE) rail.scrollTop -= Math.ceil((EDGE - dt) / 3);
|
|
908
896
|
else if (db < EDGE) rail.scrollTop += Math.ceil((EDGE - db) / 3);
|
|
897
|
+
// Over a thumb its own dragover set the indicator (O(1)); over a gap
|
|
898
|
+
// no thumb handler fires, so resolve the nearest slot here.
|
|
899
|
+
const overThumb = e.target && e.target.closest && e.target.closest('.thumb');
|
|
900
|
+
if (!overThumb) {
|
|
901
|
+
const tgt = this._railDropTarget(e.clientY);
|
|
902
|
+
if (tgt) this._setDrop(tgt.i, tgt.pos);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
rail.addEventListener('drop', (e) => {
|
|
906
|
+
// A drop over a thumb is handled by the thumb's own handler, which
|
|
907
|
+
// nulls _dragFrom before the event bubbles here — so this branch only
|
|
908
|
+
// runs for releases that land in a gap / the rail padding. It commits
|
|
909
|
+
// to whatever slot the indicator is currently showing.
|
|
910
|
+
if (this._dragFrom == null) return;
|
|
911
|
+
e.preventDefault();
|
|
912
|
+
const on = this._dropOn;
|
|
913
|
+
const from = this._dragFrom;
|
|
914
|
+
this._dragFrom = null;
|
|
915
|
+
let to = on ? (this._dropWhere === 'after' ? on.i + 1 : on.i) : from;
|
|
916
|
+
if (from < to) to--;
|
|
917
|
+
this._clearDrop();
|
|
918
|
+
if (on && to !== from) this._moveSlide(from, to);
|
|
909
919
|
});
|
|
910
920
|
|
|
911
921
|
const menu = document.createElement('div');
|
|
912
922
|
menu.className = 'ctxmenu export-hidden';
|
|
913
923
|
menu.setAttribute('data-miaoda-chrome', '');
|
|
914
924
|
menu.innerHTML = `
|
|
915
|
-
<button type="button" data-act="skip"
|
|
925
|
+
<button type="button" data-act="skip">隐藏此页</button>
|
|
916
926
|
<button type="button" data-act="up">上移</button>
|
|
917
927
|
<button type="button" data-act="down">下移</button>
|
|
918
928
|
<hr>
|
|
919
|
-
<button type="button" data-act="delete"
|
|
929
|
+
<button type="button" data-act="delete">删除</button>
|
|
920
930
|
`;
|
|
921
931
|
menu.addEventListener('click', (e) => {
|
|
922
932
|
const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
|
|
@@ -926,7 +936,7 @@
|
|
|
926
936
|
if (act === 'skip') this._toggleSkip(i);
|
|
927
937
|
else if (act === 'up') this._moveSlide(i, i - 1);
|
|
928
938
|
else if (act === 'down') this._moveSlide(i, i + 1);
|
|
929
|
-
else if (act === 'delete') this.
|
|
939
|
+
else if (act === 'delete') this._deleteSlide(i);
|
|
930
940
|
});
|
|
931
941
|
menu.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
932
942
|
|
|
@@ -952,43 +962,64 @@
|
|
|
952
962
|
resize.addEventListener('pointercancel', up);
|
|
953
963
|
});
|
|
954
964
|
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
965
|
+
// Speaker-notes dock — a resize grip + an editable textarea bound to the
|
|
966
|
+
// current slide's note. Chrome (export-hidden / data-miaoda-chrome) like
|
|
967
|
+
// the rail/overlay so it's excluded from PPTX/screenshot capture.
|
|
968
|
+
const notes = document.createElement('div');
|
|
969
|
+
notes.className = 'notes export-hidden';
|
|
970
|
+
notes.setAttribute('data-miaoda-chrome', '');
|
|
971
|
+
const notesHandle = document.createElement('div');
|
|
972
|
+
notesHandle.className = 'notes-handle';
|
|
973
|
+
notesHandle.innerHTML = '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.3008 11.4434C13.3008 10.822 13.8045 10.3184 14.4258 10.3184C15.047 10.3185 15.5508 10.8221 15.5508 11.4434C15.5508 12.0646 15.047 12.5682 14.4258 12.5684C13.8045 12.5684 13.3008 12.0647 13.3008 11.4434ZM13.3008 6.55664C13.3008 5.93532 13.8045 5.43164 14.4258 5.43164C15.047 5.43179 15.5508 5.93541 15.5508 6.55664C15.5508 7.17787 15.047 7.68149 14.4258 7.68164C13.8045 7.68164 13.3008 7.17796 13.3008 6.55664ZM7.875 11.4434C7.875 10.822 8.37868 10.3184 9 10.3184C9.62132 10.3184 10.125 10.822 10.125 11.4434C10.125 12.0647 9.62132 12.5684 9 12.5684C8.37868 12.5684 7.875 12.0647 7.875 11.4434ZM7.875 6.55664C7.875 5.93532 8.37868 5.43164 9 5.43164C9.62132 5.43164 10.125 5.93532 10.125 6.55664C10.125 7.17796 9.62132 7.68164 9 7.68164C8.37868 7.68164 7.875 7.17796 7.875 6.55664ZM2.44922 11.4434C2.44922 10.822 2.9529 10.3184 3.57422 10.3184C4.19554 10.3184 4.69922 10.822 4.69922 11.4434C4.69922 12.0647 4.19554 12.5684 3.57422 12.5684C2.9529 12.5684 2.44922 12.0647 2.44922 11.4434ZM2.44922 6.55664C2.44922 5.93532 2.9529 5.43164 3.57422 5.43164C4.19554 5.43164 4.69922 5.93532 4.69922 6.55664C4.69922 7.17796 4.19554 7.68164 3.57422 7.68164C2.9529 7.68164 2.44922 7.17796 2.44922 6.55664Z" fill="currentColor"/></svg>';
|
|
974
|
+
notesHandle.setAttribute('title', '拖动调整高度');
|
|
975
|
+
const notesBody = document.createElement('textarea');
|
|
976
|
+
notesBody.className = 'notes-body';
|
|
977
|
+
notesBody.setAttribute('placeholder', '点击添加演示者备注');
|
|
978
|
+
notesBody.setAttribute('aria-label', '演示者备注');
|
|
979
|
+
notesBody.setAttribute('spellcheck', 'false');
|
|
980
|
+
notes.append(notesHandle, notesBody);
|
|
981
|
+
|
|
982
|
+
// Drag the grip to resize the dock height (mirror the rail-resize
|
|
983
|
+
// pattern); persist to localStorage so it survives reloads.
|
|
984
|
+
notesHandle.addEventListener('pointerdown', (e) => {
|
|
985
|
+
e.preventDefault();
|
|
986
|
+
notesHandle.setPointerCapture(e.pointerId);
|
|
987
|
+
notesHandle.setAttribute('data-dragging', '');
|
|
988
|
+
const move = (ev) => this._setNotesHeight(window.innerHeight - ev.clientY);
|
|
989
|
+
const up = () => {
|
|
990
|
+
notesHandle.removeEventListener('pointermove', move);
|
|
991
|
+
notesHandle.removeEventListener('pointerup', up);
|
|
992
|
+
notesHandle.removeEventListener('pointercancel', up);
|
|
993
|
+
notesHandle.removeAttribute('data-dragging');
|
|
994
|
+
try { localStorage.setItem('deck-stage.notesHeight', String(this._notesPx)); } catch (err) {}
|
|
995
|
+
};
|
|
996
|
+
notesHandle.addEventListener('pointermove', move);
|
|
997
|
+
notesHandle.addEventListener('pointerup', up);
|
|
998
|
+
notesHandle.addEventListener('pointercancel', up);
|
|
973
999
|
});
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
this.
|
|
978
|
-
this.
|
|
1000
|
+
|
|
1001
|
+
// Edit → write the current slide's note + debounced persist.
|
|
1002
|
+
notesBody.addEventListener('input', () => {
|
|
1003
|
+
if (!Array.isArray(this._notes)) this._notes = [];
|
|
1004
|
+
this._notes[this._index] = notesBody.value;
|
|
1005
|
+
this._scheduleNotesPersist();
|
|
979
1006
|
});
|
|
1007
|
+
// At window scope the keydown target is retargeted to the host
|
|
1008
|
+
// <deck-stage> (not this textarea), so _onKey's INPUT/TEXTAREA guard
|
|
1009
|
+
// never matches — stop propagation here so typing (space/arrows/…) edits
|
|
1010
|
+
// the note instead of navigating slides (same trick as the thumb keydown).
|
|
1011
|
+
notesBody.addEventListener('keydown', (e) => e.stopPropagation());
|
|
980
1012
|
|
|
981
|
-
this._root.append(style, rail, resize, stage,
|
|
1013
|
+
this._root.append(style, rail, resize, stage, notes, menu);
|
|
982
1014
|
this._canvas = canvas;
|
|
983
1015
|
this._stage = stage;
|
|
984
1016
|
this._slot = slot;
|
|
985
|
-
this._overlay = overlay;
|
|
986
1017
|
this._rail = rail;
|
|
987
1018
|
this._resize = resize;
|
|
988
1019
|
this._menu = menu;
|
|
989
|
-
this.
|
|
990
|
-
this.
|
|
991
|
-
this.
|
|
1020
|
+
this._notesPanel = notes;
|
|
1021
|
+
this._notesHandle = notesHandle;
|
|
1022
|
+
this._notesBody = notesBody;
|
|
992
1023
|
|
|
993
1024
|
// Restore persisted rail width.
|
|
994
1025
|
let rw = 192;
|
|
@@ -998,6 +1029,15 @@
|
|
|
998
1029
|
} catch (err) {}
|
|
999
1030
|
this._setRailWidth(rw);
|
|
1000
1031
|
this._syncRailHidden();
|
|
1032
|
+
|
|
1033
|
+
// Restore persisted notes-dock height.
|
|
1034
|
+
let nh = 160;
|
|
1035
|
+
try {
|
|
1036
|
+
const s = localStorage.getItem('deck-stage.notesHeight');
|
|
1037
|
+
if (s) nh = parseInt(s, 10) || nh;
|
|
1038
|
+
} catch (err) {}
|
|
1039
|
+
this._setNotesHeight(nh);
|
|
1040
|
+
this._syncNotesHidden();
|
|
1001
1041
|
}
|
|
1002
1042
|
|
|
1003
1043
|
_setRailWidth(px) {
|
|
@@ -1016,6 +1056,74 @@
|
|
|
1016
1056
|
}
|
|
1017
1057
|
}
|
|
1018
1058
|
|
|
1059
|
+
// ── Speaker-notes dock ────────────────────────────────────────────────
|
|
1060
|
+
|
|
1061
|
+
_setNotesHeight(px) {
|
|
1062
|
+
const max = Math.max(120, Math.round(window.innerHeight * 0.5));
|
|
1063
|
+
const h = Math.max(58, Math.min(max, Math.round(px)));
|
|
1064
|
+
this._notesPx = h;
|
|
1065
|
+
this.style.setProperty('--deck-notes-h', h + 'px');
|
|
1066
|
+
this._fit();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
_notesVisible() {
|
|
1070
|
+
if (NOTES_DISABLED) return false;
|
|
1071
|
+
return !!this._hasNotesTag && !!this._railEnabled
|
|
1072
|
+
&& !this._presenting && !this._previewMode
|
|
1073
|
+
&& !this.hasAttribute('noscale') && !this.hasAttribute('no-rail')
|
|
1074
|
+
&& !NARROW_MQ.matches;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Height _fit subtracts from the stage region. Clamped so a tall dock on
|
|
1078
|
+
* a short window still leaves ~160px of canvas (no negative/NaN scale). */
|
|
1079
|
+
_notesHeight() {
|
|
1080
|
+
if (!this._notesVisible()) return 0;
|
|
1081
|
+
return Math.min(this._notesPx || 0, Math.max(0, window.innerHeight - 160));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
_syncNotesHidden() {
|
|
1085
|
+
if (!this._notesPanel) return;
|
|
1086
|
+
// Static conditions (no-rail/noscale/narrow/print) live in CSS; toggle
|
|
1087
|
+
// only the runtime-dynamic hide (presenting / preview / rail disabled).
|
|
1088
|
+
const hide = NOTES_DISABLED || !this._hasNotesTag || !this._railEnabled || this._presenting || this._previewMode;
|
|
1089
|
+
this._notesPanel.toggleAttribute('data-hidden', hide);
|
|
1090
|
+
this._notesPanel.inert = !this._notesVisible(); // keep textarea out of tab order when hidden
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Load the current slide's note into the body (no-op if unchanged so it
|
|
1094
|
+
* doesn't disturb an in-progress edit). */
|
|
1095
|
+
_syncNotesBody() {
|
|
1096
|
+
if (!this._notesBody) return;
|
|
1097
|
+
const val = (Array.isArray(this._notes) && this._notes[this._index]) || '';
|
|
1098
|
+
if (this._notesBody.value !== val) this._notesBody.value = val;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** this._notes with holes → '' and length preserved (safe to JSON). */
|
|
1102
|
+
_notesForEmit() {
|
|
1103
|
+
const src = Array.isArray(this._notes) ? this._notes : [];
|
|
1104
|
+
return Array.from(src, (v) => (typeof v === 'string' ? v : (v == null ? '' : String(v))));
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
_scheduleNotesPersist() {
|
|
1108
|
+
clearTimeout(this._notesPersistTimer);
|
|
1109
|
+
this._notesPersistTimer = setTimeout(() => this._emitNotesChange(), 300);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Persist a pure text edit: mirror the in-DOM #speaker-notes tag (if any)
|
|
1113
|
+
* for in-iframe consumers, and post the full array to the parent so the
|
|
1114
|
+
* host writes it to disk — the tag lives OUTSIDE <deck-stage>, so the
|
|
1115
|
+
* deck-changed outerHTML path can't carry notes. */
|
|
1116
|
+
_emitNotesChange() {
|
|
1117
|
+
const notes = this._notesForEmit();
|
|
1118
|
+
this._notes = notes;
|
|
1119
|
+
const tag = document.getElementById('speaker-notes');
|
|
1120
|
+
if (tag) tag.textContent = JSON.stringify(notes);
|
|
1121
|
+
this.dispatchEvent(new CustomEvent('noteschange', {
|
|
1122
|
+
detail: { notes, index: this._index }, bubbles: true, composed: true,
|
|
1123
|
+
}));
|
|
1124
|
+
try { window.parent.postMessage({ type: 'miaoda:deck:notes-changed', index: this._index, notes }, '*'); } catch (e) {}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1019
1127
|
/** @page must live in the document stylesheet — it's a no-op inside
|
|
1020
1128
|
* shadow DOM. Inject/update a single <head> style tag so the print
|
|
1021
1129
|
* sheet matches the design size and Save-as-PDF yields one slide per
|
|
@@ -1047,7 +1155,7 @@
|
|
|
1047
1155
|
if (this._squelchSlotChange) { this._squelchSlotChange = false; return; }
|
|
1048
1156
|
this._collectSlides();
|
|
1049
1157
|
this._restoreIndex();
|
|
1050
|
-
this._applyIndex({
|
|
1158
|
+
this._applyIndex({ broadcast: true, reason: 'init' });
|
|
1051
1159
|
this._fit();
|
|
1052
1160
|
}
|
|
1053
1161
|
|
|
@@ -1072,7 +1180,6 @@
|
|
|
1072
1180
|
slide.setAttribute('data-deck-slide', String(i));
|
|
1073
1181
|
});
|
|
1074
1182
|
|
|
1075
|
-
if (this._totalEl) this._totalEl.textContent = String(this._slides.length || 1);
|
|
1076
1183
|
if (this._index >= this._slides.length) this._index = Math.max(0, this._slides.length - 1);
|
|
1077
1184
|
this._markLastVisible();
|
|
1078
1185
|
this._renderRail();
|
|
@@ -1092,6 +1199,7 @@
|
|
|
1092
1199
|
|
|
1093
1200
|
_loadNotes() {
|
|
1094
1201
|
const tag = document.getElementById('speaker-notes');
|
|
1202
|
+
this._hasNotesTag = !!tag;
|
|
1095
1203
|
if (!tag) { this._notes = []; return; }
|
|
1096
1204
|
try {
|
|
1097
1205
|
const parsed = JSON.parse(tag.textContent || '[]');
|
|
@@ -1102,18 +1210,38 @@
|
|
|
1102
1210
|
}
|
|
1103
1211
|
}
|
|
1104
1212
|
|
|
1213
|
+
/** Parse a `#<int>` fragment (1-indexed) into a 0-based slide index, or
|
|
1214
|
+
* -1 when the hash is absent/malformed or points outside the deck.
|
|
1215
|
+
* Shared by _restoreIndex (load time) and _onHashChange (runtime). */
|
|
1216
|
+
_slideFromHash() {
|
|
1217
|
+
const h = (location.hash || '').match(/^#(\d+)$/);
|
|
1218
|
+
if (!h) return -1;
|
|
1219
|
+
const n = parseInt(h[1], 10) - 1;
|
|
1220
|
+
return n >= 0 && n < this._slides.length ? n : -1;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1105
1223
|
_restoreIndex() {
|
|
1106
1224
|
// The host's ?slide= param is delivered as a #<int> hash (1-indexed) on
|
|
1107
1225
|
// the iframe src. No hash → slide 1; the deck itself keeps no position
|
|
1108
1226
|
// state across loads.
|
|
1109
|
-
const
|
|
1110
|
-
if (
|
|
1111
|
-
const n = parseInt(h[1], 10) - 1;
|
|
1112
|
-
if (n >= 0 && n < this._slides.length) this._index = n;
|
|
1113
|
-
}
|
|
1227
|
+
const n = this._slideFromHash();
|
|
1228
|
+
if (n >= 0) this._index = n;
|
|
1114
1229
|
}
|
|
1115
1230
|
|
|
1116
|
-
|
|
1231
|
+
_onHashChange() {
|
|
1232
|
+
// Runtime counterpart to _restoreIndex, which only reads the fragment
|
|
1233
|
+
// once (at connectedCallback). A same-document fragment change — a deep
|
|
1234
|
+
// link edited in the address bar, or an automation/screenshot tool that
|
|
1235
|
+
// navigates to #N without a full reload — fires hashchange but never
|
|
1236
|
+
// re-runs connectedCallback, so honor it here too. The deck's own
|
|
1237
|
+
// navigation writes the fragment via history.replaceState, which does
|
|
1238
|
+
// NOT fire hashchange, so this can't feed back into a loop; the index
|
|
1239
|
+
// guard also makes a redundant fragment a no-op.
|
|
1240
|
+
const n = this._slideFromHash();
|
|
1241
|
+
if (n >= 0 && n !== this._index) this._go(n, 'api');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
_applyIndex({ broadcast = true, reason = 'init' } = {}) {
|
|
1117
1245
|
if (!this._slides.length) return;
|
|
1118
1246
|
const prev = this._prevIndex == null ? -1 : this._prevIndex;
|
|
1119
1247
|
const curr = this._index;
|
|
@@ -1125,13 +1253,13 @@
|
|
|
1125
1253
|
if (i === curr) s.setAttribute('data-deck-active', '');
|
|
1126
1254
|
else s.removeAttribute('data-deck-active');
|
|
1127
1255
|
});
|
|
1128
|
-
if (this._countEl) this._countEl.textContent = String(curr + 1);
|
|
1129
1256
|
// Follow-scroll on every navigation (init deep-link, keyboard, click,
|
|
1130
1257
|
// tap, external goTo) — the only time we *don't* want the rail to
|
|
1131
1258
|
// track current is after a rail-internal mutation, where _renderRail
|
|
1132
1259
|
// has already restored the user's scroll position and yanking back to
|
|
1133
1260
|
// current would undo it.
|
|
1134
1261
|
this._syncRail(reason !== 'mutation');
|
|
1262
|
+
this._syncNotesBody();
|
|
1135
1263
|
|
|
1136
1264
|
if (broadcast) {
|
|
1137
1265
|
// (1) Parent-window postMessage for speaker-notes renderers.
|
|
@@ -1158,19 +1286,6 @@
|
|
|
1158
1286
|
}
|
|
1159
1287
|
|
|
1160
1288
|
this._prevIndex = curr;
|
|
1161
|
-
if (showOverlay) this._flashOverlay();
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
_flashOverlay() {
|
|
1165
|
-
// Host posts miaoda:deck:presenting while in fullscreen/tab presentation
|
|
1166
|
-
// mode — suppress the nav footer entirely (both hover and slide-change
|
|
1167
|
-
// flash) so the audience sees clean slides.
|
|
1168
|
-
if (!this._overlay || this._presenting) return;
|
|
1169
|
-
this._overlay.setAttribute('data-visible', '');
|
|
1170
|
-
if (this._hideTimer) clearTimeout(this._hideTimer);
|
|
1171
|
-
this._hideTimer = setTimeout(() => {
|
|
1172
|
-
this._overlay.removeAttribute('data-visible');
|
|
1173
|
-
}, OVERLAY_HIDE_MS);
|
|
1174
1289
|
}
|
|
1175
1290
|
|
|
1176
1291
|
_railWidth() {
|
|
@@ -1192,18 +1307,30 @@
|
|
|
1192
1307
|
// resetTransformSelector can't reach .canvas.style.transform directly.
|
|
1193
1308
|
if (this.hasAttribute('noscale')) {
|
|
1194
1309
|
this._canvas.style.transform = 'none';
|
|
1195
|
-
|
|
1196
|
-
if (
|
|
1310
|
+
this.removeAttribute('data-windowed');
|
|
1311
|
+
if (stage) { stage.style.left = '0'; stage.style.right = '0'; stage.style.top = '0'; stage.style.bottom = '0'; }
|
|
1197
1312
|
return;
|
|
1198
1313
|
}
|
|
1199
1314
|
const rw = this._railWidth();
|
|
1200
|
-
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1315
|
+
const nh = this._notesHeight();
|
|
1316
|
+
// Windowed (20px margin + card shadow) in edit/preview; full-bleed while
|
|
1317
|
+
// presenting or inside presenter-thumbnail iframes (_snthumb).
|
|
1318
|
+
const windowed = !this._presenting && !this._snthumb;
|
|
1319
|
+
const m = windowed ? DECK_MARGIN : 0;
|
|
1320
|
+
this.toggleAttribute('data-windowed', windowed);
|
|
1321
|
+
// Explicit four-side inset shrinks the flex box to the safe region; the
|
|
1322
|
+
// design-size canvas centres in it, then scale() shrinks it around the
|
|
1323
|
+
// centre — so it stays inside the inset with equal margins on all sides.
|
|
1324
|
+
if (stage) {
|
|
1325
|
+
stage.style.left = (rw + m) + 'px';
|
|
1326
|
+
stage.style.right = m + 'px';
|
|
1327
|
+
stage.style.top = m + 'px';
|
|
1328
|
+
stage.style.bottom = (m + nh) + 'px';
|
|
1329
|
+
}
|
|
1330
|
+
// The notes dock spans the non-rail width; its left edge tracks the rail.
|
|
1331
|
+
if (this._notesPanel) this._notesPanel.style.left = (rw + DECK_MARGIN) + 'px';
|
|
1332
|
+
const vw = window.innerWidth - rw - 2 * m;
|
|
1333
|
+
const vh = window.innerHeight - nh - 2 * m;
|
|
1207
1334
|
const s = Math.min(vw / this.designWidth, vh / this.designHeight);
|
|
1208
1335
|
this._canvas.style.transform = `scale(${s})`;
|
|
1209
1336
|
}
|
|
@@ -1220,22 +1347,13 @@
|
|
|
1220
1347
|
}
|
|
1221
1348
|
}
|
|
1222
1349
|
|
|
1223
|
-
_onMouseMove() {
|
|
1224
|
-
// Keep overlay visible while mouse moves; hide after idle.
|
|
1225
|
-
this._flashOverlay();
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
1350
|
_onMessage(e) {
|
|
1229
1351
|
const d = e.data;
|
|
1230
1352
|
if (d && d.type === 'miaoda:deck:presenting' && typeof d.on === 'boolean') {
|
|
1231
1353
|
this._presenting = d.on;
|
|
1232
|
-
if (this._presenting && this._overlay) {
|
|
1233
|
-
this._overlay.removeAttribute('data-visible');
|
|
1234
|
-
if (this._hideTimer) clearTimeout(this._hideTimer);
|
|
1235
|
-
}
|
|
1236
1354
|
this._syncRailHidden();
|
|
1355
|
+
this._syncNotesHidden();
|
|
1237
1356
|
this._closeMenu();
|
|
1238
|
-
this._closeConfirm();
|
|
1239
1357
|
this._fit();
|
|
1240
1358
|
this._scaleThumbs();
|
|
1241
1359
|
}
|
|
@@ -1248,8 +1366,8 @@
|
|
|
1248
1366
|
if (d.on === this._previewMode) return;
|
|
1249
1367
|
this._previewMode = d.on;
|
|
1250
1368
|
this._syncRailHidden();
|
|
1369
|
+
this._syncNotesHidden();
|
|
1251
1370
|
this._closeMenu();
|
|
1252
|
-
this._closeConfirm();
|
|
1253
1371
|
this._fit();
|
|
1254
1372
|
this._scaleThumbs();
|
|
1255
1373
|
}
|
|
@@ -1295,8 +1413,11 @@
|
|
|
1295
1413
|
}
|
|
1296
1414
|
|
|
1297
1415
|
_onTap(e) {
|
|
1298
|
-
// Touch-only
|
|
1299
|
-
|
|
1416
|
+
// Touch-only for normal browse/edit — on desktop there, keyboard + the
|
|
1417
|
+
// overlay toolbar cover nav. But in presenting mode the overlay is hidden
|
|
1418
|
+
// and click-to-advance is the expected gesture (Keynote/PPT), so allow it
|
|
1419
|
+
// on fine pointers too.
|
|
1420
|
+
if (FINE_POINTER_MQ.matches && !this._presenting) return;
|
|
1300
1421
|
// Only taps that land on the stage (slide content or letterbox); the
|
|
1301
1422
|
// overlay / rail / menus are siblings with their own click handlers.
|
|
1302
1423
|
const path = e.composedPath();
|
|
@@ -1320,13 +1441,6 @@
|
|
|
1320
1441
|
// Ignore when the user is typing.
|
|
1321
1442
|
const t = e.target;
|
|
1322
1443
|
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
1323
|
-
// Confirm dialog swallows nav keys while open; Escape cancels. Enter
|
|
1324
|
-
// is left to the focused button's native activation so Tab→Cancel
|
|
1325
|
-
// →Enter activates Cancel, not the window-level confirm path.
|
|
1326
|
-
if (this._confirm && this._confirm.hasAttribute('data-open')) {
|
|
1327
|
-
if (e.key === 'Escape') { this._closeConfirm(); e.preventDefault(); }
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
1444
|
if (e.key === 'Escape' && this._menu && this._menu.hasAttribute('data-open')) {
|
|
1331
1445
|
this._closeMenu();
|
|
1332
1446
|
e.preventDefault();
|
|
@@ -1337,9 +1451,9 @@
|
|
|
1337
1451
|
const key = e.key;
|
|
1338
1452
|
let handled = true;
|
|
1339
1453
|
|
|
1340
|
-
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'Spacebar') {
|
|
1454
|
+
if (key === 'ArrowRight' || key === 'ArrowDown' || key === 'PageDown' || key === ' ' || key === 'Spacebar') {
|
|
1341
1455
|
this._advance(1, 'keyboard');
|
|
1342
|
-
} else if (key === 'ArrowLeft' || key === 'PageUp') {
|
|
1456
|
+
} else if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'PageUp') {
|
|
1343
1457
|
this._advance(-1, 'keyboard');
|
|
1344
1458
|
} else if (key === 'Home') {
|
|
1345
1459
|
this._go(0, 'keyboard');
|
|
@@ -1357,31 +1471,26 @@
|
|
|
1357
1471
|
|
|
1358
1472
|
if (handled) {
|
|
1359
1473
|
e.preventDefault();
|
|
1360
|
-
this._flashOverlay();
|
|
1361
1474
|
}
|
|
1362
1475
|
}
|
|
1363
1476
|
|
|
1364
1477
|
_go(i, reason = 'api') {
|
|
1365
1478
|
if (!this._slides.length) return;
|
|
1366
1479
|
const clamped = Math.max(0, Math.min(this._slides.length - 1, i));
|
|
1367
|
-
if (clamped === this._index)
|
|
1368
|
-
this._flashOverlay();
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1480
|
+
if (clamped === this._index) return;
|
|
1371
1481
|
this._index = clamped;
|
|
1372
|
-
this._applyIndex({
|
|
1482
|
+
this._applyIndex({ broadcast: true, reason });
|
|
1373
1483
|
}
|
|
1374
1484
|
|
|
1375
|
-
/** Step forward/back skipping any slide marked data-deck-skip.
|
|
1376
|
-
*
|
|
1377
|
-
* nothing further in that direction. */
|
|
1485
|
+
/** Step forward/back skipping any slide marked data-deck-skip. Clamps at
|
|
1486
|
+
* the ends (no-op past the first/last non-skipped slide). */
|
|
1378
1487
|
_advance(dir, reason) {
|
|
1379
1488
|
if (!this._slides.length) return;
|
|
1380
1489
|
let i = this._index + dir;
|
|
1381
1490
|
while (i >= 0 && i < this._slides.length && this._slides[i].hasAttribute('data-deck-skip')) {
|
|
1382
1491
|
i += dir;
|
|
1383
1492
|
}
|
|
1384
|
-
if (i < 0 || i >= this._slides.length)
|
|
1493
|
+
if (i < 0 || i >= this._slides.length) return;
|
|
1385
1494
|
this._go(i, reason);
|
|
1386
1495
|
}
|
|
1387
1496
|
|
|
@@ -1476,6 +1585,10 @@
|
|
|
1476
1585
|
num.className = 'num';
|
|
1477
1586
|
const frame = document.createElement('div');
|
|
1478
1587
|
frame.className = 'frame';
|
|
1588
|
+
const skipIcon = document.createElement('div');
|
|
1589
|
+
skipIcon.className = 'skip-icon';
|
|
1590
|
+
skipIcon.innerHTML = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.8447 1.82978C2.16723 1.50736 2.68943 1.51112 3.0119 1.8336L18.3385 17.1606C18.6638 17.4859 18.6637 18.0136 18.3384 18.3389C18.0131 18.6641 17.4855 18.6643 17.1602 18.339L1.83317 3.01234C1.51069 2.68986 1.50692 2.16767 1.82935 1.84514C1.83447 1.84002 1.83958 1.8349 1.8447 1.82978ZM3.33085 6.27753L6.68668 9.63337C6.63272 10.1199 6.68658 10.6123 6.84443 11.0757C7.00228 11.5391 7.26025 11.962 7.60001 12.3144C7.93976 12.6668 8.35296 12.94 8.81024 13.1147C9.26753 13.2894 9.75767 13.3612 10.2458 13.325L10.3667 13.3134L13.3529 16.2996C12.2833 16.8225 11.1604 17.0838 9.98626 17.0838C6.65746 17.0838 3.75395 14.9833 1.27539 10.7819C0.991215 10.3003 0.987158 9.70269 1.26629 9.21806C1.91897 8.08486 2.60738 7.10484 3.33085 6.27753ZM13.3333 10.0005C13.3334 9.54197 13.2389 9.08841 13.0556 8.66813C12.8724 8.24784 12.6045 7.86987 12.2686 7.55784C11.9327 7.24581 11.536 7.00644 11.1033 6.85468C10.6707 6.70293 10.2114 6.64205 9.75418 6.67587L9.63293 6.68712L6.6246 3.67878C7.67308 3.17422 8.82227 2.91381 9.98585 2.91712C13.3783 2.91712 16.2919 5.01573 18.7276 9.21295C19.0101 9.6999 19.0068 10.3018 18.7193 10.7858C18.0552 11.9035 17.3618 12.8724 16.6388 13.693L13.3133 10.3675C13.3267 10.2467 13.3333 10.1246 13.3333 10.0005Z" fill="#646A73"/></svg>';
|
|
1591
|
+
frame.append(skipIcon);
|
|
1479
1592
|
thumb.append(num, frame);
|
|
1480
1593
|
|
|
1481
1594
|
const entry = { thumb, num, frame, slide, clone: null, host: null, i: -1 };
|
|
@@ -1508,11 +1621,23 @@
|
|
|
1508
1621
|
thumb.setAttribute('data-dragging', '');
|
|
1509
1622
|
e.dataTransfer.effectAllowed = 'move';
|
|
1510
1623
|
try { e.dataTransfer.setData('text/plain', String(this._dragFrom)); } catch (err) {}
|
|
1624
|
+
// Hand the browser a clean elevated card instead of its raw .thumb
|
|
1625
|
+
// bitmap; anchor it under the cursor at the grab point (no jump).
|
|
1626
|
+
try {
|
|
1627
|
+
const g = this._makeDragGhost(entry);
|
|
1628
|
+
if (g) {
|
|
1629
|
+
const r = frame.getBoundingClientRect();
|
|
1630
|
+
const gx = g.pad + Math.max(0, Math.min(g.w, e.clientX - r.left));
|
|
1631
|
+
const gy = g.pad + Math.max(0, Math.min(g.h, e.clientY - r.top));
|
|
1632
|
+
e.dataTransfer.setDragImage(g.el, gx, gy);
|
|
1633
|
+
}
|
|
1634
|
+
} catch (err) {}
|
|
1511
1635
|
});
|
|
1512
1636
|
thumb.addEventListener('dragend', () => {
|
|
1513
1637
|
thumb.removeAttribute('data-dragging');
|
|
1514
1638
|
this._clearDrop();
|
|
1515
1639
|
this._dragFrom = null;
|
|
1640
|
+
if (this._dragGhost) { this._dragGhost.remove(); this._dragGhost = null; }
|
|
1516
1641
|
});
|
|
1517
1642
|
thumb.addEventListener('dragover', (e) => {
|
|
1518
1643
|
if (this._dragFrom == null) return;
|
|
@@ -1539,6 +1664,51 @@
|
|
|
1539
1664
|
return entry;
|
|
1540
1665
|
}
|
|
1541
1666
|
|
|
1667
|
+
/** Build the floating drag-image for a rail thumb.
|
|
1668
|
+
* The browser's default drag-image is a raw bitmap of the whole .thumb:
|
|
1669
|
+
* it includes the number gutter and clips the current-slide's dark ring
|
|
1670
|
+
* (which sits 2px outside the frame via outline-offset) at the bitmap
|
|
1671
|
+
* edge. Instead we hand setDragImage a purpose-built card — just the
|
|
1672
|
+
* slide frame, rounded, with a soft drop-shadow, transparent around it.
|
|
1673
|
+
* The card lives inside a transparent padded wrapper because the bitmap
|
|
1674
|
+
* crops to the element's border-box, so the shadow needs room inside the
|
|
1675
|
+
* box or it clips too. Returns { el, pad, w, h } for the anchor math, or
|
|
1676
|
+
* null if the frame has no size yet. */
|
|
1677
|
+
_makeDragGhost(entry) {
|
|
1678
|
+
if (this._dragGhost) { this._dragGhost.remove(); this._dragGhost = null; }
|
|
1679
|
+
this._materialize(entry);
|
|
1680
|
+
const w = entry.frame.offsetWidth, h = entry.frame.offsetHeight;
|
|
1681
|
+
if (!w || !h) return null;
|
|
1682
|
+
const pad = 36; // ≥ the shadow's blur + offset so it stays in the bitmap
|
|
1683
|
+
const wrap = document.createElement('div');
|
|
1684
|
+
wrap.style.cssText =
|
|
1685
|
+
'position:fixed;top:0;left:-100000px;pointer-events:none;' +
|
|
1686
|
+
'background:transparent;box-sizing:content-box;padding:' + pad + 'px;';
|
|
1687
|
+
const card = document.createElement('div');
|
|
1688
|
+
// Drop-shadow only — no hairline ring (the 0 0 0 1px shadow reads as a
|
|
1689
|
+
// border, which we don't want on the floating card).
|
|
1690
|
+
card.style.cssText =
|
|
1691
|
+
'position:relative;width:' + w + 'px;height:' + h + 'px;background:#fff;' +
|
|
1692
|
+
'border-radius:6px;overflow:hidden;' +
|
|
1693
|
+
'box-shadow:0 6px 10px 0 rgba(31, 35, 41, 0.20);cursor:grabbing;';
|
|
1694
|
+
const host = document.createElement('div');
|
|
1695
|
+
host.style.cssText = 'position:absolute;inset:0;';
|
|
1696
|
+
this._syncThumbHostAttrs(host);
|
|
1697
|
+
const sr = host.attachShadow({ mode: 'open' });
|
|
1698
|
+
if (this._adoptedSheet) sr.adoptedStyleSheets = [this._adoptedSheet];
|
|
1699
|
+
else {
|
|
1700
|
+
const st = document.createElement('style');
|
|
1701
|
+
st.textContent = this._authorCss || '';
|
|
1702
|
+
sr.appendChild(st);
|
|
1703
|
+
}
|
|
1704
|
+
if (entry.clone) sr.appendChild(entry.clone.cloneNode(true));
|
|
1705
|
+
card.appendChild(host);
|
|
1706
|
+
wrap.appendChild(card);
|
|
1707
|
+
(this.shadowRoot || document.body).appendChild(wrap);
|
|
1708
|
+
this._dragGhost = wrap;
|
|
1709
|
+
return { el: wrap, pad, w, h };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1542
1712
|
/** Lazily build the clone for a thumb that has scrolled into view. */
|
|
1543
1713
|
_materialize(entry) {
|
|
1544
1714
|
if (entry.host) return;
|
|
@@ -1670,6 +1840,25 @@
|
|
|
1670
1840
|
});
|
|
1671
1841
|
}
|
|
1672
1842
|
|
|
1843
|
+
/** Nearest reorder slot for a Y coordinate anywhere in the rail — used when
|
|
1844
|
+
* the pointer is over a gap, where no thumb dragover fires. Returns
|
|
1845
|
+
* { i, pos } matching _setDrop's contract, or null. Mirrors the thumb
|
|
1846
|
+
* handler's midpoint test so the gap indicator lines up with the thumbs. */
|
|
1847
|
+
_railDropTarget(clientY) {
|
|
1848
|
+
const ts = this._thumbs;
|
|
1849
|
+
if (!ts || !ts.length) return null;
|
|
1850
|
+
for (let k = 0; k < ts.length; k++) {
|
|
1851
|
+
const r = ts[k].thumb.getBoundingClientRect();
|
|
1852
|
+
if (clientY < r.top + r.height / 2) return { i: ts[k].i, pos: 'before' };
|
|
1853
|
+
const next = ts[k + 1];
|
|
1854
|
+
if (!next) return { i: ts[k].i, pos: 'after' };
|
|
1855
|
+
const nr = next.thumb.getBoundingClientRect();
|
|
1856
|
+
if (clientY < nr.top + nr.height / 2) return { i: ts[k].i, pos: 'after' };
|
|
1857
|
+
}
|
|
1858
|
+
const last = ts[ts.length - 1];
|
|
1859
|
+
return { i: last.i, pos: 'after' };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1673
1862
|
_setDrop(i, where) {
|
|
1674
1863
|
// dragover fires at pointer-event rate; touch only the previous
|
|
1675
1864
|
// and new target rather than sweeping all N thumbs.
|
|
@@ -1679,11 +1868,15 @@
|
|
|
1679
1868
|
}
|
|
1680
1869
|
if (t) t.thumb.setAttribute('data-drop', where);
|
|
1681
1870
|
this._dropOn = t || null;
|
|
1871
|
+
// Remembered so a drop that lands in a gap (rail handler) can commit to
|
|
1872
|
+
// the slot the indicator is showing without re-deriving before/after.
|
|
1873
|
+
this._dropWhere = t ? where : null;
|
|
1682
1874
|
}
|
|
1683
1875
|
|
|
1684
1876
|
_clearDrop() {
|
|
1685
1877
|
if (this._dropOn) this._dropOn.thumb.removeAttribute('data-drop');
|
|
1686
1878
|
this._dropOn = null;
|
|
1879
|
+
this._dropWhere = null;
|
|
1687
1880
|
}
|
|
1688
1881
|
|
|
1689
1882
|
_syncRail(follow) {
|
|
@@ -1705,7 +1898,7 @@
|
|
|
1705
1898
|
this._menuIndex = i;
|
|
1706
1899
|
const slide = this._slides[i];
|
|
1707
1900
|
const skip = slide && slide.hasAttribute('data-deck-skip');
|
|
1708
|
-
this._menu.querySelector('[data-act="skip"]').textContent = skip ? '
|
|
1901
|
+
this._menu.querySelector('[data-act="skip"]').textContent = skip ? '取消隐藏' : '隐藏此页';
|
|
1709
1902
|
this._menu.querySelector('[data-act="up"]').disabled = i <= 0;
|
|
1710
1903
|
this._menu.querySelector('[data-act="down"]').disabled = i >= this._slides.length - 1;
|
|
1711
1904
|
this._menu.querySelector('[data-act="delete"]').disabled = this._slides.length <= 1;
|
|
@@ -1725,21 +1918,12 @@
|
|
|
1725
1918
|
this._menuIndex = -1;
|
|
1726
1919
|
}
|
|
1727
1920
|
|
|
1728
|
-
_openConfirm(i) {
|
|
1729
|
-
if (!this._confirm) return;
|
|
1730
|
-
this._confirmIndex = i;
|
|
1731
|
-
this._confirm.querySelector('.title').textContent = '删除第 ' + (i + 1) + ' 页?';
|
|
1732
|
-
this._confirm.setAttribute('data-open', '');
|
|
1733
|
-
const btn = this._confirm.querySelector('.danger');
|
|
1734
|
-
if (btn && btn.focus) btn.focus();
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
_closeConfirm() {
|
|
1738
|
-
if (this._confirm) this._confirm.removeAttribute('data-open');
|
|
1739
|
-
this._confirmIndex = -1;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
1921
|
_emitDeckChange(detail) {
|
|
1922
|
+
// A structural edit carries the (possibly reindexed) notes array so the
|
|
1923
|
+
// host writes deck-stage + speaker-notes in ONE file write. Cancel any
|
|
1924
|
+
// pending text-edit notes-changed so the two never race two writes at
|
|
1925
|
+
// the same file (etag conflict would drop the second).
|
|
1926
|
+
clearTimeout(this._notesPersistTimer);
|
|
1743
1927
|
this.dispatchEvent(new CustomEvent('deckchange', {
|
|
1744
1928
|
detail, bubbles: true, composed: true,
|
|
1745
1929
|
}));
|
|
@@ -1751,6 +1935,7 @@
|
|
|
1751
1935
|
type: 'miaoda:deck:deck-changed',
|
|
1752
1936
|
action: detail.action, from: detail.from, to: detail.to,
|
|
1753
1937
|
html: this.outerHTML,
|
|
1938
|
+
notes: this._notesForEmit(),
|
|
1754
1939
|
}, '*');
|
|
1755
1940
|
} catch (e) {}
|
|
1756
1941
|
}
|
|
@@ -1762,9 +1947,11 @@
|
|
|
1762
1947
|
if (i < this._index || (wasCurrent && i === this._slides.length - 1)) this._index--;
|
|
1763
1948
|
this._squelchSlotChange = true;
|
|
1764
1949
|
slide.remove();
|
|
1950
|
+
// Keep the per-slide-indexed notes array aligned with the deck.
|
|
1951
|
+
if (Array.isArray(this._notes)) this._notes.splice(i, 1);
|
|
1765
1952
|
this._emitDeckChange({ action: 'delete', from: i, slide });
|
|
1766
1953
|
this._collectSlides();
|
|
1767
|
-
this._applyIndex({
|
|
1954
|
+
this._applyIndex({ broadcast: true, reason: 'mutation' });
|
|
1768
1955
|
}
|
|
1769
1956
|
|
|
1770
1957
|
_toggleSkip(i) {
|
|
@@ -1804,9 +1991,14 @@
|
|
|
1804
1991
|
else if (i > cur && j <= cur) this._index = cur + 1;
|
|
1805
1992
|
this._squelchSlotChange = true;
|
|
1806
1993
|
this.insertBefore(slide, ref);
|
|
1994
|
+
// Move the note entry in lockstep so notes stay aligned to slides.
|
|
1995
|
+
if (Array.isArray(this._notes)) {
|
|
1996
|
+
const [n] = this._notes.splice(i, 1);
|
|
1997
|
+
this._notes.splice(j, 0, n === undefined ? '' : n);
|
|
1998
|
+
}
|
|
1807
1999
|
this._emitDeckChange({ action: 'move', from: i, to: j, slide });
|
|
1808
2000
|
this._collectSlides();
|
|
1809
|
-
this._applyIndex({
|
|
2001
|
+
this._applyIndex({ broadcast: true, reason: 'mutation' });
|
|
1810
2002
|
}
|
|
1811
2003
|
|
|
1812
2004
|
// Public API ------------------------------------------------------------
|