@blockslides/extension-add-slide-button 0.1.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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@blockslides/extension-add-slide-button",
3
+ "description": "add slide button extension for blockslides",
4
+ "version": "0.1.0",
5
+ "author": "keivanmojmali",
6
+ "homepage": "https://github.com/keivanmojmali/blockslides",
7
+ "keywords": [
8
+ "blockslides",
9
+ "blockslides extension",
10
+ "slide button"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "types": {
17
+ "import": "./dist/index.d.ts",
18
+ "require": "./dist/index.d.cts"
19
+ },
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "main": "dist/index.cjs",
25
+ "module": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "files": [
28
+ "src",
29
+ "dist"
30
+ ],
31
+ "devDependencies": {
32
+ "@blockslides/core": "^0.1.0",
33
+ "@blockslides/pm": "^0.1.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@blockslides/core": "^0.1.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/keivanmojmali/blockslides",
41
+ "directory": "packages/extension-add-slide-button"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup"
45
+ }
46
+ }
@@ -0,0 +1,250 @@
1
+ import { Extension, createStyleTag } from "@blockslides/core";
2
+ import type { Node as ProseMirrorNode } from "@blockslides/pm/model";
3
+ import { Plugin, PluginKey } from "@blockslides/pm/state";
4
+ import { NodeView } from "@blockslides/pm/view";
5
+ import type { EditorView } from "@blockslides/pm/view";
6
+
7
+ //TODO: Add ability to easy choose a layout type like 1-1, 1-1-1, etc.
8
+
9
+ export interface AddSlideButtonOptions {
10
+ /**
11
+ * Whether to inject CSS styles for the button.
12
+ * @default true
13
+ */
14
+ injectCSS?: boolean;
15
+ /**
16
+ * Nonce for Content Security Policy.
17
+ * @default undefined
18
+ */
19
+ injectNonce?: string;
20
+ /**
21
+ * Custom CSS styles for the button element.
22
+ * These will override or extend the default theme styles.
23
+ * Supports both camelCase (React) and kebab-case (Vue/CSS) property names.
24
+ * @default {}
25
+ * @example { backgroundColor: '#000', color: '#fff' } // React/camelCase
26
+ * @example { 'background-color': '#000', 'color': '#fff' } // Vue/kebab-case
27
+ */
28
+ buttonStyle: Record<string, string>;
29
+ /**
30
+ * Button content - can be text, HTML, icon, emoji, etc.
31
+ * @default '+'
32
+ * @example '+' | 'Add Slide' | '➕' | '<svg>...</svg>'
33
+ */
34
+ content: string;
35
+ /**
36
+ * Custom click handler for the button.
37
+ * If not provided, will insert a new slide at the clicked position.
38
+ */
39
+ onClick:
40
+ | ((params: {
41
+ slideIndex: number;
42
+ position: number;
43
+ view: EditorView;
44
+ event: MouseEvent;
45
+ }) => void)
46
+ | null;
47
+ }
48
+
49
+ class SlideNodeView implements NodeView {
50
+ dom: HTMLElement;
51
+ contentDOM: HTMLElement;
52
+ button: HTMLElement;
53
+ options: AddSlideButtonOptions;
54
+ view: EditorView;
55
+ getPos: () => number | undefined;
56
+ node: ProseMirrorNode;
57
+
58
+ constructor(
59
+ node: ProseMirrorNode,
60
+ view: EditorView,
61
+ getPos: () => number | undefined,
62
+ options: AddSlideButtonOptions
63
+ ) {
64
+ this.node = node;
65
+ this.view = view;
66
+ this.getPos = getPos;
67
+ this.options = options;
68
+
69
+ // Create wrapper div
70
+ this.dom = document.createElement("div");
71
+ this.dom.classList.add("slide-wrapper");
72
+
73
+ // Use the slide node's type to render the correct DOM from Slide extension
74
+ const slideSpec = node.type.spec;
75
+ const rendered = slideSpec.toDOM ? slideSpec.toDOM(node) : null;
76
+
77
+ if (rendered && Array.isArray(rendered)) {
78
+ const [tag, attrs] = rendered;
79
+ this.contentDOM = document.createElement(tag as string);
80
+
81
+ // Apply all attributes from the Slide extension's renderHTML
82
+ if (attrs && typeof attrs === "object") {
83
+ Object.entries(attrs).forEach(([key, value]) => {
84
+ if (key === "class") {
85
+ this.contentDOM.className = value as string;
86
+ } else {
87
+ this.contentDOM.setAttribute(key, String(value));
88
+ }
89
+ });
90
+ }
91
+ } else {
92
+ // Fallback if toDOM isn't defined (shouldn't happen)
93
+ this.contentDOM = document.createElement("div");
94
+ this.contentDOM.className = "slide";
95
+ this.contentDOM.setAttribute("data-node-type", "slide");
96
+ }
97
+
98
+ // Create the add button
99
+ this.button = document.createElement("button");
100
+ this.button.className = "add-slide-button";
101
+ this.button.innerHTML = options.content;
102
+ this.button.setAttribute("type", "button");
103
+ this.button.contentEditable = "false";
104
+
105
+ // Apply custom styles
106
+ if (options.buttonStyle) {
107
+ Object.entries(options.buttonStyle).forEach(([key, value]) => {
108
+ const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
109
+ letter.toUpperCase()
110
+ );
111
+ (this.button.style as any)[camelKey] = value;
112
+ });
113
+ }
114
+
115
+ this.button.onclick = (event: MouseEvent) => {
116
+ event.preventDefault();
117
+ event.stopPropagation();
118
+
119
+ const pos = this.getPos();
120
+ if (pos === undefined) return;
121
+
122
+ if (options.onClick) {
123
+ // Calculate slide index
124
+ let slideIndex = 0;
125
+ this.view.state.doc.nodesBetween(0, pos, (n) => {
126
+ if (n.type.name === "slide") slideIndex++;
127
+ });
128
+
129
+ options.onClick({
130
+ slideIndex,
131
+ position: pos + this.node.nodeSize,
132
+ view: this.view,
133
+ event,
134
+ });
135
+ } else {
136
+ // Default: insert new slide after current one
137
+ const schema = this.view.state.schema;
138
+ const slideType = schema.nodes.slide;
139
+ const paragraphType = schema.nodes.paragraph;
140
+
141
+ // Insert slide with empty paragraph
142
+ const slideContent = paragraphType.create();
143
+
144
+ const slide = slideType.create(null, slideContent);
145
+ const tr = this.view.state.tr.insert(pos + this.node.nodeSize, slide);
146
+ this.view.dispatch(tr);
147
+ }
148
+ };
149
+
150
+ // Append to wrapper
151
+ this.dom.appendChild(this.contentDOM);
152
+ this.dom.appendChild(this.button);
153
+ }
154
+
155
+ update(node: ProseMirrorNode) {
156
+ if (node.type.name !== "slide") return false;
157
+ this.node = node;
158
+ return true;
159
+ }
160
+
161
+ destroy() {
162
+ this.button.onclick = null;
163
+ }
164
+ }
165
+
166
+ const addSlideButtonStyles = `
167
+ .slide-wrapper {
168
+ position: relative;
169
+ }
170
+
171
+ .add-slide-button {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ width: 48px;
176
+ height: 48px;
177
+ margin: 16px auto 32px auto;
178
+ padding: 0;
179
+ border: 2px solid var(--slide-border, #e5e5e5);
180
+ border-radius: 25%;
181
+ background-color: var(--slide-bg, #ffffff);
182
+ color: var(--editor-fg, #1a1a1a);
183
+ font-size: 24px;
184
+ font-weight: 300;
185
+ cursor: pointer;
186
+ transition: all 0.2s ease;
187
+ box-shadow: var(--slide-shadow, 0 4px 12px rgba(0, 0, 0, 0.08));
188
+ }
189
+
190
+ .add-slide-button:hover {
191
+ background-color: var(--editor-hover, #f0f0f0);
192
+ border-color: var(--editor-selection, #3b82f6);
193
+ transform: scale(1.05);
194
+ }
195
+
196
+ .add-slide-button:active {
197
+ background-color: var(--editor-active, #e8e8e8);
198
+ transform: scale(0.95);
199
+ }
200
+
201
+ .add-slide-button:focus {
202
+ outline: 2px solid var(--editor-focus, #3b82f6);
203
+ outline-offset: 2px;
204
+ }
205
+ `;
206
+
207
+ export const AddSlideButton = Extension.create<AddSlideButtonOptions>({
208
+ name: "addSlideButton",
209
+
210
+ addOptions() {
211
+ return {
212
+ injectCSS: true,
213
+ injectNonce: undefined,
214
+ buttonStyle: {},
215
+ content: "+",
216
+ onClick: null,
217
+ };
218
+ },
219
+
220
+ addProseMirrorPlugins() {
221
+ const options = this.options;
222
+
223
+ // Inject CSS styles if enabled
224
+ if (options.injectCSS) {
225
+ createStyleTag(
226
+ addSlideButtonStyles,
227
+ options.injectNonce,
228
+ "add-slide-button"
229
+ );
230
+ }
231
+
232
+ return [
233
+ new Plugin({
234
+ key: new PluginKey("addSlideButton"),
235
+ props: {
236
+ nodeViews: {
237
+ slide: (node, view, getPos) => {
238
+ return new SlideNodeView(
239
+ node,
240
+ view,
241
+ getPos as () => number,
242
+ options
243
+ );
244
+ },
245
+ },
246
+ },
247
+ }),
248
+ ];
249
+ },
250
+ });
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { AddSlideButton } from "./add-slide-button.js";
2
+
3
+ export * from "./add-slide-button.js";
4
+
5
+ export default AddSlideButton;