Browse Source

PR: TOC current position (#10099)

* adds current location marker in the ToC whenever a cell is selected

* adds documentation

* add default value to index and deprecation warning

* avoid type coersion and backward-invompatible signature changes
Andrew Fulton 4 years ago
parent
commit
bce3c51c1a

+ 10 - 2
packages/toc/src/generators/notebook/get_code_cell_heading.ts

@@ -22,6 +22,7 @@ type onClickFactory = (line: number) => () => void;
  * @param executionCount - execution count
  * @param lastLevel - last heading level
  * @param cellRef - cell reference
+ * @param index - index of referenced cell relative to other cells in the notebook
  * @returns notebook heading
  */
 function getCodeCellHeading(
@@ -29,9 +30,15 @@ function getCodeCellHeading(
   onClick: onClickFactory,
   executionCount: string,
   lastLevel: number,
-  cellRef: Cell
+  cellRef: Cell,
+  index: number = -1
 ): INotebookHeading {
   let headings: INotebookHeading[] = [];
+  if (index === -1) {
+    console.warn(
+      'Deprecation warning! index argument will become mandatory in the next version'
+    );
+  }
   if (text) {
     const lines = text.split('\n');
     const len = Math.min(lines.length, 3);
@@ -48,7 +55,8 @@ function getCodeCellHeading(
       type: 'code',
       prompt: executionCount,
       cellRef: cellRef,
-      hasChild: false
+      hasChild: false,
+      index: index
     });
   }
   return headings[0];

+ 12 - 3
packages/toc/src/generators/notebook/get_markdown_heading.ts

@@ -24,6 +24,7 @@ type onClickFactory = (line: number) => () => void;
  * @param dict - numbering dictionary
  * @param lastLevel - last level
  * @param cellRef - cell reference
+ * @param index - index of referenced cell relative to other cells in the notebook
  * @returns notebook heading
  */
 function getMarkdownHeadings(
@@ -31,10 +32,16 @@ function getMarkdownHeadings(
   onClick: onClickFactory,
   dict: any,
   lastLevel: number,
-  cellRef: Cell
+  cellRef: Cell,
+  index: number = -1
 ): INotebookHeading[] {
   const clbk = onClick(0);
   let headings: INotebookHeading[] = [];
+  if (index === -1) {
+    console.warn(
+      'Deprecation warning! index argument will become mandatory in the next version'
+    );
+  }
   for (const line of text.split('\n')) {
     const heading = parseHeading(line);
     if (heading) {
@@ -45,7 +52,8 @@ function getMarkdownHeadings(
         onClick: clbk,
         type: 'header',
         cellRef: cellRef,
-        hasChild: false
+        hasChild: false,
+        index
       });
     } else {
       headings.push({
@@ -54,7 +62,8 @@ function getMarkdownHeadings(
         onClick: clbk,
         type: 'markdown',
         cellRef: cellRef,
-        hasChild: false
+        hasChild: false,
+        index
       });
     }
   }

+ 12 - 3
packages/toc/src/generators/notebook/get_rendered_html_heading.ts

@@ -27,6 +27,7 @@ type onClickFactory = (el: Element) => () => void;
  * @param lastLevel - last level
  * @param numbering - boolean indicating whether to enable numbering
  * @param cellRef - cell reference
+ * @param index - index of referenced cell relative to other cells in the notebook
  * @returns notebook heading
  */
 function getRenderedHTMLHeadings(
@@ -36,10 +37,16 @@ function getRenderedHTMLHeadings(
   dict: INumberingDictionary,
   lastLevel: number,
   numbering = false,
-  cellRef: Cell
+  cellRef: Cell,
+  index: number = -1
 ): INotebookHeading[] {
   let nodes = node.querySelectorAll('h1, h2, h3, h4, h5, h6, p');
 
+  if (index === -1) {
+    console.warn(
+      'Deprecation warning! index argument will become mandatory in the next version'
+    );
+  }
   let headings: INotebookHeading[] = [];
   for (const el of nodes) {
     if (el.nodeName.toLowerCase() === 'p') {
@@ -52,7 +59,8 @@ function getRenderedHTMLHeadings(
           onClick: onClick(el),
           type: 'markdown',
           cellRef: cellRef,
-          hasChild: false
+          hasChild: false,
+          index: index
         });
       }
       continue;
@@ -79,7 +87,8 @@ function getRenderedHTMLHeadings(
       onClick: onClick(el),
       type: 'header',
       cellRef: cellRef,
-      hasChild: false
+      hasChild: false,
+      index: index
     });
   }
   return headings;

+ 11 - 7
packages/toc/src/generators/notebook/index.ts

@@ -78,10 +78,11 @@ function createNotebookGenerator(
    *
    * @private
    * @param item - heading to render
+   * @param toc - list of all headers to render
    * @returns rendered item
    */
-  function renderItem(item: INotebookHeading) {
-    return render(options, tracker, item);
+  function renderItem(item: INotebookHeading, toc: INotebookHeading[] = []) {
+    return render(options, tracker, item, toc);
   }
 
   /**
@@ -103,7 +104,6 @@ function createNotebookGenerator(
     for (let i = 0; i < panel.content.widgets.length; i++) {
       let cell: Cell = panel.content.widgets[i];
       let model = cell.model;
-
       let collapsed = model.metadata.get('toc-hr-collapsed') as boolean;
       collapsed = collapsed || false;
 
@@ -122,7 +122,8 @@ function createNotebookGenerator(
             onClick,
             executionCount,
             getLastHeadingLevel(headings),
-            cell
+            cell,
+            i
           );
           [headings, prev] = appendHeading(
             headings,
@@ -155,7 +156,8 @@ function createNotebookGenerator(
             dict,
             getLastHeadingLevel(headings),
             options.numbering,
-            cell
+            cell,
+            i
           );
           for (const heading of htmlHeadings) {
             [headings, prev, collapseLevel] = appendMarkdownHeading(
@@ -197,7 +199,8 @@ function createNotebookGenerator(
             dict,
             lastLevel,
             options.numbering,
-            cell
+            cell,
+            i
           );
           for (heading of htmlHeadings) {
             [headings, prev, collapseLevel] = appendMarkdownHeading(
@@ -223,7 +226,8 @@ function createNotebookGenerator(
             onClick,
             dict,
             lastLevel,
-            cell
+            cell,
+            i
           );
           for (heading of markdownHeadings) {
             [headings, prev, collapseLevel] = appendMarkdownHeading(

+ 47 - 3
packages/toc/src/generators/notebook/render.tsx

@@ -16,12 +16,14 @@ import { OptionsManager } from './options_manager';
  * @param options - generator options
  * @param tracker - notebook tracker
  * @param item - notebook heading
+ * @param toc - current list of notebook headings
  * @returns rendered item
  */
 function render(
   options: OptionsManager,
   tracker: INotebookTracker,
-  item: INotebookHeading
+  item: INotebookHeading,
+  toc: INotebookHeading[] = []
 ) {
   let jsx;
   if (item.type === 'markdown' || item.type === 'header') {
@@ -81,7 +83,11 @@ function render(
             className={
               'toc-entry-holder ' +
               fontSizeClass +
-              (tracker.activeCell === item.cellRef ? ' toc-active-cell' : '')
+              (tracker.activeCell === item.cellRef
+                ? ' toc-active-cell'
+                : previousHeader(tracker, item, toc)
+                ? ' toc-active-cell'
+                : '')
             }
           >
             {button}
@@ -135,7 +141,11 @@ function render(
             className={
               'toc-entry-holder ' +
               fontSizeClass +
-              (tracker.activeCell === item.cellRef ? ' toc-active-cell' : '')
+              (tracker.activeCell === item.cellRef
+                ? ' toc-active-cell'
+                : previousHeader(tracker, item, toc)
+                ? ' toc-active-cell'
+                : '')
             }
           >
             {button}
@@ -191,6 +201,40 @@ function render(
   }
 }
 
+/**
+ * Used to find the nearest above heading to an active notebook cell
+ *
+ * @private
+ * @param tracker - notebook tracker
+ * @param item - notebook heading
+ * @param toc - current list of notebook headings
+ * @returns true if heading is nearest above a selected cell, otherwise false
+ */
+function previousHeader(
+  tracker: INotebookTracker,
+  item: INotebookHeading,
+  toc: INotebookHeading[]
+) {
+  if (item.index > -1 || toc?.length) {
+    let activeCellIndex = tracker.currentWidget!.content.activeCellIndex;
+    let headerIndex = item.index;
+    // header index has to be less than the active cell index
+    if (headerIndex < activeCellIndex) {
+      let tocIndexOfNextHeader = toc.indexOf(item) + 1;
+      // return true if header is the last header
+      if (tocIndexOfNextHeader >= toc.length) {
+        return true;
+      }
+      // return true if the next header cells index is greater than the active cells index
+      let nextHeaderIndex = toc?.[tocIndexOfNextHeader].index;
+      if (nextHeaderIndex > activeCellIndex) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
 /**
  * Exports.
  */

+ 6 - 2
packages/toc/src/registry.ts

@@ -5,7 +5,7 @@ import { IWidgetTracker } from '@jupyterlab/apputils';
 import { Token } from '@lumino/coreutils';
 import { Signal, ISignal } from '@lumino/signaling';
 import { Widget } from '@lumino/widgets';
-import { IHeading } from './utils/headings';
+import { IHeading, INotebookHeading } from './utils/headings';
 
 /**
  * Interface describing the table of contents registry.
@@ -162,9 +162,13 @@ export namespace TableOfContentsRegistry {
      * -   If not present, a default renderer will be used.
      *
      * @param item - heading
+     * @param toc - list of headings
      * @returns JSX element
      */
-    itemRenderer?: (item: IHeading) => JSX.Element | null;
+    itemRenderer?: (
+      item: IHeading,
+      toc: INotebookHeading[]
+    ) => JSX.Element | null;
 
     /**
      * Returns a toolbar component.

+ 4 - 3
packages/toc/src/toc.tsx

@@ -118,9 +118,10 @@ export class TableOfContents extends Widget {
         title = PathExt.basename(context.localPath);
       }
     }
-    let itemRenderer: (item: IHeading) => JSX.Element | null = (
-      item: IHeading
-    ) => {
+    let itemRenderer: (
+      item: IHeading,
+      toc: IHeading[]
+    ) => JSX.Element | null = (item: IHeading) => {
       return <span>{item.text}</span>;
     };
     if (this._current && this._current.generator.itemRenderer) {

+ 8 - 3
packages/toc/src/toc_item.tsx

@@ -14,14 +14,19 @@ interface IProperties {
    * Heading to render.
    */
   heading: IHeading;
+  /**
+   * List of headings to use for rendering current position in toc
+   */
+  toc: IHeading[];
 
   /**
    * Renders a heading.
    *
    * @param item - heading
+   * @param toc - list of headings
    * @returns rendered heading
    */
-  itemRenderer: (item: IHeading) => JSX.Element | null;
+  itemRenderer: (item: IHeading, toc: IHeading[]) => JSX.Element | null;
 }
 
 /**
@@ -43,7 +48,7 @@ class TOCItem extends React.Component<IProperties, IState> {
    * @returns rendered entry
    */
   render() {
-    const { heading } = this.props;
+    const { heading, toc } = this.props;
 
     // Create an onClick handler for the TOC item
     // that scrolls the anchor into view.
@@ -53,7 +58,7 @@ class TOCItem extends React.Component<IProperties, IState> {
       heading.onClick();
     };
 
-    let content = this.props.itemRenderer(heading);
+    let content = this.props.itemRenderer(heading, toc);
     return content && <li onClick={onClick}>{content}</li>;
   }
 }

+ 3 - 1
packages/toc/src/toc_tree.tsx

@@ -37,9 +37,10 @@ interface IProperties extends React.Props<TOCTree> {
    * Renders a heading item.
    *
    * @param item - heading
+   * @param toc - list of headings in toc to use for rendering current position
    * @returns rendered heading
    */
-  itemRenderer: (item: IHeading) => JSX.Element | null;
+  itemRenderer: (item: IHeading, toc: IHeading[]) => JSX.Element | null;
 }
 
 /**
@@ -67,6 +68,7 @@ class TOCTree extends React.Component<IProperties, IState> {
       return (
         <TOCItem
           heading={el}
+          toc={this.props.toc}
           itemRenderer={this.props.itemRenderer}
           key={`${el.text}-${el.level}-${i++}`}
         />

+ 5 - 0
packages/toc/src/utils/headings.ts

@@ -76,6 +76,11 @@ interface INotebookHeading extends INumberedHeading {
    * Boolean indicating whether a heading has a child node.
    */
   hasChild?: boolean;
+
+  /**
+   * index of reference cell in the notebook
+   */
+  index: number;
 }
 
 /**