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.
- package/README.md +401 -0
- package/index.ts +81 -0
- package/package.json +42 -0
- package/src/core/AssetManager.ts +127 -0
- package/src/core/ComponentManager.ts +206 -0
- package/src/core/Page.ts +138 -0
- package/src/core/StyleManager.ts +221 -0
- package/src/core/ToolbarManager.ts +170 -0
- package/src/core/init.ts +374 -0
- package/src/styles/branding.css +109 -0
- package/src/types.ts +157 -0
- package/src/utils/helpers.ts +199 -0
- package/src/utils/toolbar.ts +140 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for the HTML content editing library
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PageData {
|
|
6
|
+
title: string;
|
|
7
|
+
item_id: number;
|
|
8
|
+
body: PageBody;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PageBody {
|
|
12
|
+
html: string;
|
|
13
|
+
components: string; // JSON string of Component[]
|
|
14
|
+
assets: Asset[];
|
|
15
|
+
css: string;
|
|
16
|
+
styles: Style[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Component {
|
|
20
|
+
type: string;
|
|
21
|
+
attributes?: Record<string, any>;
|
|
22
|
+
components?: Component[];
|
|
23
|
+
tagName?: string;
|
|
24
|
+
void?: boolean;
|
|
25
|
+
style?: string;
|
|
26
|
+
script?: string;
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ToolbarConfig {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
actions: ToolbarAction[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolbarAction {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
icon: string;
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
danger?: boolean;
|
|
41
|
+
description?: string;
|
|
42
|
+
handler?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Asset {
|
|
46
|
+
type: 'image' | 'video' | 'audio' | 'document';
|
|
47
|
+
src: string;
|
|
48
|
+
unitDim: 'px' | '%' | 'em' | 'rem';
|
|
49
|
+
height: number;
|
|
50
|
+
width: number;
|
|
51
|
+
blinkCDN?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Style {
|
|
55
|
+
selectors: (string | SelectorObject)[];
|
|
56
|
+
selectorsAdd?: string;
|
|
57
|
+
style: CSSProperties;
|
|
58
|
+
mediaText?: string;
|
|
59
|
+
atRuleType?: 'media' | 'keyframes' | 'supports';
|
|
60
|
+
state?: 'hover' | 'active' | 'focus' | 'visited';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SelectorObject {
|
|
64
|
+
name: string;
|
|
65
|
+
active?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type CSSProperties = Record<string, string>;
|
|
69
|
+
|
|
70
|
+
export interface ParsedComponents {
|
|
71
|
+
components: Component[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ComponentQuery {
|
|
75
|
+
id?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
attributes?: Record<string, any>;
|
|
78
|
+
tagName?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface StyleQuery {
|
|
82
|
+
selector?: string;
|
|
83
|
+
mediaText?: string;
|
|
84
|
+
state?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UpdateOptions {
|
|
88
|
+
merge?: boolean;
|
|
89
|
+
overwrite?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ToolbarRule {
|
|
93
|
+
selector: ComponentSelector;
|
|
94
|
+
config: ToolbarConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type ComponentSelector =
|
|
98
|
+
| { id: string }
|
|
99
|
+
| { type: string }
|
|
100
|
+
| { tagName: string }
|
|
101
|
+
| { attributes: Record<string, any> }
|
|
102
|
+
| { custom: (component: Component) => boolean };
|
|
103
|
+
|
|
104
|
+
export interface InitConfig {
|
|
105
|
+
// Required: iframe element ID (user creates this in their HTML)
|
|
106
|
+
iframeId: string;
|
|
107
|
+
|
|
108
|
+
// Required: page data
|
|
109
|
+
data: PageData | string;
|
|
110
|
+
|
|
111
|
+
// Optional: toolbar configuration (runtime only)
|
|
112
|
+
toolbars?: ToolbarInitConfig;
|
|
113
|
+
|
|
114
|
+
// Optional: UI container IDs (user controls placement)
|
|
115
|
+
ui?: {
|
|
116
|
+
sidebar?: {
|
|
117
|
+
containerId?: string; // Where to render sidebar (optional)
|
|
118
|
+
enabled?: boolean;
|
|
119
|
+
};
|
|
120
|
+
stats?: {
|
|
121
|
+
containerId?: string; // Where to render stats (optional)
|
|
122
|
+
enabled?: boolean;
|
|
123
|
+
};
|
|
124
|
+
selectedInfo?: {
|
|
125
|
+
containerId?: string; // Where to render selected component info (optional)
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Optional: event callbacks
|
|
131
|
+
onComponentSelect?: (component: Component) => void;
|
|
132
|
+
onComponentEdit?: (component: Component) => void;
|
|
133
|
+
onComponentDelete?: (component: Component) => void;
|
|
134
|
+
onComponentDuplicate?: (component: Component, duplicate: Component) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ToolbarInitConfig {
|
|
138
|
+
byId?: Record<string, ToolbarConfig>;
|
|
139
|
+
byType?: Record<string, ToolbarConfig>;
|
|
140
|
+
byTag?: Record<string, ToolbarConfig>;
|
|
141
|
+
default?: ToolbarConfig;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface EditorTsEditor {
|
|
145
|
+
page: any; // Page class (avoid circular dependency)
|
|
146
|
+
on(event: string, callback: Function): void;
|
|
147
|
+
off(event: string, callback: Function): void;
|
|
148
|
+
refresh(): void;
|
|
149
|
+
save(): string;
|
|
150
|
+
destroy(): void;
|
|
151
|
+
elements: {
|
|
152
|
+
iframe: HTMLIFrameElement;
|
|
153
|
+
sidebar?: HTMLElement;
|
|
154
|
+
stats?: HTMLElement;
|
|
155
|
+
selectedInfo?: HTMLElement;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Component, CSSProperties } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility functions for working with page data
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deep clone an object
|
|
9
|
+
*/
|
|
10
|
+
export function deepClone<T>(obj: T): T {
|
|
11
|
+
return JSON.parse(JSON.stringify(obj));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a unique ID
|
|
16
|
+
*/
|
|
17
|
+
export function generateId(prefix = 'id'): string {
|
|
18
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
19
|
+
const timestamp = Date.now().toString(36);
|
|
20
|
+
return `${prefix}-${timestamp}-${random}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert CSS object to CSS string
|
|
25
|
+
*/
|
|
26
|
+
export function cssObjectToString(css: CSSProperties): string {
|
|
27
|
+
return Object.entries(css)
|
|
28
|
+
.map(([key, value]) => `${key}: ${value};`)
|
|
29
|
+
.join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert CSS string to CSS object
|
|
34
|
+
*/
|
|
35
|
+
export function cssStringToObject(cssString: string): CSSProperties {
|
|
36
|
+
const css: CSSProperties = {};
|
|
37
|
+
const declarations = cssString.split(';').filter((d) => d.trim());
|
|
38
|
+
|
|
39
|
+
for (const declaration of declarations) {
|
|
40
|
+
const [key, ...valueParts] = declaration.split(':');
|
|
41
|
+
if (key && valueParts.length > 0) {
|
|
42
|
+
css[key.trim()] = valueParts.join(':').trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return css;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Flatten component tree to array
|
|
51
|
+
*/
|
|
52
|
+
export function flattenComponents(components: Component[]): Component[] {
|
|
53
|
+
const result: Component[] = [];
|
|
54
|
+
|
|
55
|
+
function traverse(comps: Component[]) {
|
|
56
|
+
for (const comp of comps) {
|
|
57
|
+
result.push(comp);
|
|
58
|
+
if (comp.components && comp.components.length > 0) {
|
|
59
|
+
traverse(comp.components);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
traverse(components);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find component by path (e.g., "0.1.2" for first component, second child, third grandchild)
|
|
70
|
+
*/
|
|
71
|
+
export function getComponentByPath(components: Component[], path: string): Component | null {
|
|
72
|
+
const indices = path.split('.').map(Number);
|
|
73
|
+
let current: Component[] = components;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < indices.length; i++) {
|
|
76
|
+
const index = indices[i];
|
|
77
|
+
if (index === undefined || !current[index]) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (i === indices.length - 1) {
|
|
82
|
+
return current[index]!;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
current = current[index]!.components || [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sanitize HTML string
|
|
93
|
+
*/
|
|
94
|
+
export function sanitizeHTML(html: string): string {
|
|
95
|
+
return html
|
|
96
|
+
.replace(/&/g, '&')
|
|
97
|
+
.replace(/</g, '<')
|
|
98
|
+
.replace(/>/g, '>')
|
|
99
|
+
.replace(/"/g, '"')
|
|
100
|
+
.replace(/'/g, ''');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract all IDs from component tree
|
|
105
|
+
*/
|
|
106
|
+
export function extractComponentIds(components: Component[]): string[] {
|
|
107
|
+
const ids: string[] = [];
|
|
108
|
+
|
|
109
|
+
function traverse(comps: Component[]) {
|
|
110
|
+
for (const comp of comps) {
|
|
111
|
+
if (comp.attributes?.id) {
|
|
112
|
+
ids.push(comp.attributes.id as string);
|
|
113
|
+
}
|
|
114
|
+
if (comp.components && comp.components.length > 0) {
|
|
115
|
+
traverse(comp.components);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
traverse(components);
|
|
121
|
+
return ids;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Merge CSS properties (later properties override earlier ones)
|
|
126
|
+
*/
|
|
127
|
+
export function mergeCSSProperties(...properties: CSSProperties[]): CSSProperties {
|
|
128
|
+
return Object.assign({}, ...properties);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if a selector is an ID selector
|
|
133
|
+
*/
|
|
134
|
+
export function isIdSelector(selector: string): boolean {
|
|
135
|
+
return selector.startsWith('#');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if a selector is a class selector
|
|
140
|
+
*/
|
|
141
|
+
export function isClassSelector(selector: string): boolean {
|
|
142
|
+
return selector.startsWith('.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse selector to extract name
|
|
147
|
+
*/
|
|
148
|
+
export function parseSelector(selector: string): { type: 'id' | 'class' | 'tag' | 'complex'; name: string } {
|
|
149
|
+
if (isIdSelector(selector)) {
|
|
150
|
+
return { type: 'id', name: selector.substring(1) };
|
|
151
|
+
}
|
|
152
|
+
if (isClassSelector(selector)) {
|
|
153
|
+
return { type: 'class', name: selector.substring(1) };
|
|
154
|
+
}
|
|
155
|
+
if (selector.includes(' ') || selector.includes('>') || selector.includes('+')) {
|
|
156
|
+
return { type: 'complex', name: selector };
|
|
157
|
+
}
|
|
158
|
+
return { type: 'tag', name: selector };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format file size in bytes to human-readable format
|
|
163
|
+
*/
|
|
164
|
+
export function formatFileSize(bytes: number): string {
|
|
165
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
166
|
+
let size = bytes;
|
|
167
|
+
let unitIndex = 0;
|
|
168
|
+
|
|
169
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
170
|
+
size /= 1024;
|
|
171
|
+
unitIndex++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate URL
|
|
179
|
+
*/
|
|
180
|
+
export function isValidURL(url: string): boolean {
|
|
181
|
+
try {
|
|
182
|
+
new URL(url);
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extract domain from URL
|
|
191
|
+
*/
|
|
192
|
+
export function extractDomain(url: string): string | null {
|
|
193
|
+
try {
|
|
194
|
+
const urlObj = new URL(url);
|
|
195
|
+
return urlObj.hostname;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ToolbarConfig, ToolbarAction } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default toolbar configurations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default toolbar actions available for all components
|
|
9
|
+
*/
|
|
10
|
+
export const defaultToolbarActions: ToolbarAction[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 'edit',
|
|
13
|
+
label: 'Edit',
|
|
14
|
+
icon: '✏️',
|
|
15
|
+
enabled: true,
|
|
16
|
+
description: 'Edit component properties in sidebar',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'editJS',
|
|
20
|
+
label: 'Edit JS',
|
|
21
|
+
icon: '📜',
|
|
22
|
+
enabled: true,
|
|
23
|
+
description: 'Edit component JavaScript with Monaco editor',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'duplicate',
|
|
27
|
+
label: 'Duplicate',
|
|
28
|
+
icon: '📋',
|
|
29
|
+
enabled: true,
|
|
30
|
+
description: 'Create a copy of this component',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'delete',
|
|
34
|
+
label: 'Delete',
|
|
35
|
+
icon: '🗑️',
|
|
36
|
+
enabled: true,
|
|
37
|
+
danger: true,
|
|
38
|
+
description: 'Remove this component from the page',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default toolbar configuration
|
|
44
|
+
*/
|
|
45
|
+
export const defaultToolbarConfig: ToolbarConfig = {
|
|
46
|
+
enabled: true,
|
|
47
|
+
actions: defaultToolbarActions,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a custom toolbar config
|
|
52
|
+
*/
|
|
53
|
+
export function createToolbarConfig(
|
|
54
|
+
actions: ToolbarAction[],
|
|
55
|
+
enabled = true
|
|
56
|
+
): ToolbarConfig {
|
|
57
|
+
return {
|
|
58
|
+
enabled,
|
|
59
|
+
actions,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Merge toolbar configs (later config overrides earlier)
|
|
65
|
+
*/
|
|
66
|
+
export function mergeToolbarConfigs(
|
|
67
|
+
base: ToolbarConfig,
|
|
68
|
+
override: Partial<ToolbarConfig>
|
|
69
|
+
): ToolbarConfig {
|
|
70
|
+
return {
|
|
71
|
+
enabled: override.enabled ?? base.enabled,
|
|
72
|
+
actions: override.actions ?? base.actions,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get enabled actions from a toolbar config
|
|
78
|
+
*/
|
|
79
|
+
export function getEnabledActions(config: ToolbarConfig): ToolbarAction[] {
|
|
80
|
+
return config.actions.filter((action) => action.enabled);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find a specific action in toolbar config
|
|
85
|
+
*/
|
|
86
|
+
export function findToolbarAction(
|
|
87
|
+
config: ToolbarConfig,
|
|
88
|
+
actionId: string
|
|
89
|
+
): ToolbarAction | null {
|
|
90
|
+
const action = config.actions.find((a) => a.id === actionId);
|
|
91
|
+
return action || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Preset toolbar configs for common scenarios
|
|
96
|
+
*/
|
|
97
|
+
export const toolbarPresets = {
|
|
98
|
+
/**
|
|
99
|
+
* Full toolbar with all actions
|
|
100
|
+
*/
|
|
101
|
+
full: defaultToolbarConfig,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read-only toolbar (only view actions)
|
|
105
|
+
*/
|
|
106
|
+
readOnly: createToolbarConfig([
|
|
107
|
+
{
|
|
108
|
+
id: 'view',
|
|
109
|
+
label: 'View',
|
|
110
|
+
icon: '👁️',
|
|
111
|
+
enabled: true,
|
|
112
|
+
description: 'View component details',
|
|
113
|
+
},
|
|
114
|
+
]),
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Edit-only toolbar (no delete)
|
|
118
|
+
*/
|
|
119
|
+
editOnly: createToolbarConfig([
|
|
120
|
+
{ ...defaultToolbarActions[0]! }, // edit
|
|
121
|
+
{ ...defaultToolbarActions[1]! }, // editJS
|
|
122
|
+
{ ...defaultToolbarActions[2]! }, // duplicate
|
|
123
|
+
]),
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Minimal toolbar (edit and delete only)
|
|
127
|
+
*/
|
|
128
|
+
minimal: createToolbarConfig([
|
|
129
|
+
{ ...defaultToolbarActions[0]! }, // edit
|
|
130
|
+
{ ...defaultToolbarActions[3]! }, // delete
|
|
131
|
+
]),
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Disabled toolbar
|
|
135
|
+
*/
|
|
136
|
+
disabled: {
|
|
137
|
+
enabled: false,
|
|
138
|
+
actions: [],
|
|
139
|
+
},
|
|
140
|
+
};
|