@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 CHANGED
@@ -1,4 +1,4 @@
1
- # Basix 1.3.4
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. Requires a `#editable` element in the DOM.
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 id="editable" contenteditable="true"></div>
837
- <!-- Optional side panel elements -->
838
- <textarea id="code"></textarea>
839
- <div id="preview"></div>
840
- <div id="sidePanel"></div>
841
- <span id="wordCount"></span>
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
- // Full editor with side panel
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
- // Simple mode — hides the side panel permanently
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 #code, #preview, or #sidePanel in the DOM. */
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
- const editable = document.getElementById('editable');
14
- if (!editable) {
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 = document.getElementById('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 = document.getElementById('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 = document.getElementById('code');
27
- const preview = document.getElementById('preview');
28
- const sidePanel = document.getElementById('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: #code, #preview and #sidePanel are required unless simple: true');
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
- document.querySelectorAll('[data-cmd]').forEach(btn => {
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
- document.getElementById('linkBtn')?.addEventListener('click', () => {
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 = document.getElementById('imageFile');
64
- document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click(), sig);
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
- document.getElementById('cleanBtn')?.addEventListener('click', () => {
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
- document.getElementById('undoBtn')?.addEventListener('click', () => this.undo(), sig);
89
- document.getElementById('redoBtn')?.addEventListener('click', () => this.redo(), sig);
90
- document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
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 = document.querySelectorAll('.code-actions button');
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
- const saveBtn = document.getElementById('saveBtn');
115
- saveBtn?.addEventListener('click', () => this.downloadHTML(), sig);
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
- saveBtn?.click();
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
- document.querySelectorAll('.side-tab[data-tab]').forEach(tab => {
192
+ this.qAll('.side-tab[data-tab]').forEach(tab => {
176
193
  tab.addEventListener('click', () => {
177
- const targetId = tab.dataset.tab;
178
- document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
179
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
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
- document.getElementById(targetId)?.classList.add('active');
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() || '\u200B';
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
- document.querySelectorAll('[data-cmd]').forEach(btn => {
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.4",
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/*",