editor-ts 0.0.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.
@@ -0,0 +1,170 @@
1
+ import type { Component, ToolbarConfig, ToolbarRule, ComponentSelector } from '../types';
2
+ import { defaultToolbarConfig } from '../utils/toolbar';
3
+
4
+ /**
5
+ * Manager for runtime toolbar configurations
6
+ * Toolbars are NOT stored in JSON - they are configured at runtime
7
+ */
8
+ export class ToolbarManager {
9
+ private rules: ToolbarRule[] = [];
10
+ private globalDefault: ToolbarConfig = defaultToolbarConfig;
11
+
12
+ /**
13
+ * Set global default toolbar for all components
14
+ */
15
+ setGlobalDefault(config: ToolbarConfig): void {
16
+ this.globalDefault = config;
17
+ }
18
+
19
+ /**
20
+ * Configure toolbar for components matching a selector
21
+ */
22
+ configure(selector: ComponentSelector, config: ToolbarConfig): void {
23
+ // Remove existing rule for same selector
24
+ this.rules = this.rules.filter(rule =>
25
+ JSON.stringify(rule.selector) !== JSON.stringify(selector)
26
+ );
27
+
28
+ // Add new rule
29
+ this.rules.push({ selector, config });
30
+ }
31
+
32
+ /**
33
+ * Configure toolbar by component ID
34
+ */
35
+ configureById(id: string, config: ToolbarConfig): void {
36
+ this.configure({ id }, config);
37
+ }
38
+
39
+ /**
40
+ * Configure toolbar by component type
41
+ */
42
+ configureByType(type: string, config: ToolbarConfig): void {
43
+ this.configure({ type }, config);
44
+ }
45
+
46
+ /**
47
+ * Configure toolbar by tag name
48
+ */
49
+ configureByTag(tagName: string, config: ToolbarConfig): void {
50
+ this.configure({ tagName }, config);
51
+ }
52
+
53
+ /**
54
+ * Configure toolbar with custom matcher function
55
+ */
56
+ configureCustom(matcher: (component: Component) => boolean, config: ToolbarConfig): void {
57
+ this.configure({ custom: matcher }, config);
58
+ }
59
+
60
+ /**
61
+ * Get toolbar configuration for a specific component
62
+ */
63
+ getToolbarForComponent(component: Component): ToolbarConfig {
64
+ // Check rules in reverse order (last added has priority)
65
+ for (let i = this.rules.length - 1; i >= 0; i--) {
66
+ const rule = this.rules[i];
67
+ if (rule && this.matchesSelector(component, rule.selector)) {
68
+ return rule.config;
69
+ }
70
+ }
71
+
72
+ // Return global default
73
+ return this.globalDefault;
74
+ }
75
+
76
+ /**
77
+ * Get toolbar by component ID (convenience method)
78
+ */
79
+ getToolbarById(components: Component[], id: string): ToolbarConfig | null {
80
+ const component = this.findComponentById(components, id);
81
+ if (component) {
82
+ return this.getToolbarForComponent(component);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Check if component matches selector
89
+ */
90
+ private matchesSelector(component: Component, selector: ComponentSelector): boolean {
91
+ if ('id' in selector) {
92
+ return component.attributes?.id === selector.id;
93
+ }
94
+ if ('type' in selector) {
95
+ return component.type === selector.type;
96
+ }
97
+ if ('tagName' in selector) {
98
+ return component.tagName === selector.tagName;
99
+ }
100
+ if ('attributes' in selector) {
101
+ return Object.entries(selector.attributes).every(
102
+ ([key, value]) => component.attributes?.[key] === value
103
+ );
104
+ }
105
+ if ('custom' in selector) {
106
+ return selector.custom(component);
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Find component by ID in tree
113
+ */
114
+ private findComponentById(components: Component[], id: string): Component | null {
115
+ for (const comp of components) {
116
+ if (comp.attributes?.id === id) {
117
+ return comp;
118
+ }
119
+ if (comp.components && comp.components.length > 0) {
120
+ const found = this.findComponentById(comp.components, id);
121
+ if (found) return found;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Remove toolbar configuration for a selector
129
+ */
130
+ removeConfiguration(selector: ComponentSelector): boolean {
131
+ const initialLength = this.rules.length;
132
+ this.rules = this.rules.filter(rule =>
133
+ JSON.stringify(rule.selector) !== JSON.stringify(selector)
134
+ );
135
+ return this.rules.length < initialLength;
136
+ }
137
+
138
+ /**
139
+ * Clear all toolbar configurations
140
+ */
141
+ clearAll(): void {
142
+ this.rules = [];
143
+ }
144
+
145
+ /**
146
+ * Get all toolbar rules
147
+ */
148
+ getAllRules(): ToolbarRule[] {
149
+ return [...this.rules];
150
+ }
151
+
152
+ /**
153
+ * Export toolbar configuration as JSON (for sharing config, not data)
154
+ */
155
+ exportConfig(): string {
156
+ return JSON.stringify({
157
+ globalDefault: this.globalDefault,
158
+ rules: this.rules,
159
+ }, null, 2);
160
+ }
161
+
162
+ /**
163
+ * Import toolbar configuration from JSON
164
+ */
165
+ importConfig(json: string): void {
166
+ const config = JSON.parse(json);
167
+ this.globalDefault = config.globalDefault || defaultToolbarConfig;
168
+ this.rules = config.rules || [];
169
+ }
170
+ }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * EditorTs Editor Initialization
3
+ * Users control the layout - init() just populates their containers
4
+ */
5
+
6
+ import { Page } from './Page';
7
+ import type { InitConfig, EditorTsEditor, Component } from '../types';
8
+
9
+ /**
10
+ * Initialize EditorTs Editor
11
+ * User creates the HTML structure, init() populates it
12
+ */
13
+ export function init(config: InitConfig): EditorTsEditor {
14
+ // Get the iframe element (required)
15
+ const iframe = document.getElementById(config.iframeId) as HTMLIFrameElement;
16
+ if (!iframe || iframe.tagName !== 'IFRAME') {
17
+ throw new Error(`Iframe element #${config.iframeId} not found or is not an iframe`);
18
+ }
19
+
20
+ // Create Page instance
21
+ const page = new Page(config.data);
22
+
23
+ // Configure toolbars from config
24
+ if (config.toolbars) {
25
+ if (config.toolbars.byId) {
26
+ Object.entries(config.toolbars.byId).forEach(([id, toolbarConfig]) => {
27
+ page.toolbars.configureById(id, toolbarConfig);
28
+ });
29
+ }
30
+
31
+ if (config.toolbars.byType) {
32
+ Object.entries(config.toolbars.byType).forEach(([type, toolbarConfig]) => {
33
+ page.toolbars.configureByType(type, toolbarConfig);
34
+ });
35
+ }
36
+
37
+ if (config.toolbars.byTag) {
38
+ Object.entries(config.toolbars.byTag).forEach(([tag, toolbarConfig]) => {
39
+ page.toolbars.configureByTag(tag, toolbarConfig);
40
+ });
41
+ }
42
+
43
+ if (config.toolbars.default) {
44
+ page.toolbars.setGlobalDefault(config.toolbars.default);
45
+ }
46
+ }
47
+
48
+ // Event system
49
+ const eventListeners: Record<string, Function[]> = {};
50
+
51
+ const on = (event: string, callback: Function) => {
52
+ if (!eventListeners[event]) {
53
+ eventListeners[event] = [];
54
+ }
55
+ eventListeners[event]!.push(callback);
56
+ };
57
+
58
+ const off = (event: string, callback: Function) => {
59
+ if (eventListeners[event]) {
60
+ eventListeners[event] = eventListeners[event]!.filter(cb => cb !== callback);
61
+ }
62
+ };
63
+
64
+ const emit = (event: string, ...args: any[]) => {
65
+ if (eventListeners[event]) {
66
+ eventListeners[event]!.forEach(callback => callback(...args));
67
+ }
68
+ };
69
+
70
+ // Get optional UI containers
71
+ const sidebarContainer = config.ui?.sidebar?.containerId
72
+ ? document.getElementById(config.ui.sidebar.containerId)
73
+ : null;
74
+
75
+ const statsContainer = config.ui?.stats?.containerId
76
+ ? document.getElementById(config.ui.stats.containerId)
77
+ : null;
78
+
79
+ const selectedInfoContainer = config.ui?.selectedInfo?.containerId
80
+ ? document.getElementById(config.ui.selectedInfo.containerId)
81
+ : null;
82
+
83
+ // Populate stats if container provided
84
+ if (statsContainer && config.ui?.stats?.enabled !== false) {
85
+ statsContainer.innerHTML = `
86
+ <div style="font-size: 0.85rem;">
87
+ <div>Components: ${page.components.count()}</div>
88
+ <div>Styles: ${page.styles.count()}</div>
89
+ <div>Assets: ${page.assets.count()}</div>
90
+ </div>
91
+ `;
92
+ }
93
+
94
+ // Build iframe content with WYSIWYG
95
+ const iframeContent = `<!DOCTYPE html>
96
+ <html>
97
+ <head>
98
+ <meta charset="UTF-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
+ <title>${page.getTitle()}</title>
101
+ <style>${page.getCSS()}</style>
102
+ <style>
103
+ /* WYSIWYG editing styles */
104
+ .editorts-highlight {
105
+ outline: 2px dashed var(--color-editor-light-text, #212C3E) !important;
106
+ outline-offset: 2px;
107
+ cursor: pointer !important;
108
+ position: relative !important;
109
+ }
110
+ .editorts-highlight:hover {
111
+ outline: 2px solid var(--color-editor-light-text, #212C3E) !important;
112
+ background-color: rgba(33, 44, 62, 0.05) !important;
113
+ }
114
+ .editorts-selected {
115
+ outline: 3px solid #10b981 !important;
116
+ background-color: rgba(16, 185, 129, 0.1) !important;
117
+ }
118
+ .editorts-context-toolbar {
119
+ position: absolute;
120
+ top: -42px;
121
+ left: 0;
122
+ background: white;
123
+ border: 2px solid var(--color-editor-light-text, #212C3E);
124
+ border-radius: 6px;
125
+ padding: 0.4rem;
126
+ display: flex;
127
+ gap: 0.3rem;
128
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
129
+ z-index: 9999;
130
+ }
131
+ .toolbar-action {
132
+ background: white;
133
+ border: 1px solid var(--color-primary-border, #e5e7eb);
134
+ padding: 0.5rem 0.75rem;
135
+ border-radius: 4px;
136
+ cursor: pointer;
137
+ font-size: 0.85rem;
138
+ white-space: nowrap;
139
+ font-family: var(--font-main);
140
+ transition: all 0.2s;
141
+ }
142
+ .toolbar-action:hover {
143
+ background: var(--color-editor-light-bg, #EDF0F5);
144
+ border-color: var(--color-editor-light-text, #212C3E);
145
+ }
146
+ .toolbar-action.danger:hover {
147
+ background: #fee;
148
+ border-color: #ef4444;
149
+ color: #ef4444;
150
+ }
151
+ </style>
152
+ </head>
153
+ ${page.getHTML()}
154
+ <script>
155
+ let selectedElement = null;
156
+
157
+ // Initialize WYSIWYG
158
+ function initWYSIWYG() {
159
+ document.querySelectorAll('[id]').forEach(el => {
160
+ if (!el.id || el.id.startsWith('editorts-')) return;
161
+
162
+ el.classList.add('editorts-highlight');
163
+
164
+ el.addEventListener('click', (e) => {
165
+ e.stopPropagation();
166
+ selectElement(el);
167
+ });
168
+ });
169
+ }
170
+
171
+ function selectElement(el) {
172
+ // Clear previous selection
173
+ if (selectedElement) {
174
+ selectedElement.classList.remove('editorts-selected');
175
+ const oldToolbar = selectedElement.querySelector('.editorts-context-toolbar');
176
+ if (oldToolbar) oldToolbar.remove();
177
+ }
178
+
179
+ // Highlight new selection
180
+ selectedElement = el;
181
+ el.classList.add('editorts-selected');
182
+
183
+ // Notify parent
184
+ window.parent.postMessage({
185
+ type: 'editorts:componentSelected',
186
+ id: el.id,
187
+ tagName: el.tagName.toLowerCase(),
188
+ className: el.className
189
+ }, '*');
190
+
191
+ // Request toolbar config
192
+ window.parent.postMessage({
193
+ type: 'editorts:getToolbar',
194
+ id: el.id
195
+ }, '*');
196
+ }
197
+
198
+ // Listen for toolbar config from parent
199
+ window.addEventListener('message', (event) => {
200
+ if (event.data.type === 'editorts:toolbarConfig') {
201
+ renderToolbar(event.data.config, event.data.elementId);
202
+ } else if (event.data.type === 'editorts:toolbarAction') {
203
+ handleToolbarAction(event.data.action, event.data.elementId);
204
+ }
205
+ });
206
+
207
+ function renderToolbar(toolbarConfig, elementId) {
208
+ const el = document.getElementById(elementId);
209
+ if (!el || !toolbarConfig.enabled) return;
210
+
211
+ const toolbar = document.createElement('div');
212
+ toolbar.className = 'editorts-context-toolbar';
213
+
214
+ const enabledActions = toolbarConfig.actions.filter(a => a.enabled);
215
+ enabledActions.forEach(action => {
216
+ const btn = document.createElement('button');
217
+ btn.className = 'toolbar-action' + (action.danger ? ' danger' : '');
218
+ btn.textContent = action.icon + ' ' + action.label;
219
+ btn.onclick = () => {
220
+ window.parent.postMessage({
221
+ type: 'editorts:toolbarAction',
222
+ action: action.id,
223
+ elementId: elementId
224
+ }, '*');
225
+ };
226
+ toolbar.appendChild(btn);
227
+ });
228
+
229
+ el.appendChild(toolbar);
230
+ }
231
+
232
+ function handleToolbarAction(action, elementId) {
233
+ const el = document.getElementById(elementId);
234
+ if (!el) return;
235
+
236
+ if (action === 'delete') {
237
+ el.remove();
238
+ }
239
+ }
240
+
241
+ // Initialize
242
+ if (document.readyState === 'loading') {
243
+ document.addEventListener('DOMContentLoaded', initWYSIWYG);
244
+ } else {
245
+ initWYSIWYG();
246
+ }
247
+ </script>
248
+ </html>`;
249
+
250
+ // Load content into iframe
251
+ iframe.srcdoc = iframeContent;
252
+
253
+ // Handle messages from iframe
254
+ window.addEventListener('message', (event) => {
255
+ if (event.data.type === 'editorts:componentSelected') {
256
+ const component = page.components.findById(event.data.id);
257
+ if (component) {
258
+ // Update selected info container if provided
259
+ if (selectedInfoContainer && config.ui?.selectedInfo?.enabled !== false) {
260
+ selectedInfoContainer.innerHTML = `
261
+ <div><strong>ID:</strong> ${event.data.id}</div>
262
+ <div><strong>Tag:</strong> ${event.data.tagName}</div>
263
+ `;
264
+ }
265
+
266
+ // Emit event
267
+ emit('componentSelect', component);
268
+ if (config.onComponentSelect) {
269
+ config.onComponentSelect(component);
270
+ }
271
+ }
272
+ } else if (event.data.type === 'editorts:getToolbar') {
273
+ // Send toolbar config to iframe
274
+ const component = page.components.findById(event.data.id);
275
+ if (component) {
276
+ const toolbarConfig = page.toolbars.getToolbarForComponent(component);
277
+ iframe.contentWindow?.postMessage({
278
+ type: 'editorts:toolbarConfig',
279
+ config: toolbarConfig,
280
+ elementId: event.data.id
281
+ }, '*');
282
+ }
283
+ } else if (event.data.type === 'editorts:toolbarAction') {
284
+ handleToolbarAction(event.data.action, event.data.elementId);
285
+ }
286
+ });
287
+
288
+ // Handle toolbar actions
289
+ function handleToolbarAction(actionId: string, elementId: string) {
290
+ const component = page.components.findById(elementId);
291
+ if (!component) return;
292
+
293
+ switch (actionId) {
294
+ case 'edit':
295
+ emit('componentEdit', component);
296
+ if (config.onComponentEdit) {
297
+ config.onComponentEdit(component);
298
+ }
299
+ break;
300
+
301
+ case 'editJS':
302
+ emit('componentEditJS', component);
303
+ break;
304
+
305
+ case 'duplicate':
306
+ const clone = JSON.parse(JSON.stringify(component));
307
+ clone.attributes = clone.attributes || {};
308
+ clone.attributes.id = elementId + '-copy-' + Date.now();
309
+ page.components.addComponent(clone);
310
+
311
+ emit('componentDuplicate', component, clone);
312
+ if (config.onComponentDuplicate) {
313
+ config.onComponentDuplicate(component, clone);
314
+ }
315
+
316
+ refresh();
317
+ break;
318
+
319
+ case 'delete':
320
+ page.components.removeComponent(elementId);
321
+
322
+ emit('componentDelete', component);
323
+ if (config.onComponentDelete) {
324
+ config.onComponentDelete(component);
325
+ }
326
+
327
+ // Notify iframe to remove element
328
+ iframe.contentWindow?.postMessage({
329
+ type: 'editorts:toolbarAction',
330
+ action: 'delete',
331
+ elementId: elementId
332
+ }, '*');
333
+ break;
334
+ }
335
+ }
336
+
337
+ // Refresh iframe
338
+ function refresh() {
339
+ iframe.srcdoc = iframeContent;
340
+ }
341
+
342
+ // Save page data
343
+ function save(): string {
344
+ return page.toJSON();
345
+ }
346
+
347
+ // Destroy editor
348
+ function destroy() {
349
+ iframe.srcdoc = '';
350
+ if (sidebarContainer) sidebarContainer.innerHTML = '';
351
+ if (statsContainer) statsContainer.innerHTML = '';
352
+ if (selectedInfoContainer) selectedInfoContainer.innerHTML = '';
353
+
354
+ Object.keys(eventListeners).forEach(key => {
355
+ eventListeners[key] = [];
356
+ });
357
+ }
358
+
359
+ // Return EditorTsEditor instance
360
+ return {
361
+ page,
362
+ on,
363
+ off,
364
+ refresh,
365
+ save,
366
+ destroy,
367
+ elements: {
368
+ iframe,
369
+ sidebar: sidebarContainer || undefined,
370
+ stats: statsContainer || undefined,
371
+ selectedInfo: selectedInfoContainer || undefined,
372
+ }
373
+ };
374
+ }
@@ -0,0 +1,109 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
2
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
3
+
4
+ /* Set Noto Sans as the default font family */
5
+ :root {
6
+ font-family: "Noto Sans", sans-serif;
7
+ }
8
+
9
+ /* Theme variables */
10
+ :root {
11
+ /* Font families */
12
+ --font-main: 'Noto Sans', sans-serif;
13
+ --font-secondary: 'Poppins', sans-serif;
14
+
15
+ /* Dark mode colors */
16
+ --color-catppuccin-mocha-dark: #1e1e2e;
17
+ --color-primary-dark-bg: #171717;
18
+ --color-secondary-dark-bg: #212121;
19
+ --color-primary-dark-border: #323232;
20
+ --color-dark-chat-bg: #2c2c2c;
21
+ --color-primary-dark-text: #FFFFFF;
22
+ --color-primary-dark-input-bg: #404040;
23
+ --color-dark-focus-ring: rgba(255, 255, 255, 0.3);
24
+ --color-dark-button-focus-ring: rgba(255, 255, 255, 0.4);
25
+
26
+ /* Light mode colors */
27
+ --color-editor-light-text: #212C3E;
28
+ --color-editor-light-bg: #EDF0F5;
29
+ --color-primary-bg: #F9F9F9;
30
+ --color-secondary-bg: #FFFFFF;
31
+ --color-sidemenu-bg: #F0F2F8;
32
+ --color-primary-border: #e5e7eb;
33
+ --color-primary-text: #000000;
34
+ --color-primary-input-bg: #f9f9f9;
35
+ --color-light-focus-ring: rgba(0, 0, 0, 0.2);
36
+ --color-light-button-focus-ring: rgba(0, 0, 0, 0.3);
37
+
38
+ /* Shadow styles */
39
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
40
+ --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
41
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
42
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
43
+ }
44
+
45
+ /* Apply font to all elements */
46
+ body {
47
+ font-family: var(--font-main);
48
+ }
49
+
50
+ /* Optional: Use Poppins for headings */
51
+ h1, h2, h3, h4, h5, h6 {
52
+ font-family: var(--font-secondary);
53
+ font-weight: 600;
54
+ }
55
+
56
+ /* Apply theme colors */
57
+ .bg-primary {
58
+ background-color: var(--color-primary-bg);
59
+ }
60
+
61
+ .bg-secondary {
62
+ background-color: var(--color-secondary-bg);
63
+ }
64
+
65
+ .bg-sidemenu {
66
+ background-color: var(--color-sidemenu-bg);
67
+ }
68
+
69
+ .text-primary {
70
+ color: var(--color-primary-text);
71
+ }
72
+
73
+ .border-primary {
74
+ border-color: var(--color-primary-border);
75
+ }
76
+
77
+ .input-bg {
78
+ background-color: var(--color-primary-input-bg);
79
+ }
80
+
81
+ /* Focus styles */
82
+ input:focus, textarea:focus, select:focus {
83
+ outline: none;
84
+ box-shadow: 0 0 0 3px var(--color-light-focus-ring);
85
+ }
86
+
87
+ button:focus {
88
+ outline: none;
89
+ box-shadow: 0 0 0 3px var(--color-light-button-focus-ring);
90
+ }
91
+
92
+ /* Dark mode support */
93
+ @media (prefers-color-scheme: dark) {
94
+ :root {
95
+ --color-primary-bg: var(--color-primary-dark-bg);
96
+ --color-secondary-bg: var(--color-secondary-dark-bg);
97
+ --color-primary-text: var(--color-primary-dark-text);
98
+ --color-primary-input-bg: var(--color-primary-dark-input-bg);
99
+ --color-primary-border: var(--color-primary-dark-border);
100
+ }
101
+
102
+ input:focus, textarea:focus, select:focus {
103
+ box-shadow: 0 0 0 3px var(--color-dark-focus-ring);
104
+ }
105
+
106
+ button:focus {
107
+ box-shadow: 0 0 0 3px var(--color-dark-button-focus-ring);
108
+ }
109
+ }