@pfmcodes/caret 0.3.1 → 0.3.2

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
@@ -191,7 +191,7 @@ pnpm add @pfmcodes/caret
191
191
  <div id="result"></div>
192
192
 
193
193
  <script type="module">
194
- import Caret from '.node_modules/@pfmcodes/caret/index.js';
194
+ import Caret from './index.js';
195
195
 
196
196
  window.language = 'javascript';
197
197
  window.currentTheme = 'tokyo-night-dark';
@@ -239,13 +239,12 @@ for i in range(25):
239
239
  const editorInstance = await Caret.createEditor(
240
240
  document.getElementById('editor'),
241
241
  jsCode,
242
- 'demo-editor',
243
242
  {
244
243
  dark: true,
245
244
  language: 'javascript',
245
+ id: `${Math.floor(Math.random() * 1000000000)}`,
246
246
  hlTheme: 'tokyo-night-dark',
247
247
  focusColor: '#7116d8',
248
- id: Math.floor(Math.random() * 1000000000),
249
248
  theme: {
250
249
  dark: {
251
250
  'background.editor': '#1a1a2e',
@@ -264,7 +263,7 @@ for i in range(25):
264
263
  }
265
264
  }
266
265
  );
267
-
266
+
268
267
  window.editorInstance = editorInstance;
269
268
 
270
269
  window.changeLanguage = async (lang) => {
@@ -358,8 +357,8 @@ import Caret from './node_modules/@pfmcodes/caret/index.js';
358
357
  const editor = await Caret.createEditor(
359
358
  document.getElementById('editor'), // parent element
360
359
  'const x = 42;', // initial content
361
- 'my-editor', // unique id
362
360
  {
361
+ id: 'my-editor', // unique id
363
362
  dark: true,
364
363
  language: 'javascript',
365
364
  hlTheme: 'tokyo-night-dark'
@@ -375,10 +374,10 @@ import Caret from './node_modules/@pfmcodes/caret/index.js';
375
374
  const editor = await Caret.createEditor(
376
375
  document.getElementById('editor'),
377
376
  code,
378
- 'readonly-editor',
379
377
  {
380
378
  dark: true,
381
379
  language: 'python',
380
+ id: 'readonly-editor',
382
381
  hlTheme: 'github-dark',
383
382
  lock: true
384
383
  }
@@ -391,8 +390,8 @@ const editor = await Caret.createEditor(
391
390
  import Caret from './node_modules/@pfmcodes/caret/index.js';
392
391
 
393
392
  // each editor needs a unique id
394
- const editor1 = await Caret.createEditor(el1, code1, 'editor-1', options);
395
- const editor2 = await Caret.createEditor(el2, code2, 'editor-2', options);
393
+ const editor1 = await Caret.createEditor(el1, code1, {id: 'editor-1', ...options});
394
+ const editor2 = await Caret.createEditor(el2, code2, { id: 'editor-2', ...options });
396
395
  ```
397
396
 
398
397
  ### Custom Theme
@@ -465,17 +464,18 @@ Creates a new editor instance.
465
464
 
466
465
  **Options:**
467
466
 
468
- | Option | Type | Default | Description |
469
- |--------|------|---------|-------------|
470
- | `dark` | `boolean` | `false` | Dark mode |
471
- | `shadow` | `boolean` | `true` | Box shadow |
472
- | `focusColor` | `string` | `#7c3aed` | Border color on focus |
473
- | `shadowColor` | `string` | `#000` | Shadow color |
474
- | `lock` | `boolean` | `false` | Read-only mode |
475
- | `language` | `string` | `plaintext` | Highlight.js language |
476
- | `hlTheme` | `string` | `hybrid` | Highlight.js theme |
477
- | `font` | `object` | | Custom font `{ url, name }` |
478
- | `theme` | `object` | — | Custom colors (see above) |
467
+ | Option | Type | Default | Required | Description |
468
+ |--------|------|---------|----------|-------------|
469
+ | `dark` | `boolean` | `false` | ✕ | Dark mode |
470
+ | `id` | `string` | - | | required to distinguish between multiple instances |
471
+ | `shadow` | `boolean` | `true` | | Box shadow |
472
+ | `focusColor` | `string` | `#7c3aed` | | Border color on focus |
473
+ | `shadowColor` | `string` | `#000` | | Shadow color |
474
+ | `lock` | `boolean` | `false` | | Read-only mode |
475
+ | `language` | `string` | `plaintext` | ✓ | Highlight.js language |
476
+ | `hlTheme` | `string` | `hybrid` | | Highlight.js theme |
477
+ | `font` | `object` | — | ✕ | Custom font `{ url, name }` |
478
+ | `theme` | `object` | — | ✕ | Custom colors ([see more.](#default-colors)) |
479
479
 
480
480
  **Returns:** `Promise<EditorInstance>`
481
481
 
@@ -520,6 +520,23 @@ await editor.setLanguage('python');
520
520
  editor.delete();
521
521
  ```
522
522
 
523
+ ### Default Colors
524
+ from `option`:
525
+ | Property | Default |
526
+ |----------|---------|
527
+ | focusColor| #7c3aed |
528
+ | shadowColor | #000000 |
529
+ ---
530
+
531
+ from `theme`:
532
+ | Property | Dark | Light |
533
+ |----------|------|-------|
534
+ | `background.editor` | #101010 | #d4d4d4 |
535
+ | `editor.caret` | #ffffff | #111111 |
536
+ | `color.editor` | #ffffff | #000000 |
537
+ | `color.lineCounter` | #ffffff | #111111 |
538
+ | `background.lineCounter` | #1e1e1e | #ffffff |
539
+
523
540
  ### Keyboard Shortcuts
524
541
 
525
542
  | Shortcut | Action |
@@ -2,8 +2,8 @@
2
2
  * createTextEditor {
3
3
  * @param {HTMLElement} parent - The element to append the editor to
4
4
  * @param {string} content - Initial content of the editor, default is ""
5
- * @param {string|number} id - Unique ID for this instance, used for undo/redo stack namespacing
6
5
  * @param {Object} options - Optional configuration {
6
+ * @param {string|number} id - Unique ID for this instance, used for undo/redo stack namespacing
7
7
  * @param {boolean} dark - dark theme enabled, false by default
8
8
  * @param {boolean} shadow - box shadow enabled, true by default
9
9
  * @param {string} focusColor - border color on focus, default #7c3aed
@@ -49,9 +49,9 @@
49
49
  *
50
50
  * @notes
51
51
  * - Requires Chrome/Chromium — uses EditContext API (not supported in Firefox/Safari yet)
52
- * - Undo/redo stacks accessible globally via window.caret[`undoStack.${id}`]
52
+ * - Undo/redo stacks accessible globally via window.caret.undoStack[id]
53
53
  * - \u200B (zero-width space) used internally for newline rendering, stripped from getValue()
54
- * - Keyboard shortcuts: Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo, Tab indent, Shift+Tab unindent
54
+ * - Keyboard shortcuts: Ctrl+Z: undo, Ctrl+Y / Ctrl+Shift+Z: redo, Tab: indent, Shift+Tab: unindent, Arrow keys(←→↓↑): navigate around
55
55
  * }
56
56
  */
57
57
 
@@ -64,7 +64,8 @@ import languages from "./languages.js";
64
64
 
65
65
  languages.init();
66
66
 
67
- async function createTextEditor(parent, content = "", id, options = {}) {
67
+ async function createTextEditor(parent, content = "", options = {}) {
68
+ const id = options.id;
68
69
  let onCursorMoveFn = null;
69
70
  async function isChromiumEngine() {
70
71
  if (navigator.userAgentData) {
@@ -103,21 +104,36 @@ async function createTextEditor(parent, content = "", id, options = {}) {
103
104
  }
104
105
  });
105
106
  if (id === undefined || id === null || (typeof id !== "string" && typeof id !== "number")) {
106
- console.error(`parameter 'id' of function createTextEditor must not be '${typeof id}', it must be a number or string`);
107
+ console.error(`parameter 'id' from options must not be a '${typeof id}', it must be a number or string, current value of 'id': ${options.id}`);
107
108
  return;
108
109
  }
109
110
  if (!parent || !(parent instanceof HTMLElement)) {
110
- console.error(`'parent' parameter of function 'createTextEditor' must be an HTMLElement`);
111
+ console.error(`'parent' parameter of function 'createTextEditor' must be an HTMLElement, must not be a ${typeof parent}, current value of 'parent': ${options.parent}`);
111
112
  return;
112
113
  }
113
114
  if (!("EditContext" in window)) {
114
115
  console.error("EditContext API is not supported in ", await getBrowserName());
115
116
  return;
116
117
  }
118
+ if (options.language === undefined || options.language === null || typeof options.language !== "string") {
119
+ console.error(`parameter 'language' from options must be a string, not be a '${typeof options.language}', current value of 'langauge': ${options.language}`);
120
+ return;
121
+ }
122
+
123
+ if (!window.caret) window.caret = {
124
+ instances: {},
125
+ undoStack: {},
126
+ redoStack: {}
127
+ };
128
+
129
+ if (window.caret.instances[id]) {
130
+ console.error(`Caret: instance with id "${id}" already exists`);
131
+ return;
132
+ }
117
133
 
118
- if (!window.caret) window.caret = {};
119
- window.caret[`undoStack.${id}`] = [{ content, cursor: 0 }];
120
- window.caret[`redoStack.${id}`] = [];
134
+ window.caret.instances[id] = true;
135
+ window.caret.undoStack[id] = [{ content, cursor: 0 }];
136
+ window.caret.redoStack[id] = [];
121
137
 
122
138
  const lock = options.lock || false;
123
139
  const focusColor = options.focusColor || '#7c3aed';
@@ -127,15 +143,44 @@ async function createTextEditor(parent, content = "", id, options = {}) {
127
143
  const theme = options.theme;
128
144
  const font = options.font || {};
129
145
  let language = options.language || "plaintext";
146
+ let cachedThemes = null;
147
+ let cachedBase16 = null;
148
+ let onThemeChangeFn = null;
149
+
150
+ const loadThemes = async () => {
151
+ if (!cachedThemes || !cachedBase16) {
152
+ const [themes, base16] = await Promise.all([
153
+ fetch('https://raw.githubusercontent.com/PFMCODES/Website./main/apps/caret/themes.json').then(res => res.json()),
154
+ fetch('https://raw.githubusercontent.com/PFMCODES/Website./main/apps/caret/themes-base16.json').then(res => res.json())
155
+ ]);
156
+
157
+ cachedThemes = themes;
158
+ cachedBase16 = base16;
159
+ }
160
+ };
161
+
162
+ const isValidTheme = async (theme) => {
163
+ if (!theme) return false;
164
+
165
+ await loadThemes();
166
+
167
+ const valid = cachedThemes.includes(theme) || cachedBase16.includes(theme);
168
+
169
+ if (!valid) {
170
+ console.warn(`${theme} is an invalid Highlight.js theme, defaulting to hybrid`);
171
+ }
172
+
173
+ return valid;
174
+ };
130
175
 
131
176
  const themeLink = document.createElement("link");
132
177
  themeLink.rel = "stylesheet";
133
178
  themeLink.id = `caret-theme-${id}`;
134
- themeLink.href = options.hlTheme
179
+ const validTheme = await isValidTheme(options.hlTheme);
180
+ themeLink.href = validTheme
135
181
  ? `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${options.hlTheme}.css`
136
182
  : `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
137
183
  document.head.appendChild(themeLink);
138
-
139
184
  if (!languages.registeredLanguages.includes(language)) {
140
185
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${language}.js`);
141
186
  languages.registerLanguage(language, mod.default);
@@ -202,7 +247,7 @@ async function createTextEditor(parent, content = "", id, options = {}) {
202
247
  if (options.theme) {
203
248
  caretColor = dark ? options.theme.dark["editor.caret"] : options.theme.light["editor.caret"];
204
249
  } else {
205
- caretColor = "#fff";
250
+ caretColor = dark ? "#fff" : "#111";
206
251
  }
207
252
 
208
253
  parent.style.display = "flex";
@@ -234,16 +279,16 @@ async function createTextEditor(parent, content = "", id, options = {}) {
234
279
  }
235
280
 
236
281
  function saveState() {
237
- const stack = window.caret[`undoStack.${id}`];
282
+ const stack = window.caret.undoStack[id];
238
283
  if (text !== stack[stack.length - 1]?.content) {
239
284
  stack.push({ content: text, cursor: selStart });
240
- window.caret[`redoStack.${id}`] = [];
285
+ window.caret.redoStack[id] = [];
241
286
  }
242
287
  }
243
288
 
244
289
  function undo() {
245
- const stack = window.caret[`undoStack.${id}`];
246
- const redoStack = window.caret[`redoStack.${id}`];
290
+ const stack = window.caret.undoStack[id];
291
+ const redoStack = window.caret.redoStack[id];
247
292
  if (stack.length <= 1) return;
248
293
  const current = stack.pop();
249
294
  redoStack.push(current);
@@ -257,8 +302,8 @@ async function createTextEditor(parent, content = "", id, options = {}) {
257
302
  }
258
303
 
259
304
  function redo() {
260
- const stack = window.caret[`undoStack.${id}`];
261
- const redoStack = window.caret[`redoStack.${id}`];
305
+ const stack = window.caret.undoStack[id];
306
+ const redoStack = window.caret.redoStack[id];
262
307
  if (redoStack.length === 0) return;
263
308
  const next = redoStack.pop();
264
309
  stack.push(next);
@@ -271,6 +316,7 @@ async function createTextEditor(parent, content = "", id, options = {}) {
271
316
 
272
317
  editContext.addEventListener("textupdate", (e) => {
273
318
  if (lock) return;
319
+ console.log("updateRange:", e.updateRangeStart, e.updateRangeEnd, "text:", e.text);
274
320
  text = text.slice(0, e.updateRangeStart) + e.text + text.slice(e.updateRangeEnd);
275
321
  text = text.replaceAll("\u200B", "");
276
322
  selStart = selEnd = e.selectionStart;
@@ -363,7 +409,22 @@ async function createTextEditor(parent, content = "", id, options = {}) {
363
409
  render();
364
410
  return;
365
411
  }
366
- if (e.key === "ArrowLeft") {
412
+ if (e.key === "ArrowLeft" && e.shiftKey) {
413
+ e.preventDefault();
414
+ selStart = Math.max(0, selStart - 1);
415
+ editContext.updateSelection(selStart, selEnd);
416
+ caret.update(selStart);
417
+ return;
418
+ }
419
+
420
+ if (e.key === "ArrowRight" && e.shiftKey) {
421
+ e.preventDefault();
422
+ selEnd = Math.min(text.length, selEnd + 1);
423
+ editContext.updateSelection(selStart, selEnd);
424
+ caret.update(selEnd);
425
+ return;
426
+ }
427
+ if (e.key === "ArrowLeft" && !e.shiftKey) {
367
428
  e.preventDefault();
368
429
  selStart = selEnd = Math.max(0, selStart - 1);
369
430
  editContext.updateSelection(selStart, selEnd);
@@ -372,7 +433,7 @@ async function createTextEditor(parent, content = "", id, options = {}) {
372
433
  return;
373
434
  }
374
435
 
375
- if (e.key === "ArrowRight") {
436
+ if (e.key === "ArrowRight" && !e.shiftKey) {
376
437
  e.preventDefault();
377
438
  selStart = selEnd = Math.min(text.length, selStart + 1);
378
439
  editContext.updateSelection(selStart, selEnd);
@@ -381,7 +442,38 @@ async function createTextEditor(parent, content = "", id, options = {}) {
381
442
  return;
382
443
  }
383
444
 
384
- if (e.key === "ArrowUp") {
445
+ if (e.key === "ArrowUp" && e.shiftKey) {
446
+ e.preventDefault();
447
+ const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
448
+ const prevLineEnd = lineStart - 1;
449
+ const prevLineStart = text.lastIndexOf("\n", prevLineEnd - 1) + 1;
450
+ const col = selStart - lineStart;
451
+ const prevLineLength = prevLineEnd - prevLineStart;
452
+ selStart = prevLineStart + Math.min(col, prevLineLength);
453
+ // selEnd stays — extends selection upward
454
+ editContext.updateSelection(selStart, selEnd);
455
+ caret.update(selStart);
456
+ if (onCursorMoveFn) onCursorMoveFn(selStart);
457
+ return;
458
+ }
459
+
460
+ if (e.key === "ArrowDown" && e.shiftKey) {
461
+ e.preventDefault();
462
+ const lineStart = text.lastIndexOf("\n", selEnd - 1) + 1;
463
+ const nextLineStart = text.indexOf("\n", selEnd) + 1;
464
+ const nextLineEnd = text.indexOf("\n", nextLineStart);
465
+ const finalNextLineEnd = nextLineEnd === -1 ? text.length : nextLineEnd;
466
+ const col = selEnd - lineStart;
467
+ const nextLineLength = finalNextLineEnd - nextLineStart;
468
+ selEnd = nextLineStart + Math.min(col, nextLineLength);
469
+ // selStart stays — extends selection downward
470
+ editContext.updateSelection(selStart, selEnd);
471
+ caret.update(selEnd);
472
+ if (onCursorMoveFn) onCursorMoveFn(selEnd);
473
+ return;
474
+ }
475
+
476
+ if (e.key === "ArrowUp" && !e.shiftKey) {
385
477
  e.preventDefault();
386
478
  const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
387
479
  const prevLineEnd = lineStart - 1;
@@ -395,7 +487,7 @@ async function createTextEditor(parent, content = "", id, options = {}) {
395
487
  return;
396
488
  }
397
489
 
398
- if (e.key === "ArrowDown") {
490
+ if (e.key === "ArrowDown" && !e.shiftKey) {
399
491
  e.preventDefault();
400
492
  const lineStart = text.lastIndexOf("\n", selStart - 1) + 1;
401
493
  const nextLineStart = text.indexOf("\n", selStart) + 1;
@@ -438,6 +530,60 @@ async function createTextEditor(parent, content = "", id, options = {}) {
438
530
  caret.hide();
439
531
  });
440
532
 
533
+ let isMouseDown = false;
534
+ let dragStart = 0;
535
+
536
+ main.addEventListener("mousedown", (e) => {
537
+ isMouseDown = true;
538
+ main.focus();
539
+ const range = document.caretRangeFromPoint(e.clientX, e.clientY);
540
+ if (!range) return;
541
+
542
+ let offset = 0;
543
+ let remaining = 0;
544
+ const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
545
+ let node;
546
+ while ((node = walker.nextNode())) {
547
+ if (node === range.startContainer) {
548
+ offset = remaining + range.startOffset;
549
+ break;
550
+ }
551
+ remaining += node.textContent.length;
552
+ }
553
+
554
+ dragStart = offset;
555
+ selStart = selEnd = offset;
556
+ editContext.updateSelection(selStart, selEnd);
557
+ caret.update(selStart);
558
+ });
559
+
560
+ main.addEventListener("mousemove", (e) => {
561
+ if (!isMouseDown) return;
562
+ const range = document.caretRangeFromPoint(e.clientX, e.clientY);
563
+ if (!range) return;
564
+
565
+ let offset = 0;
566
+ let remaining = 0;
567
+ const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
568
+ let node;
569
+ while ((node = walker.nextNode())) {
570
+ if (node === range.startContainer) {
571
+ offset = remaining + range.startOffset;
572
+ break;
573
+ }
574
+ remaining += node.textContent.length;
575
+ }
576
+
577
+ selStart = Math.min(dragStart, offset);
578
+ selEnd = Math.max(dragStart, offset);
579
+ editContext.updateSelection(selStart, selEnd);
580
+ caret.update(selStart);
581
+ });
582
+
583
+ document.addEventListener("mouseup", () => {
584
+ isMouseDown = false;
585
+ });
586
+
441
587
  main.addEventListener("click", (e) => {
442
588
  main.focus();
443
589
  const range = document.caretRangeFromPoint(e.clientX, e.clientY);
@@ -482,6 +628,14 @@ async function createTextEditor(parent, content = "", id, options = {}) {
482
628
  id: options.id,
483
629
  onChange: (fn) => { onChangeFn = fn; },
484
630
  isFocused: () => isFocused,
631
+ setTheme: async (name) => {
632
+ const validTheme = await isValidTheme(name);
633
+ themeLink.href = validTheme
634
+ ? `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/${name}.css`
635
+ : `https://esm.sh/@pfmcodes/highlight.js@1.0.0/styles/hybrid.css`;
636
+ if (onThemeChangeFn) onThemeChangeFn(validTheme ? name : "hybrid");
637
+ },
638
+ onThemeChange: (cb) => { onThemeChangeFn = cb; },
485
639
  setLanguage: async (lang) => {
486
640
  if (!languages.registeredLanguages.includes(lang)) {
487
641
  const mod = await import(`https://esm.sh/@pfmcodes/highlight.js@1.0.0/es/languages/${lang}.js`);
@@ -490,12 +644,16 @@ async function createTextEditor(parent, content = "", id, options = {}) {
490
644
  language = lang;
491
645
  render();
492
646
  },
647
+ onCursorMove: (cb) => { onCursorMoveFn = cb },
493
648
  delete: () => {
494
649
  parent.removeChild(main);
495
650
  parent.removeChild(lineCounter);
496
651
  caret.destroy();
497
652
  document.head.removeChild(themeLink);
498
653
  parent.style = "";
654
+ delete window.caret.instances[id];
655
+ delete window.caret.undoStack[id];
656
+ delete window.caret.redoStack[id];
499
657
  }
500
658
  };
501
659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pfmcodes/caret",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "The official code editor engine for lexius",
5
5
  "type": "module",
6
6
  "main": "./index.js",