@zikojs/notebook 0.10.0 → 0.11.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zikojs/notebook",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A lightweight, extensible JavaScript notebook component with Markdown, executable cells, persistent state, and pluggable runtimes.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -166,47 +166,62 @@ export function createCodeEditor(app, cellData, actions) {
166
166
  ? view.state.doc.toString()
167
167
  : cellData.code,
168
168
 
169
- sync: (isActive, isMarkdownView) => {
170
- if (!isMarkdownView) {
171
- if (!view) {
172
- requestAnimationFrame(() => initEditor());
173
- } else {
174
- view.dispatch({
175
- effects: [
176
- languageCompartment.reconfigure(
177
- languageFor(cellData.type)
178
- ),
179
-
180
- readOnlyCompartment.reconfigure(
181
- EditorState.readOnly.of(cellData.readonly)
182
- ),
183
-
184
- editableCompartment.reconfigure(
185
- EditorView.editable.of(!cellData.readonly)
186
- )
187
- ]
188
- });
189
-
190
- const currentValue = view.state.doc.toString();
191
-
192
- if (currentValue !== cellData.code) {
193
- view.dispatch({
194
- changes: {
195
- from: 0,
196
- to: currentValue.length,
197
- insert: cellData.code
198
- }
199
- });
200
- }
169
+
170
+ sync: (isActive, isMarkdownView) => {
171
+ if (!isMarkdownView) {
172
+ if (!view) {
173
+ requestAnimationFrame(() => {
174
+ initEditor();
175
+
176
+ // The editor may only become available after initialization.
177
+ // Check the current active cell, not a potentially stale
178
+ // isActive value captured by this callback.
179
+ if (
180
+ app.store.get("activeCellId") === cellData.id &&
181
+ !cellData.readonly &&
182
+ view
183
+ ) {
184
+ view.focus();
201
185
  }
186
+ });
187
+ } else {
188
+ view.dispatch({
189
+ effects: [
190
+ languageCompartment.reconfigure(
191
+ languageFor(cellData.type)
192
+ ),
193
+ readOnlyCompartment.reconfigure(
194
+ EditorState.readOnly.of(cellData.readonly)
195
+ ),
196
+ editableCompartment.reconfigure(
197
+ EditorView.editable.of(!cellData.readonly)
198
+ )
199
+ ]
200
+ });
201
+
202
+ const currentValue = view.state.doc.toString();
203
+
204
+ if (currentValue !== cellData.code) {
205
+ view.dispatch({
206
+ changes: {
207
+ from: 0,
208
+ to: currentValue.length,
209
+ insert: cellData.code
210
+ }
211
+ });
212
+ }
202
213
 
203
- if (isActive && view && !cellData.readonly) {
204
- requestAnimationFrame(() => view.focus());
205
- }
206
- } else if (view) {
207
- view.destroy();
208
- view = null;
214
+ if (
215
+ isActive &&
216
+ !cellData.readonly
217
+ ) {
218
+ requestAnimationFrame(() => view?.focus());
209
219
  }
210
220
  }
221
+ } else if (view) {
222
+ view.destroy();
223
+ view = null;
224
+ }
225
+ }
211
226
  };
212
227
  }
package/src/notebook.js CHANGED
@@ -424,8 +424,10 @@ export class UINotebook extends UIElement {
424
424
  app._refreshCell(cellData);
425
425
  },
426
426
 
427
- addBelow: () =>
428
- app.addCell(cellData.type, null, cellData.id),
427
+ addBelow: () => {
428
+ app.setActiveCell(cellData.id);
429
+ app.addCell(cellData.type, null, cellData.id);
430
+ },
429
431
 
430
432
  deleteCell: () => app.deleteCell(cellData.id),
431
433
 
@@ -523,117 +525,111 @@ export class UINotebook extends UIElement {
523
525
  });
524
526
  }
525
527
 
526
- focusCell(targetId) {
527
- this.store.get("cells").forEach((cell) => {
528
- if (
529
- cell.type === "markdown" &&
530
- cell.isEditingMarkdown &&
531
- cell.id !== targetId
532
- ) {
533
- cell.isEditingMarkdown = false;
534
- this._refreshCell(cell);
535
- }
536
- });
528
+ focusCell(id) {
529
+ const cells = this.store.get("cells");
530
+ const cell = cells.find((c) => c.id === id);
537
531
 
538
- const target = this.store
539
- .get("cells")
540
- .find((cell) => cell.id === targetId);
532
+ if (!cell) return;
541
533
 
542
- if (
543
- target &&
544
- target.type === "markdown" &&
545
- !target.readonly
546
- ) {
547
- target.isEditingMarkdown = true;
534
+ for (const c of cells) {
535
+ if (c.type === "markdown" && c.id !== id) {
536
+ c.isEditingMarkdown = false;
548
537
  }
538
+ }
549
539
 
550
- this.store.set("activeCellId", targetId);
540
+ if (cell.type === "markdown" && !cell.readonly) {
541
+ cell.isEditingMarkdown = true;
551
542
  }
552
543
 
553
- addCell(
554
- type = "code",
555
- initialCode = null,
556
- afterId = null,
557
- readonly = false
558
- ) {
559
- const cells = [...this.store.get("cells")];
560
-
561
- if (cells.length >= this.maxCells) {
562
- console.warn(
563
- `Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`
564
- );
544
+ this.store.set("activeCellId", id);
545
+ this._refreshCell(cell);
546
+
547
+ // Scroll the target cell into view when it is outside the viewport.
548
+ requestAnimationFrame(() => {
549
+ const element = cell._dom;
565
550
 
566
- return null;
551
+ if (!element?.isConnected) return;
552
+
553
+ const rect = element.getBoundingClientRect();
554
+ const viewportHeight = window.innerHeight;
555
+
556
+ if (rect.top < 0 || rect.bottom > viewportHeight) {
557
+ element.scrollIntoView({
558
+ behavior: "smooth",
559
+ block: rect.top < 0 ? "start" : "nearest",
560
+ });
567
561
  }
562
+ });
563
+ }
568
564
 
569
- const newId = this.nextCellId++;
570
-
571
- const defaultText =
572
- type === "markdown"
573
- ? "### New Markdown Cell\nClick to edit..."
574
- : "";
575
-
576
- const source =
577
- initialCode !== null ? initialCode : defaultText;
578
-
579
- const pendingCode = isPromiseLike(source)
580
- ? Promise.resolve(source)
581
- : null;
582
-
583
- const newCell = {
584
- id: newId,
585
- type,
586
- code: pendingCode
587
- ? ""
588
- : typeof source === "string"
589
- ? source
590
- : "",
591
- _codePromise: pendingCode,
592
- _codeLoadError: null,
593
-
594
- readonly,
595
- isEditingMarkdown:
596
- !readonly &&
597
- type === "markdown" &&
598
- initialCode === null,
599
- runtime: null,
600
- execCount: null,
601
- outputNode: null,
602
- hasDomOutput: false,
603
-
604
- _copiedFlag: false,
605
- _dom: null,
606
- _editor: null,
607
- _actions: null,
608
- _inPrompt: null,
609
- _outPrompt: null,
610
- _bodySlot: null,
611
- _outputGrid: null,
612
- _outputRef: null,
613
- };
565
+
566
+
567
+ addCell(type = "code", initialCode = null, afterId = null, readonly = false) {
568
+ const cells = [...this.store.get("cells")];
614
569
 
615
- if (afterId === null) {
570
+ if (cells.length >= this.maxCells) {
571
+ console.warn(
572
+ `Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`
573
+ );
574
+ return null;
575
+ }
576
+
577
+ // Prefer the cell associated with the clicked "+" button.
578
+ // Fall back to the last focused cell, then the last cell.
579
+ const sourceCell =
580
+ (afterId !== null
581
+ ? cells.find((cell) => cell.id === afterId)
582
+ : null) ??
583
+ cells.find(
584
+ (cell) => cell.id === this.store.get("activeCellId")
585
+ ) ??
586
+ cells.at(-1);
587
+
588
+ const newId = this.nextCellId++;
589
+ const defaultText =
590
+ type === "markdown"
591
+ ? "### New Markdown Cell\nClick to edit..."
592
+ : "";
593
+
594
+ const newCell = {
595
+ id: newId,
596
+ type,
597
+ code: initialCode !== null ? initialCode : defaultText,
598
+ readonly,
599
+ isEditingMarkdown:
600
+ !readonly && type === "markdown" && initialCode === null,
601
+ runtime: sourceCell?.runtime ?? null,
602
+ execCount: null,
603
+ outputNode: null,
604
+ hasDomOutput: false,
605
+ _copiedFlag: false,
606
+ _dom: null,
607
+ _editor: null,
608
+ _actions: null,
609
+ _inPrompt: null,
610
+ _outPrompt: null,
611
+ _bodySlot: null,
612
+ _outputGrid: null,
613
+ _outputRef: null,
614
+ };
615
+
616
+ if (afterId === null) {
617
+ cells.push(newCell);
618
+ } else {
619
+ const index = cells.findIndex((cell) => cell.id === afterId);
620
+ if (index === -1) {
616
621
  cells.push(newCell);
617
622
  } else {
618
- const index = cells.findIndex(
619
- (cell) => cell.id === afterId
620
- );
621
-
622
623
  cells.splice(index + 1, 0, newCell);
623
624
  }
625
+ }
624
626
 
625
- this.store.set("cells", cells);
626
- this._renderAllCells();
627
- this.focusCell(newId);
628
-
629
- // Resolve newly added Promise-based code without blocking
630
- // the synchronous addCell() API.
631
- if (pendingCode) {
632
- this._resolveCellCode(newCell);
633
- }
627
+ this.store.set("cells", cells);
628
+ this._renderAllCells();
629
+ this.focusCell(newId);
634
630
 
635
- return newId;
636
- }
631
+ return newId;
632
+ }
637
633
 
638
634
  deleteCell(id) {
639
635
  const cells = this.store.get("cells");
@@ -656,19 +652,26 @@ export class UINotebook extends UIElement {
656
652
  this.focusCell(updatedCells[nextActiveIndex].id);
657
653
  }
658
654
 
659
- nextCell(currentId, defaultType) {
660
- const cells = this.store.get("cells");
661
- const index = cells.findIndex(
662
- (cell) => cell.id === currentId
663
- );
655
+
656
+ nextCell(currentId, defaultType) {
657
+ const cells = this.store.get("cells");
658
+ const index = cells.findIndex((cell) => cell.id === currentId);
659
+
660
+ if (index === -1) return;
664
661
 
665
- if (index === cells.length - 1) {
666
- this.addCell(defaultType, null, currentId);
667
- } else if (index >= 0) {
668
- this.focusCell(cells[index + 1].id);
662
+ if (index === cells.length - 1) {
663
+ const newId = this.addCell(defaultType, null, currentId);
664
+
665
+ if (newId !== null) {
666
+ this.focusCell(newId);
669
667
  }
668
+
669
+ return;
670
670
  }
671
671
 
672
+ this.focusCell(cells[index + 1].id);
673
+ }
674
+
672
675
  async runCell(id) {
673
676
  await this.ready;
674
677
 
@@ -761,31 +764,30 @@ export class UINotebook extends UIElement {
761
764
  return window.__notebook_scope_map.get(this.id);
762
765
  }
763
766
 
764
- getNotebookData({
765
- inputs = true,
766
- outputs = true,
767
- } = {}) {
768
- return this.store.get("cells").map((cell) => {
769
- const exportedCell = {
770
- id: cell.id,
771
- type: cell.type,
772
- readonly: cell.readonly,
773
- order: cell.execCount,
774
- };
767
+
768
+ getNotebookData({ inputs = true, outputs = true } = {}) {
769
+ return this.store.get("cells").map((cell) => {
770
+ const exportedCell = {
771
+ id: cell.id,
772
+ type: cell.type,
773
+ readonly: cell.readonly,
774
+ runtime: cell.runtime ?? null,
775
+ order: cell.execCount,
776
+ };
775
777
 
776
- if (inputs) {
777
- exportedCell.code = cell.code;
778
- }
778
+ if (inputs) {
779
+ exportedCell.code = cell._editor?.getValue() ?? cell.code;
780
+ }
779
781
 
780
- if (outputs) {
781
- exportedCell.outputHtml = cell.outputNode
782
- ? cell.outputNode.innerHTML
783
- : null;
784
- }
782
+ if (outputs) {
783
+ exportedCell.outputHtml = cell.outputNode
784
+ ? cell.outputNode.innerHTML
785
+ : cell._outputRef?.innerHTML ?? null;
786
+ }
785
787
 
786
- return exportedCell;
787
- });
788
- }
788
+ return exportedCell;
789
+ });
790
+ }
789
791
  }
790
792
 
791
793
  export const Notebook = call_with_optional_props(UINotebook);