@dodlhuat/basix 1.3.4 → 1.3.5
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 +78 -12
- package/js/editor.d.ts +6 -1
- package/js/editor.js +48 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Basix 1.3.
|
|
1
|
+
# Basix 1.3.5
|
|
2
2
|
|
|
3
3
|
Basix is intended as a starter for the rapid development of a design. Each design element can be added individually to
|
|
4
4
|
include only the data required. It is using plain javascript / typescript and therefore is not dependent on any plugin.
|
|
@@ -830,25 +830,91 @@ new CodeViewer('#output', '.card { padding: 1rem; }', 'css');
|
|
|
830
830
|
|
|
831
831
|
### Editor
|
|
832
832
|
|
|
833
|
-
The Editor component provides a contenteditable rich-text editing area with undo/redo, word count, and an optional side panel showing the raw HTML source and a live preview.
|
|
833
|
+
The Editor component provides a contenteditable rich-text editing area with undo/redo, word count, and an optional side panel showing the raw HTML source and a live preview. Internal elements are located by `data-editor` attributes scoped to a `root` container — no globally unique IDs required, which makes multiple editors on the same page possible.
|
|
834
834
|
|
|
835
835
|
``` html
|
|
836
|
-
<div
|
|
837
|
-
|
|
838
|
-
<
|
|
839
|
-
<
|
|
840
|
-
<
|
|
841
|
-
<
|
|
836
|
+
<div class="editor" id="my-editor">
|
|
837
|
+
<div class="editor-toolbar">
|
|
838
|
+
<button data-editor-action="undo" title="Undo">↩</button>
|
|
839
|
+
<button data-editor-action="redo" title="Redo">↪</button>
|
|
840
|
+
<button data-cmd="bold">B</button>
|
|
841
|
+
<button data-cmd="italic">I</button>
|
|
842
|
+
<button data-editor-action="link">Link</button>
|
|
843
|
+
<!-- … -->
|
|
844
|
+
</div>
|
|
845
|
+
<div class="editor-body">
|
|
846
|
+
<div class="editor-main">
|
|
847
|
+
<div data-editor="editable" class="editable" contenteditable="true"></div>
|
|
848
|
+
</div>
|
|
849
|
+
<!-- Full mode: add side panel with code + preview -->
|
|
850
|
+
<div data-editor="side-panel" class="editor-side">
|
|
851
|
+
<div class="side-tabs">
|
|
852
|
+
<button class="side-tab active" data-tab="code-panel">HTML</button>
|
|
853
|
+
<button class="side-tab" data-tab="preview-panel">Preview</button>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="side-panels">
|
|
856
|
+
<div class="side-panel active" data-editor="code-panel">
|
|
857
|
+
<textarea data-editor="code"></textarea>
|
|
858
|
+
</div>
|
|
859
|
+
<div class="side-panel" data-editor="preview-panel">
|
|
860
|
+
<div data-editor="preview" class="preview-content"></div>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
<div class="editor-footer">
|
|
866
|
+
<span data-editor="wordcount">0 words</span>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
842
869
|
```
|
|
843
870
|
|
|
844
871
|
``` js
|
|
845
|
-
//
|
|
846
|
-
new Editor();
|
|
872
|
+
// Single editor
|
|
873
|
+
new Editor({ root: '#my-editor' });
|
|
874
|
+
|
|
875
|
+
// Simple mode — only requires [data-editor="editable"], hides the side panel
|
|
876
|
+
new Editor({ root: '#my-editor', simple: true });
|
|
877
|
+
|
|
878
|
+
// Multiple editors on the same page
|
|
879
|
+
const editor1 = new Editor({ root: '#editor-1' });
|
|
880
|
+
const editor2 = new Editor({ root: '#editor-2' });
|
|
847
881
|
|
|
848
|
-
|
|
849
|
-
new Editor({ simple: true });
|
|
882
|
+
editor1.destroy();
|
|
850
883
|
```
|
|
851
884
|
|
|
885
|
+
#### Options
|
|
886
|
+
|
|
887
|
+
| Option | Type | Default | Description |
|
|
888
|
+
|---|---|---|---|
|
|
889
|
+
| `root` | string \| HTMLElement | `document.body` | Root container. Required when multiple editors coexist on the same page. |
|
|
890
|
+
| `simple` | boolean | `false` | Simple mode requires only `[data-editor="editable"]`. Full mode additionally requires `code`, `preview`, `side-panel`, `code-panel`, and `preview-panel` elements. |
|
|
891
|
+
|
|
892
|
+
#### `data-editor` elements
|
|
893
|
+
|
|
894
|
+
| Value | Element | Required |
|
|
895
|
+
|---|---|---|
|
|
896
|
+
| `editable` | `contenteditable` div — the writing area | always |
|
|
897
|
+
| `code` | `<textarea>` — HTML source view | full mode |
|
|
898
|
+
| `preview` | `<div>` — live preview | full mode |
|
|
899
|
+
| `side-panel` | Side panel container | full mode |
|
|
900
|
+
| `code-panel` | Code tab panel (matches `data-tab="code-panel"`) | full mode |
|
|
901
|
+
| `preview-panel` | Preview tab panel | full mode |
|
|
902
|
+
| `wordcount` | Word count display | optional |
|
|
903
|
+
| `image-file` | `<input type="file">` for image insertion | optional |
|
|
904
|
+
|
|
905
|
+
#### `data-editor-action` buttons
|
|
906
|
+
|
|
907
|
+
| Value | Description |
|
|
908
|
+
|---|---|
|
|
909
|
+
| `undo` | Triggers undo |
|
|
910
|
+
| `redo` | Triggers redo |
|
|
911
|
+
| `link` | Opens URL prompt for link insertion |
|
|
912
|
+
| `image` | Triggers the image file picker |
|
|
913
|
+
| `save` | Downloads content as an HTML file (Ctrl+S) |
|
|
914
|
+
| `clear` | Clears all content |
|
|
915
|
+
| `clean` | Strips formatting from the current selection |
|
|
916
|
+
| `toggle-code` | Shows / hides the HTML side panel |
|
|
917
|
+
|
|
852
918
|
### Custom Scrollbar
|
|
853
919
|
|
|
854
920
|
The Scrollbar component creates custom-styled scrollbars. Supports pointer/touch dragging, track clicking, and automatic thumb sizing. Can be used with any class.
|
package/js/editor.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
interface EditorOptions {
|
|
2
2
|
/** Hides the entire side panel (code/preview) permanently. Safe to use
|
|
3
|
-
* without
|
|
3
|
+
* without [data-editor="code"], [data-editor="preview"], or [data-editor="side-panel"] in the DOM. */
|
|
4
4
|
simple?: boolean;
|
|
5
|
+
/** Root container element or CSS selector. Required when using multiple editors on one page. */
|
|
6
|
+
root?: string | HTMLElement;
|
|
5
7
|
}
|
|
6
8
|
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
7
9
|
declare class Editor {
|
|
10
|
+
private readonly root;
|
|
8
11
|
private readonly editable;
|
|
9
12
|
private readonly code;
|
|
10
13
|
private readonly preview;
|
|
@@ -14,6 +17,8 @@ declare class Editor {
|
|
|
14
17
|
private redoStack;
|
|
15
18
|
private abortController;
|
|
16
19
|
constructor(options?: EditorOptions);
|
|
20
|
+
private q;
|
|
21
|
+
private qAll;
|
|
17
22
|
private bindToolbar;
|
|
18
23
|
private bindActions;
|
|
19
24
|
private bindKeyboard;
|
package/js/editor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sanitizeHtml } from './utils.js';
|
|
2
2
|
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
3
3
|
class Editor {
|
|
4
|
+
root;
|
|
4
5
|
editable;
|
|
5
6
|
code;
|
|
6
7
|
preview;
|
|
@@ -10,24 +11,35 @@ class Editor {
|
|
|
10
11
|
redoStack = [];
|
|
11
12
|
abortController = new AbortController();
|
|
12
13
|
constructor(options = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error('Editor: #editable element not found');
|
|
14
|
+
if (options.root instanceof HTMLElement) {
|
|
15
|
+
this.root = options.root;
|
|
16
16
|
}
|
|
17
|
+
else if (typeof options.root === 'string') {
|
|
18
|
+
const el = document.querySelector(options.root);
|
|
19
|
+
if (!el)
|
|
20
|
+
throw new Error(`Editor: root "${options.root}" not found`);
|
|
21
|
+
this.root = el;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.root = document.body;
|
|
25
|
+
}
|
|
26
|
+
const editable = this.q('[data-editor="editable"]');
|
|
27
|
+
if (!editable)
|
|
28
|
+
throw new Error('Editor: [data-editor="editable"] element not found');
|
|
17
29
|
this.editable = editable;
|
|
18
|
-
this.wordCount =
|
|
30
|
+
this.wordCount = this.q('[data-editor="wordcount"]');
|
|
19
31
|
if (options.simple) {
|
|
20
32
|
this.code = null;
|
|
21
33
|
this.preview = null;
|
|
22
|
-
this.sidePanel =
|
|
34
|
+
this.sidePanel = this.q('[data-editor="side-panel"]');
|
|
23
35
|
this.sidePanel?.classList.add('hidden');
|
|
24
36
|
}
|
|
25
37
|
else {
|
|
26
|
-
const code =
|
|
27
|
-
const preview =
|
|
28
|
-
const sidePanel =
|
|
38
|
+
const code = this.q('[data-editor="code"]');
|
|
39
|
+
const preview = this.q('[data-editor="preview"]');
|
|
40
|
+
const sidePanel = this.q('[data-editor="side-panel"]');
|
|
29
41
|
if (!code || !preview || !sidePanel) {
|
|
30
|
-
throw new Error('Editor:
|
|
42
|
+
throw new Error('Editor: [data-editor="code"], [data-editor="preview"] and [data-editor="side-panel"] are required unless simple: true');
|
|
31
43
|
}
|
|
32
44
|
this.code = code;
|
|
33
45
|
this.preview = preview;
|
|
@@ -42,9 +54,15 @@ class Editor {
|
|
|
42
54
|
this.syncViews();
|
|
43
55
|
this.saveState();
|
|
44
56
|
}
|
|
57
|
+
q(selector) {
|
|
58
|
+
return this.root.querySelector(selector);
|
|
59
|
+
}
|
|
60
|
+
qAll(selector) {
|
|
61
|
+
return this.root.querySelectorAll(selector);
|
|
62
|
+
}
|
|
45
63
|
bindToolbar() {
|
|
46
64
|
const sig = { signal: this.abortController.signal };
|
|
47
|
-
|
|
65
|
+
this.qAll('[data-cmd]').forEach(btn => {
|
|
48
66
|
btn.addEventListener('click', () => {
|
|
49
67
|
const cmd = btn.dataset.cmd;
|
|
50
68
|
const val = btn.dataset.value ?? null;
|
|
@@ -55,13 +73,13 @@ class Editor {
|
|
|
55
73
|
}
|
|
56
74
|
bindActions() {
|
|
57
75
|
const sig = { signal: this.abortController.signal };
|
|
58
|
-
|
|
76
|
+
this.q('[data-editor-action="link"]')?.addEventListener('click', () => {
|
|
59
77
|
const url = prompt('Enter URL:', 'https://');
|
|
60
78
|
if (url)
|
|
61
79
|
this.exec('createLink', url);
|
|
62
80
|
}, sig);
|
|
63
|
-
const imageFile =
|
|
64
|
-
|
|
81
|
+
const imageFile = this.q('[data-editor="image-file"]');
|
|
82
|
+
this.q('[data-editor-action="image"]')?.addEventListener('click', () => imageFile?.click(), sig);
|
|
65
83
|
imageFile?.addEventListener('change', () => {
|
|
66
84
|
const file = imageFile.files?.[0];
|
|
67
85
|
if (!file)
|
|
@@ -75,7 +93,7 @@ class Editor {
|
|
|
75
93
|
reader.readAsDataURL(file);
|
|
76
94
|
imageFile.value = '';
|
|
77
95
|
}, sig);
|
|
78
|
-
|
|
96
|
+
this.q('[data-editor-action="clean"]')?.addEventListener('click', () => {
|
|
79
97
|
const sel = window.getSelection();
|
|
80
98
|
if (!sel || sel.rangeCount === 0)
|
|
81
99
|
return;
|
|
@@ -85,16 +103,15 @@ class Editor {
|
|
|
85
103
|
range.insertNode(document.createTextNode(text));
|
|
86
104
|
this.onContentChange();
|
|
87
105
|
}, sig);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
this.q('[data-editor-action="undo"]')?.addEventListener('click', () => this.undo(), sig);
|
|
107
|
+
this.q('[data-editor-action="redo"]')?.addEventListener('click', () => this.redo(), sig);
|
|
108
|
+
this.q('[data-editor-action="toggle-code"]')?.addEventListener('click', () => {
|
|
91
109
|
this.sidePanel?.classList.toggle('hidden');
|
|
92
110
|
this.syncViews();
|
|
93
111
|
}, sig);
|
|
94
|
-
// Code action buttons — matched by position within .code-actions
|
|
95
112
|
if (this.code) {
|
|
96
113
|
const code = this.code;
|
|
97
|
-
const codeActions =
|
|
114
|
+
const codeActions = this.qAll('.code-actions button');
|
|
98
115
|
codeActions[0]?.addEventListener('click', () => {
|
|
99
116
|
this.editable.innerHTML = sanitizeHtml(code.value);
|
|
100
117
|
this.onContentChange();
|
|
@@ -111,9 +128,8 @@ class Editor {
|
|
|
111
128
|
.trim();
|
|
112
129
|
}, sig);
|
|
113
130
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
document.getElementById('clearBtn')?.addEventListener('click', () => {
|
|
131
|
+
this.q('[data-editor-action="save"]')?.addEventListener('click', () => this.downloadHTML(), sig);
|
|
132
|
+
this.q('[data-editor-action="clear"]')?.addEventListener('click', () => {
|
|
117
133
|
if (confirm('Clear all content?')) {
|
|
118
134
|
this.editable.innerHTML = '';
|
|
119
135
|
this.onContentChange();
|
|
@@ -121,8 +137,9 @@ class Editor {
|
|
|
121
137
|
}, sig);
|
|
122
138
|
}
|
|
123
139
|
bindKeyboard() {
|
|
124
|
-
const saveBtn = document.getElementById('saveBtn');
|
|
125
140
|
window.addEventListener('keydown', (e) => {
|
|
141
|
+
if (!this.root.contains(document.activeElement))
|
|
142
|
+
return;
|
|
126
143
|
const mod = e.ctrlKey || e.metaKey;
|
|
127
144
|
if (!mod)
|
|
128
145
|
return;
|
|
@@ -147,7 +164,7 @@ class Editor {
|
|
|
147
164
|
}
|
|
148
165
|
else if (key === 's') {
|
|
149
166
|
e.preventDefault();
|
|
150
|
-
|
|
167
|
+
this.q('[data-editor-action="save"]')?.click();
|
|
151
168
|
}
|
|
152
169
|
else if (key === 'z' && !e.shiftKey) {
|
|
153
170
|
e.preventDefault();
|
|
@@ -172,13 +189,13 @@ class Editor {
|
|
|
172
189
|
}
|
|
173
190
|
bindTabs() {
|
|
174
191
|
const sig = { signal: this.abortController.signal };
|
|
175
|
-
|
|
192
|
+
this.qAll('.side-tab[data-tab]').forEach(tab => {
|
|
176
193
|
tab.addEventListener('click', () => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
const target = tab.dataset.tab;
|
|
195
|
+
this.qAll('.side-tab').forEach(t => t.classList.remove('active'));
|
|
196
|
+
this.qAll('.side-panel').forEach(p => p.classList.remove('active'));
|
|
180
197
|
tab.classList.add('active');
|
|
181
|
-
|
|
198
|
+
this.q(`[data-editor="${target}"]`)?.classList.add('active');
|
|
182
199
|
}, sig);
|
|
183
200
|
});
|
|
184
201
|
}
|
|
@@ -365,7 +382,7 @@ class Editor {
|
|
|
365
382
|
const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
|
|
366
383
|
for (const line of lines) {
|
|
367
384
|
const li = document.createElement('li');
|
|
368
|
-
li.textContent = line.trim() || '
|
|
385
|
+
li.textContent = line.trim() || '';
|
|
369
386
|
list.appendChild(li);
|
|
370
387
|
}
|
|
371
388
|
range.deleteContents();
|
|
@@ -439,7 +456,7 @@ ${content}
|
|
|
439
456
|
const element = container.nodeType === Node.TEXT_NODE
|
|
440
457
|
? container.parentElement
|
|
441
458
|
: container;
|
|
442
|
-
|
|
459
|
+
this.qAll('[data-cmd]').forEach(btn => {
|
|
443
460
|
const cmd = btn.dataset.cmd;
|
|
444
461
|
let active = false;
|
|
445
462
|
let current = element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dodlhuat/basix",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Basix is intended as a starter for the rapid development of a design. Each design element can be added individually to include only the data required. It is using plain javascript / typescript and therefore is not dependent on any plugin.",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./css/*": "./css/*",
|