@blockslides/extension-add-slide-button 0.1.0 → 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/dist/index.cjs +275 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +275 -26
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/add-slide-button.ts +327 -31
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockslides/extension-add-slide-button",
|
|
3
3
|
"description": "add slide button extension for blockslides",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"author": "keivanmojmali",
|
|
6
6
|
"homepage": "https://github.com/keivanmojmali/blockslides",
|
|
7
7
|
"keywords": [
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"dist"
|
|
30
30
|
],
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@blockslides/
|
|
33
|
-
"@blockslides/
|
|
32
|
+
"@blockslides/pm": "^0.1.0",
|
|
33
|
+
"@blockslides/core": "^0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@blockslides/core": "^0.
|
|
36
|
+
"@blockslides/core": "^0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/add-slide-button.ts
CHANGED
|
@@ -6,6 +6,17 @@ import type { EditorView } from "@blockslides/pm/view";
|
|
|
6
6
|
|
|
7
7
|
//TODO: Add ability to easy choose a layout type like 1-1, 1-1-1, etc.
|
|
8
8
|
|
|
9
|
+
export interface PresetTemplateOption {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
build: () => any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Heroicons outline "window" to match requested default
|
|
17
|
+
const defaultTemplateIcon =
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 5.25A1.75 1.75 0 0 1 6.25 3.5h11.5A1.75 1.75 0 0 1 19.5 5.25v13.5a1.75 1.75 0 0 1-1.75 1.75H6.25A1.75 1.75 0 0 1 4.5 18.75Z"/><path d="M4.5 8.25h15M9 3.5v4.75"/></svg>';
|
|
19
|
+
|
|
9
20
|
export interface AddSlideButtonOptions {
|
|
10
21
|
/**
|
|
11
22
|
* Whether to inject CSS styles for the button.
|
|
@@ -32,6 +43,30 @@ export interface AddSlideButtonOptions {
|
|
|
32
43
|
* @example '+' | 'Add Slide' | '➕' | '<svg>...</svg>'
|
|
33
44
|
*/
|
|
34
45
|
content: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional template chooser that renders a second button and modal.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
showPresets?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Background color for the preset modal content area.
|
|
53
|
+
* @default '#ffffff'
|
|
54
|
+
*/
|
|
55
|
+
presetBackground?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Text/icon color for the preset modal content area.
|
|
58
|
+
* @default '#000000'
|
|
59
|
+
*/
|
|
60
|
+
presetForeground?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Presets to show in the modal when showPresets is true.
|
|
63
|
+
*/
|
|
64
|
+
presets?: PresetTemplateOption[];
|
|
65
|
+
/**
|
|
66
|
+
* Content for the template button (emoji or HTML).
|
|
67
|
+
* @default '✨'
|
|
68
|
+
*/
|
|
69
|
+
templateButtonContent?: string;
|
|
35
70
|
/**
|
|
36
71
|
* Custom click handler for the button.
|
|
37
72
|
* If not provided, will insert a new slide at the clicked position.
|
|
@@ -54,6 +89,7 @@ class SlideNodeView implements NodeView {
|
|
|
54
89
|
view: EditorView;
|
|
55
90
|
getPos: () => number | undefined;
|
|
56
91
|
node: ProseMirrorNode;
|
|
92
|
+
presetModal?: HTMLElement | null;
|
|
57
93
|
|
|
58
94
|
constructor(
|
|
59
95
|
node: ProseMirrorNode,
|
|
@@ -95,24 +131,34 @@ class SlideNodeView implements NodeView {
|
|
|
95
131
|
this.contentDOM.setAttribute("data-node-type", "slide");
|
|
96
132
|
}
|
|
97
133
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
134
|
+
const createButton = (html: string) => {
|
|
135
|
+
const btn = document.createElement("button");
|
|
136
|
+
btn.className = "add-slide-button";
|
|
137
|
+
btn.innerHTML = html;
|
|
138
|
+
btn.setAttribute("type", "button");
|
|
139
|
+
btn.contentEditable = "false";
|
|
140
|
+
if (options.buttonStyle) {
|
|
141
|
+
Object.entries(options.buttonStyle).forEach(([key, value]) => {
|
|
142
|
+
const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
|
|
143
|
+
letter.toUpperCase()
|
|
144
|
+
);
|
|
145
|
+
(btn.style as any)[camelKey] = value;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return btn;
|
|
149
|
+
};
|
|
114
150
|
|
|
115
|
-
|
|
151
|
+
const insertEmptySlide = (pos: number) => {
|
|
152
|
+
const schema = this.view.state.schema;
|
|
153
|
+
const slideType = schema.nodes.slide;
|
|
154
|
+
const paragraphType = schema.nodes.paragraph;
|
|
155
|
+
const slideContent = paragraphType.create();
|
|
156
|
+
const slide = slideType.create(null, slideContent);
|
|
157
|
+
const tr = this.view.state.tr.insert(pos + this.node.nodeSize, slide);
|
|
158
|
+
this.view.dispatch(tr);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const handlePlusClick = (event: MouseEvent) => {
|
|
116
162
|
event.preventDefault();
|
|
117
163
|
event.stopPropagation();
|
|
118
164
|
|
|
@@ -120,7 +166,6 @@ class SlideNodeView implements NodeView {
|
|
|
120
166
|
if (pos === undefined) return;
|
|
121
167
|
|
|
122
168
|
if (options.onClick) {
|
|
123
|
-
// Calculate slide index
|
|
124
169
|
let slideIndex = 0;
|
|
125
170
|
this.view.state.doc.nodesBetween(0, pos, (n) => {
|
|
126
171
|
if (n.type.name === "slide") slideIndex++;
|
|
@@ -133,23 +178,137 @@ class SlideNodeView implements NodeView {
|
|
|
133
178
|
event,
|
|
134
179
|
});
|
|
135
180
|
} else {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
181
|
+
insertEmptySlide(pos);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const openPresetModal = (pos: number) => {
|
|
186
|
+
// Remove existing modal if present to avoid duplicates
|
|
187
|
+
if (this.presetModal) {
|
|
188
|
+
this.presetModal.remove();
|
|
189
|
+
}
|
|
140
190
|
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
const overlay = document.createElement("div");
|
|
192
|
+
overlay.className = "add-slide-preset-modal";
|
|
143
193
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
194
|
+
const dialog = document.createElement("div");
|
|
195
|
+
dialog.className = "add-slide-preset-dialog";
|
|
196
|
+
if (options.presetBackground) {
|
|
197
|
+
dialog.style.setProperty("--add-slide-preset-bg", options.presetBackground);
|
|
147
198
|
}
|
|
199
|
+
if (options.presetForeground) {
|
|
200
|
+
dialog.style.setProperty("--add-slide-preset-fg", options.presetForeground);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const search = document.createElement("input");
|
|
204
|
+
search.className = "add-slide-preset-search";
|
|
205
|
+
search.type = "search";
|
|
206
|
+
search.placeholder = "Choose a template";
|
|
207
|
+
dialog.appendChild(search);
|
|
208
|
+
|
|
209
|
+
const list = document.createElement("div");
|
|
210
|
+
list.className = "add-slide-preset-list";
|
|
211
|
+
|
|
212
|
+
const items: HTMLElement[] = [];
|
|
213
|
+
|
|
214
|
+
(options.presets || []).forEach((preset) => {
|
|
215
|
+
const item = document.createElement("button");
|
|
216
|
+
item.className = "add-slide-preset-item";
|
|
217
|
+
item.setAttribute("type", "button");
|
|
218
|
+
item.innerHTML = `
|
|
219
|
+
<span class="add-slide-preset-icon">${preset.icon ?? ""}</span>
|
|
220
|
+
<span class="add-slide-preset-label">${preset.label}</span>
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
item.onclick = (event) => {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
event.stopPropagation();
|
|
226
|
+
try {
|
|
227
|
+
const nodeJSON = preset.build() as any;
|
|
228
|
+
const newSlide = this.view.state.schema.nodeFromJSON(nodeJSON);
|
|
229
|
+
const tr = this.view.state.tr.insert(
|
|
230
|
+
pos + this.node.nodeSize,
|
|
231
|
+
newSlide
|
|
232
|
+
);
|
|
233
|
+
this.view.dispatch(tr);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error("Failed to insert preset slide", err);
|
|
236
|
+
} finally {
|
|
237
|
+
overlay.remove();
|
|
238
|
+
this.presetModal = null;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
list.appendChild(item);
|
|
243
|
+
items.push(item);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
search.oninput = () => {
|
|
247
|
+
const term = search.value.toLowerCase();
|
|
248
|
+
items.forEach((item) => {
|
|
249
|
+
const label = item
|
|
250
|
+
.querySelector(".add-slide-preset-label")
|
|
251
|
+
?.textContent?.toLowerCase();
|
|
252
|
+
item.style.display = !term || label?.includes(term) ? "" : "none";
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
dialog.appendChild(list);
|
|
257
|
+
|
|
258
|
+
const close = () => {
|
|
259
|
+
overlay.remove();
|
|
260
|
+
this.presetModal = null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
overlay.onclick = (e) => {
|
|
264
|
+
if (e.target === overlay) {
|
|
265
|
+
close();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
document.addEventListener(
|
|
270
|
+
"keydown",
|
|
271
|
+
(e) => {
|
|
272
|
+
if (e.key === "Escape") {
|
|
273
|
+
close();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{ once: true }
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
overlay.appendChild(dialog);
|
|
280
|
+
document.body.appendChild(overlay);
|
|
281
|
+
this.presetModal = overlay;
|
|
148
282
|
};
|
|
149
283
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
284
|
+
const plusButton = createButton(options.content);
|
|
285
|
+
plusButton.onclick = handlePlusClick;
|
|
286
|
+
this.button = plusButton;
|
|
287
|
+
|
|
288
|
+
if (options.showPresets) {
|
|
289
|
+
const templateButton = createButton(
|
|
290
|
+
options.templateButtonContent ?? "✨"
|
|
291
|
+
);
|
|
292
|
+
templateButton.onclick = (event: MouseEvent) => {
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
event.stopPropagation();
|
|
295
|
+
const pos = this.getPos();
|
|
296
|
+
if (pos === undefined) return;
|
|
297
|
+
openPresetModal(pos);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const group = document.createElement("div");
|
|
301
|
+
group.className = "add-slide-button-group";
|
|
302
|
+
group.appendChild(plusButton);
|
|
303
|
+
group.appendChild(templateButton);
|
|
304
|
+
|
|
305
|
+
this.dom.appendChild(this.contentDOM);
|
|
306
|
+
this.dom.appendChild(group);
|
|
307
|
+
} else {
|
|
308
|
+
this.button = plusButton;
|
|
309
|
+
this.dom.appendChild(this.contentDOM);
|
|
310
|
+
this.dom.appendChild(this.button);
|
|
311
|
+
}
|
|
153
312
|
}
|
|
154
313
|
|
|
155
314
|
update(node: ProseMirrorNode) {
|
|
@@ -159,7 +318,14 @@ class SlideNodeView implements NodeView {
|
|
|
159
318
|
}
|
|
160
319
|
|
|
161
320
|
destroy() {
|
|
162
|
-
this.button
|
|
321
|
+
if (this.button) {
|
|
322
|
+
this.button.onclick = null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (this.presetModal) {
|
|
326
|
+
this.presetModal.remove();
|
|
327
|
+
this.presetModal = null;
|
|
328
|
+
}
|
|
163
329
|
}
|
|
164
330
|
}
|
|
165
331
|
|
|
@@ -202,6 +368,131 @@ const addSlideButtonStyles = `
|
|
|
202
368
|
outline: 2px solid var(--editor-focus, #3b82f6);
|
|
203
369
|
outline-offset: 2px;
|
|
204
370
|
}
|
|
371
|
+
|
|
372
|
+
.add-slide-button-group {
|
|
373
|
+
display: grid;
|
|
374
|
+
grid-template-columns: repeat(2, 1fr);
|
|
375
|
+
width: 180px;
|
|
376
|
+
margin: 16px auto 32px auto;
|
|
377
|
+
border: 1px solid var(--slide-border, #e5e5e5);
|
|
378
|
+
border-radius: 12px;
|
|
379
|
+
overflow: hidden;
|
|
380
|
+
background-color: var(--slide-bg, #ffffff);
|
|
381
|
+
box-shadow: var(--slide-shadow, 0 4px 12px rgba(0, 0, 0, 0.08));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.add-slide-button-group .add-slide-button {
|
|
385
|
+
margin: 0;
|
|
386
|
+
border: none;
|
|
387
|
+
border-radius: 0;
|
|
388
|
+
width: 100%;
|
|
389
|
+
height: 48px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.add-slide-button-group .add-slide-button:first-child {
|
|
393
|
+
border-right: 1px solid var(--slide-border, #e5e5e5);
|
|
394
|
+
border-top-left-radius: 12px;
|
|
395
|
+
border-bottom-left-radius: 12px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.add-slide-button-group .add-slide-button:last-child {
|
|
399
|
+
border-top-right-radius: 12px;
|
|
400
|
+
border-bottom-right-radius: 12px;
|
|
401
|
+
white-space: nowrap;
|
|
402
|
+
font-size: 14px;
|
|
403
|
+
padding: 0 12px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.add-slide-preset-modal {
|
|
407
|
+
position: fixed;
|
|
408
|
+
inset: 0;
|
|
409
|
+
background: rgba(0, 0, 0, 0.35);
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
justify-content: center;
|
|
413
|
+
z-index: 9999;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.add-slide-preset-dialog {
|
|
417
|
+
background: var(--add-slide-preset-bg, #ffffff);
|
|
418
|
+
color: var(--add-slide-preset-fg, #000000);
|
|
419
|
+
border-radius: 8px;
|
|
420
|
+
padding: 16px 16px 12px 16px;
|
|
421
|
+
min-width: 140px;
|
|
422
|
+
max-width: 480px;
|
|
423
|
+
max-height: 60vh;
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.add-slide-preset-search {
|
|
429
|
+
width: 100%;
|
|
430
|
+
padding: 10px 12px;
|
|
431
|
+
margin-bottom: 10px;
|
|
432
|
+
border-radius: 8px;
|
|
433
|
+
border: 1px solid color-mix(in srgb, var(--add-slide-preset-fg, #000000) 18%, transparent);
|
|
434
|
+
font-size: 14px;
|
|
435
|
+
color: inherit;
|
|
436
|
+
background: color-mix(in srgb, var(--add-slide-preset-bg, #ffffff) 90%, var(--add-slide-preset-fg, #000000) 10%);
|
|
437
|
+
}
|
|
438
|
+
.add-slide-preset-search:focus {
|
|
439
|
+
outline: 2px solid color-mix(in srgb, var(--add-slide-preset-fg, #3b82f6) 35%, transparent);
|
|
440
|
+
outline-offset: 1px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.add-slide-preset-list {
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
gap: 6px;
|
|
447
|
+
max-height: 52vh;
|
|
448
|
+
overflow: auto;
|
|
449
|
+
scrollbar-width: none;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.add-slide-preset-list::-webkit-scrollbar {
|
|
453
|
+
display: none;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.add-slide-preset-item {
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-direction: column;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 8px;
|
|
461
|
+
width: 100%;
|
|
462
|
+
border: none;
|
|
463
|
+
background: transparent;
|
|
464
|
+
color: inherit;
|
|
465
|
+
padding: 10px 0 12px 0;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
border-bottom: 1px solid color-mix(in srgb, var(--add-slide-preset-fg, #000000) 12%, transparent);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.add-slide-preset-item:last-child {
|
|
471
|
+
border-bottom: none;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.add-slide-preset-item:hover {
|
|
475
|
+
background: rgba(0, 0, 0, 0.03);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.add-slide-preset-icon {
|
|
479
|
+
display: block;
|
|
480
|
+
width: 100%;
|
|
481
|
+
height: auto;
|
|
482
|
+
line-height: 0;
|
|
483
|
+
}
|
|
484
|
+
.add-slide-preset-icon > svg {
|
|
485
|
+
width: 100%;
|
|
486
|
+
height: auto;
|
|
487
|
+
display: block;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.add-slide-preset-label {
|
|
491
|
+
text-align: center;
|
|
492
|
+
font-size: 14px;
|
|
493
|
+
font-weight: 600;
|
|
494
|
+
width: 100%;
|
|
495
|
+
}
|
|
205
496
|
`;
|
|
206
497
|
|
|
207
498
|
export const AddSlideButton = Extension.create<AddSlideButtonOptions>({
|
|
@@ -213,7 +504,12 @@ export const AddSlideButton = Extension.create<AddSlideButtonOptions>({
|
|
|
213
504
|
injectNonce: undefined,
|
|
214
505
|
buttonStyle: {},
|
|
215
506
|
content: "+",
|
|
507
|
+
showPresets: false,
|
|
508
|
+
presets: [],
|
|
509
|
+
templateButtonContent: "Template +",
|
|
216
510
|
onClick: null,
|
|
511
|
+
presetBackground: "#ffffff",
|
|
512
|
+
presetForeground: "#000000",
|
|
217
513
|
};
|
|
218
514
|
},
|
|
219
515
|
|