editor-ts 0.0.1 → 0.0.12

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.
@@ -0,0 +1,109 @@
1
+ import type { Component, CustomComponentRegistry } from '../types';
2
+
3
+ export type ComponentPaletteConfig = {
4
+ container: HTMLElement;
5
+ registry: CustomComponentRegistry;
6
+ onPick: (type: string) => void;
7
+ };
8
+
9
+ export class ComponentPalette {
10
+ private container: HTMLElement;
11
+ private registry: CustomComponentRegistry;
12
+ private onPick: (type: string) => void;
13
+ private selectedType: string | null = null;
14
+
15
+ constructor(config: ComponentPaletteConfig) {
16
+ this.container = config.container;
17
+ this.registry = config.registry;
18
+ this.onPick = config.onPick;
19
+
20
+ this.render();
21
+ }
22
+
23
+ updateRegistry(registry: CustomComponentRegistry): void {
24
+ this.registry = registry;
25
+ this.render();
26
+ }
27
+
28
+ setSelected(type: string | null): void {
29
+ this.selectedType = type;
30
+ this.render();
31
+ }
32
+
33
+ destroy(): void {
34
+ this.container.innerHTML = '';
35
+ }
36
+
37
+ private render(): void {
38
+ this.container.innerHTML = '';
39
+
40
+ const wrapper = document.createElement('div');
41
+ wrapper.style.display = 'flex';
42
+ wrapper.style.flexWrap = 'wrap';
43
+ wrapper.style.gap = '0.5rem';
44
+ wrapper.style.alignItems = 'flex-start';
45
+
46
+ const types = Object.keys(this.registry).sort();
47
+
48
+ if (types.length === 0) {
49
+ this.container.textContent = 'No components available';
50
+ return;
51
+ }
52
+
53
+ types.forEach((type) => {
54
+ const def = this.registry[type];
55
+ if (!def) return;
56
+
57
+ const btn = document.createElement('button');
58
+ btn.type = 'button';
59
+ btn.style.border = '1px solid rgba(0,0,0,0.12)';
60
+ btn.style.borderRadius = '6px';
61
+ btn.style.padding = '0.5rem';
62
+ btn.style.background = 'white';
63
+ btn.style.cursor = 'pointer';
64
+ btn.style.fontSize = '0.75rem';
65
+ btn.style.width = '5.75rem';
66
+ btn.style.display = 'flex';
67
+ btn.style.flexDirection = 'column';
68
+ btn.style.alignItems = 'center';
69
+ btn.style.gap = '0.35rem';
70
+
71
+ const icon = document.createElement('div');
72
+ icon.style.width = '24px';
73
+ icon.style.height = '24px';
74
+ icon.style.display = 'flex';
75
+ icon.style.alignItems = 'center';
76
+ icon.style.justifyContent = 'center';
77
+ icon.style.color = '#111827';
78
+
79
+ if (typeof def.iconSvg === 'string' && def.iconSvg.trim() !== '') {
80
+ icon.innerHTML = def.iconSvg;
81
+ } else {
82
+ icon.textContent = '⬚';
83
+ }
84
+
85
+ const label = document.createElement('div');
86
+ label.textContent = def.label ?? type;
87
+ label.style.textAlign = 'center';
88
+ label.style.width = '100%';
89
+ label.style.overflow = 'hidden';
90
+ label.style.textOverflow = 'ellipsis';
91
+ label.style.whiteSpace = 'nowrap';
92
+
93
+ btn.appendChild(icon);
94
+ btn.appendChild(label);
95
+
96
+ btn.style.outline = this.selectedType === type ? '2px solid #3b82f6' : 'none';
97
+
98
+ btn.addEventListener('click', () => {
99
+ this.selectedType = type;
100
+ this.onPick(type);
101
+ this.render();
102
+ });
103
+
104
+ wrapper.appendChild(btn);
105
+ });
106
+
107
+ this.container.appendChild(wrapper);
108
+ }
109
+ }
@@ -0,0 +1,74 @@
1
+ import type { Component, CustomComponentRegistry, CustomComponentDefinition } from '../types';
2
+ import { generateId } from '../utils/helpers';
3
+
4
+ const placeholderImageSvg =
5
+ '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360">'
6
+ + '<rect width="640" height="360" rx="12" fill="#f3f4f6"/>'
7
+ + '<rect x="64" y="64" width="512" height="232" rx="10" fill="#ffffff" stroke="#d1d5db" stroke-width="3"/>'
8
+ + '<clipPath id="imgClip"><rect x="64" y="64" width="512" height="232" rx="10" /></clipPath>'
9
+ + '<g clip-path="url(#imgClip)">'
10
+ + '<path d="M120 244l72-72 68 68 48-48 112 88" fill="none" stroke="#9ca3af" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>'
11
+ + '<circle cx="188" cy="140" r="18" fill="#9ca3af"/>'
12
+ + '</g>'
13
+ + '<text x="320" y="322" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="22" fill="#6b7280">Click to upload</text>'
14
+ + '</svg>';
15
+
16
+ const placeholderImageDataUrl = `data:image/svg+xml,${encodeURIComponent(placeholderImageSvg)}`;
17
+
18
+ export const defaultComponentFactories = {
19
+ box: () => ({
20
+ type: 'box',
21
+ tagName: 'div',
22
+ attributes: { id: generateId('box') },
23
+ style: 'min-height: 200px;',
24
+ components: [],
25
+ }),
26
+ text: () => ({
27
+ type: 'text',
28
+ tagName: 'div',
29
+ attributes: { id: generateId('text') },
30
+ content: 'Type something here…',
31
+ }),
32
+ image: () => ({
33
+ type: 'image',
34
+ tagName: 'img',
35
+ void: true,
36
+ attributes: { id: generateId('image'), src: placeholderImageDataUrl },
37
+ }),
38
+ };
39
+
40
+ export const defaultComponentRegistry: CustomComponentRegistry = {
41
+ box: {
42
+ type: 'box',
43
+ label: 'Box',
44
+ iconSvg:
45
+ '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>',
46
+ factory: defaultComponentFactories.box,
47
+ },
48
+ text: {
49
+ type: 'text',
50
+ label: 'Text',
51
+ iconSvg:
52
+ '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16"/><path d="M10 6v12"/><path d="M14 6v12"/><path d="M4 18h16"/></svg>',
53
+ factory: defaultComponentFactories.text,
54
+ },
55
+ image: {
56
+ type: 'image',
57
+ label: 'Image',
58
+ iconSvg:
59
+ '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M8 14l2-2 3 3 2-2 3 3"/><circle cx="9" cy="9" r="1"/></svg>',
60
+ factory: defaultComponentFactories.image,
61
+ },
62
+ };
63
+
64
+ export function mergeCustomComponentRegistry(
65
+ baseRegistry: CustomComponentRegistry,
66
+ overrides?: CustomComponentRegistry
67
+ ): CustomComponentRegistry {
68
+ if (!overrides) return baseRegistry;
69
+ return { ...baseRegistry, ...overrides };
70
+ }
71
+
72
+ export function createCustomComponentDefinition(def: CustomComponentDefinition): CustomComponentDefinition {
73
+ return def;
74
+ }
@@ -0,0 +1,220 @@
1
+ import type { ShortcutDefinition } from '../types';
2
+
3
+ export type ShortcutAction = () => void | Promise<void>;
4
+
5
+ export type ShortcutContext = {
6
+ openCommandPalette: () => void;
7
+ undo: () => void | Promise<void>;
8
+ redo: () => void | Promise<void>;
9
+ deleteSelected: () => void | Promise<void>;
10
+ };
11
+
12
+ type ParsedShortcut = {
13
+ key: string;
14
+ ctrl: boolean;
15
+ meta: boolean;
16
+ alt: boolean;
17
+ shift: boolean;
18
+ mod: boolean;
19
+ action: () => void | Promise<void>;
20
+ };
21
+
22
+ type ModKey = 'ctrl' | 'meta' | 'alt';
23
+
24
+ const normalizeKey = (value: string): string => {
25
+ const lowered = value.toLowerCase();
26
+ if (lowered === ' ') return 'space';
27
+ if (lowered === 'esc') return 'escape';
28
+ return lowered;
29
+ };
30
+
31
+ const parseShortcut = (shortcut: ShortcutDefinition): ParsedShortcut | null => {
32
+ const tokens = shortcut.key
33
+ .split(/[+-]/)
34
+ .map((token: string) => token.trim().toLowerCase())
35
+ .filter(Boolean);
36
+ if (tokens.length === 0) return null;
37
+
38
+ let key: string | null = null;
39
+ let ctrl = false;
40
+ let meta = false;
41
+ let alt = false;
42
+ let shift = false;
43
+ let mod = false;
44
+
45
+ tokens.forEach((token: string) => {
46
+ switch (token) {
47
+ case 'ctrl':
48
+ case 'control':
49
+ ctrl = true;
50
+ return;
51
+ case 'cmd':
52
+ case 'command':
53
+ case 'meta':
54
+ meta = true;
55
+ return;
56
+ case 'alt':
57
+ case 'option':
58
+ alt = true;
59
+ return;
60
+ case 'shift':
61
+ shift = true;
62
+ return;
63
+ case 'mod':
64
+ mod = true;
65
+ return;
66
+ default:
67
+ key = normalizeKey(token);
68
+ return;
69
+ }
70
+ });
71
+
72
+ if (!key) return null;
73
+
74
+ return {
75
+ key,
76
+ ctrl,
77
+ meta,
78
+ alt,
79
+ shift,
80
+ mod,
81
+ action: shortcut.action,
82
+ };
83
+ };
84
+
85
+ const isEditableTarget = (target: EventTarget | null): boolean => {
86
+ if (!target || !(target instanceof HTMLElement)) return false;
87
+ if (target.isContentEditable) return true;
88
+
89
+ const tag = target.tagName.toLowerCase();
90
+ return tag === 'input' || tag === 'textarea' || tag === 'select';
91
+ };
92
+
93
+ export const createDefaultShortcuts = (handlers: Partial<ShortcutContext>): ShortcutDefinition[] => {
94
+ const shortcuts: ShortcutDefinition[] = [];
95
+
96
+ if (handlers.openCommandPalette) {
97
+ shortcuts.push({ key: 'mod+p', action: handlers.openCommandPalette });
98
+ }
99
+
100
+ if (handlers.undo) {
101
+ shortcuts.push({ key: 'mod+z', action: handlers.undo });
102
+ }
103
+
104
+ if (handlers.redo) {
105
+ shortcuts.push({ key: 'mod+shift+z', action: handlers.redo });
106
+ }
107
+
108
+ if (handlers.deleteSelected) {
109
+ shortcuts.push({ key: 'delete', action: handlers.deleteSelected });
110
+ shortcuts.push({ key: 'backspace', action: handlers.deleteSelected });
111
+ }
112
+
113
+ return shortcuts;
114
+ };
115
+
116
+ export const createCommandPaletteShortcuts = (handlers: Pick<ShortcutContext, 'openCommandPalette'>): ShortcutDefinition[] => {
117
+ return [{ key: 'mod+p', action: handlers.openCommandPalette }];
118
+ };
119
+
120
+ export const createEditorShortcuts = (handlers: Omit<ShortcutContext, 'openCommandPalette'>): ShortcutDefinition[] => {
121
+ const shortcuts: ShortcutDefinition[] = [];
122
+
123
+ if (handlers.undo) {
124
+ shortcuts.push({ key: 'mod+z', action: handlers.undo });
125
+ }
126
+
127
+ if (handlers.redo) {
128
+ shortcuts.push({ key: 'mod+shift+z', action: handlers.redo });
129
+ }
130
+
131
+ if (handlers.deleteSelected) {
132
+ shortcuts.push({ key: 'delete', action: handlers.deleteSelected });
133
+ shortcuts.push({ key: 'backspace', action: handlers.deleteSelected });
134
+ }
135
+
136
+ return shortcuts;
137
+ };
138
+
139
+ export class KeyboardShortcuts {
140
+ private shortcuts: ParsedShortcut[] = [];
141
+ private shouldIgnore?: (event: KeyboardEvent) => boolean;
142
+ private boundHandler?: (event: KeyboardEvent) => void;
143
+ private targets = new Set<Document>();
144
+ private modKey: ModKey;
145
+
146
+ constructor(options: {
147
+ shortcuts: ShortcutDefinition[];
148
+ shouldIgnore?: (event: KeyboardEvent) => boolean;
149
+ modKey?: ModKey;
150
+ }) {
151
+ this.shortcuts = options.shortcuts
152
+ .map(parseShortcut)
153
+ .filter((shortcut): shortcut is ParsedShortcut => !!shortcut);
154
+ this.shouldIgnore = options.shouldIgnore;
155
+ this.modKey = options.modKey ?? 'ctrl';
156
+ }
157
+
158
+
159
+ bind(target: Document = document): void {
160
+ if (this.targets.has(target)) return;
161
+
162
+ if (!this.boundHandler) {
163
+ this.boundHandler = (event: KeyboardEvent) => {
164
+ if (this.shouldIgnore?.(event)) return;
165
+
166
+ if (isEditableTarget(event.target) && !event.metaKey && !event.ctrlKey) {
167
+ return;
168
+ }
169
+
170
+ const normalizedKey = normalizeKey(event.key);
171
+
172
+ for (const shortcut of this.shortcuts) {
173
+ if (shortcut.key !== normalizedKey) continue;
174
+
175
+ const modPressed = this.modKey === 'meta'
176
+ ? event.metaKey
177
+ : this.modKey === 'alt'
178
+ ? event.altKey
179
+ : event.ctrlKey;
180
+ if (shortcut.mod && !modPressed) continue;
181
+ if (shortcut.ctrl && !event.ctrlKey) continue;
182
+ if (shortcut.meta && !event.metaKey) continue;
183
+ if (shortcut.alt && !event.altKey) continue;
184
+ if (shortcut.shift && !event.shiftKey) continue;
185
+
186
+ event.preventDefault();
187
+ const result = shortcut.action();
188
+ if (result && typeof (result as Promise<void>).then === 'function') {
189
+ void (result as Promise<void>);
190
+ }
191
+ return;
192
+ }
193
+ };
194
+ }
195
+
196
+ this.targets.add(target);
197
+ (target as Document & { __editortsShortcutsBound?: boolean }).__editortsShortcutsBound = true;
198
+ target.addEventListener('keydown', this.boundHandler);
199
+ }
200
+
201
+ unbind(target?: Document): void {
202
+ if (!this.boundHandler) return;
203
+
204
+ if (target) {
205
+ if (this.targets.has(target)) {
206
+ target.removeEventListener('keydown', this.boundHandler);
207
+ this.targets.delete(target);
208
+ }
209
+ } else {
210
+ this.targets.forEach((doc) => {
211
+ doc.removeEventListener('keydown', this.boundHandler!);
212
+ });
213
+ this.targets.clear();
214
+ }
215
+
216
+ if (this.targets.size === 0) {
217
+ this.boundHandler = undefined;
218
+ }
219
+ }
220
+ }