X-speedsheet source code interpretation

The overall architecture

Apply colours to a drawing

The initial rendering

1 Call the SpeadSheet constructor

1.1 The main function is to create a DOM element and insert it into the id element according to the options passed in. It is similar to how react creates a virtual DOM element and inserts it into the ID element set on the page.

1.2 Initializing some data, such as this.data, which represents the entire sheet data, is similar to setting the entire sheet data proxy.

1.3 this. Sheet indicates the initialization of the entire sheet and event. At the same time, this. Data is also mounted to this. For example, here is the data structure of this. Sheet.

! [image-20210710114201688](/Users/zsj/Library/Application Support/typora-user-images/image-20210710114201688.png)

constructor(selectors, options = {}) {
    let targetEl = selectors;

    // By default, bottombar is displayed
    this.options = { showBottomBar: true. options };// sheetIndex is similar to the index of several tables in Excel
    this.sheetIndex = 1;
    // All data, i.e., each sheet, has one index in the array
    this.datas = [];
    if (typeof selectors === "string") {
      // Get the container
      targetEl = document.querySelector(selectors);
    }
    this.bottombar = this.options.showBottomBar
      ? new Bottombar(
          () = > {
            const d = this.addSheet();
            this.sheet.resetData(d);
          },
          (index) = > {
            const d = this.datas[index];
            this.sheet.resetData(d);
          },
          () = > {
            this.deleteSheet();
          },
          (index, value) = > {
            this.datas[index].name = value; }) :null;

    / / #
    // addSheet(name, active)
    // Function to add multiple tables
    // @param name String name
    // @param active Boolean Defaults to true
    // this.data represents all data agents for a table
    this.data = this.addSheet();
    // h is to create an object whose property EL represents the dom element to be created
    const rootEl = h("div".`${cssPrefix}`).on("contextmenu".(evt) = >
      evt.preventDefault()
    );
    // Insert our created element into the browser's DOM element
    targetEl.appendChild(rootEl.el);
    // Create a table with this.data representing DataProxy class
    this.sheet = new Sheet(rootEl, this.data);
    if (this.bottombar ! = =null) {
      rootEl.child(this.bottombar.el); }}Copy the code

Sheet 2 class

As you can see from the logic in the constructor, almost all of the logic for initializing the entire views layer and initializing events is in the Sheet class

In source this.sheet = new sheet (rootEl, this.data); RootEl is a div data structure. Think of it as a viralDOM representation similar to React, but much simpler than viralDOM. This. data is a proxy object for the data in the entire sheet created in the constructor.

The Sheet constructor does some initialization, such as creating the toolbar object, creating the print object, creating the div object for the select object, creating the div object for the Edit object, etc. After creating these objects, we call the event initialization function to initialize the event. Call the render function to render.

constructor(targetEl, data) {
    // Use the publish-subscribe pattern
    this.eventMap = createEventEmitter();
    const { view, showToolbar, showContextmenu } = data.settings;

    // El represents the real DOM element
    this.el = h("div".`${cssPrefix}-sheet`);
    / / create the toolbar
    this.toolbar = newToolbar(data, view.width, ! showToolbar);// Create print
    this.print = new Print(data);
    targetEl.children(this.toolbar.el, this.el, this.print.el);
    / / sheetDataProxy agent
    this.data = data;
    // Create table
    this.tableEl = h("canvas".`${cssPrefix}-table`);
    // Create scale
    this.rowResizer = new Resizer(false, data.rows.height);
    this.colResizer = new Resizer(true, data.cols.minWidth);
    / / create a scrollbar
    this.verticalScrollbar = new Scrollbar(true);
    this.horizontalScrollbar = new Scrollbar(false);
    // Create an editor div object
    this.editor = new Editor(
      formulas,
      () = > this.getTableOffset(),
      data.rows.height
    );
    // Create a data test
    this.modalValidation = new ModalValidation();
    // Create a right-click menu bar
    this.contextMenu = new ContextMenu(() = > this.getRect(), ! showContextmenu);// Create selected, which is the object after clicking in the table
    this.selector = new Selector(data);
    // Create a mask layer from which to get the event
    this.overlayerCEl = h("div".`${cssPrefix}-overlayer-content`).children(
      this.editor.el,
      this.selector.el
    );
    this.overlayerEl = h("div".`${cssPrefix}-overlayer`).child(
      this.overlayerCEl
    );
    // Create sort
    this.sortFilter = new SortFilter();
    // Insert the created DOM elements into this.el
    this.el.children(
      this.tableEl,
      this.overlayerEl.el,
      this.rowResizer.el,
      this.colResizer.el,
      this.verticalScrollbar.el,
      this.horizontalScrollbar.el,
      this.contextMenu.el,
      this.modalValidation.el,
      this.sortFilter.el
    );
    // Create a table object
    this.table = new Table(this.tableEl.el, data);
    // Initialize the event
    sheetInitEvents.call(this);
    // Call the table render to draw the table
    sheetReset.call(this);
    // set the cell coordinates of the selected table to 0,0
    selectorSet.call(this.false.0.0);
  }
Copy the code

3 Table

The Table constructor doesn’t do much. It simply initializes some variables. This.draw is a drawing function, and a this.data is the sheetDataProxy.

The main logic here is in the render function of the Table

The render function is some simple use of canvas rendering logic, the basic relatively simple.

render() {
    // resize canvas
    const { data } = this;
    const { rows, cols } = data;
    // Fixed width of header
    const fixedWidth = cols.indexWidth;
    // Fixed height of header
    const fiexedHeight = rows.height;

    this.draw.resize(data.viewWidth(), data.viewHeight());
    this.clear();

    const viewRange = data.viewRange();
    // renderAll.call(this, viewRange, data.scroll);
    const tx = data.freezeTotalWidth();
    const ty = data.freezeTotalHeight();
    const { x, y } = data.scroll;
    / / 1
    renderContentGrid.call(this, viewRange, fixedWidth, fiexedHeight, tx, ty);
    renderContent.call(this, viewRange, fixedWidth, fiexedHeight, -x, -y);
    renderFixedHeaders.call(
      this."all",
      viewRange,
      fixedWidth,
      fiexedHeight,
      tx,
      ty
    );
    // Render the left column
    renderFixedLeftTopCell.call(this, fixedWidth, fiexedHeight);
    const [fri, fci] = data.freeze;
    if (fri > 0 || fci > 0) {
      / / 2
      if (fri > 0) {
        const vr = viewRange.clone();
        vr.sri = 0;
        vr.eri = fri - 1;
        vr.h = ty;
        renderContentGrid.call(this, vr, fixedWidth, fiexedHeight, tx, 0);
        renderContent.call(this, vr, fixedWidth, fiexedHeight, -x, 0);
        renderFixedHeaders.call(
          this."top",
          vr,
          fixedWidth,
          fiexedHeight,
          tx,
          0
        );
      }
      / / 3
      if (fci > 0) {
        const vr = viewRange.clone();
        vr.sci = 0;
        vr.eci = fci - 1;
        vr.w = tx;
        renderContentGrid.call(this, vr, fixedWidth, fiexedHeight, 0, ty);
        renderFixedHeaders.call(
          this."left",
          vr,
          fixedWidth,
          fiexedHeight,
          0,
          ty
        );
        renderContent.call(this, vr, fixedWidth, fiexedHeight, 0, -y);
      }
      / / 4
      const freezeViewRange = data.freezeViewRange();
      renderContentGrid.call(
        this,
        freezeViewRange,
        fixedWidth,
        fiexedHeight,
        0.0
      );
      renderFixedHeaders.call(
        this."all",
        freezeViewRange,
        fixedWidth,
        fiexedHeight,
        0.0
      );
      renderContent.call(this, freezeViewRange, fixedWidth, fiexedHeight, 0.0);
      / / 5
      renderFreezeHighlightLine.call(this, fixedWidth, fiexedHeight, tx, ty); }}Copy the code

This is basically the end of the process of using x_Spreadsheet (‘# x-graph-demo ‘,options).

At this time, the UI diagram is as follows:

! [image-20210710154508068](/Users/zsj/Library/Application Support/typora-user-images/image-20210710154508068.png)

loadData

  1. After the initial rendering is complete, we can then use loadData to load the data to complete the process of loading the data in the table.

    You can start by looking at the effect of calling loadData with the following data.

     const rows = {
        len: 80.1: {
          cells: {
            0: { text: 'testingtesttestetst' },
            2: { text: 'testing'}},},2: {
          cells: {
            // Style: 0 is the first element of styles in data
            0: { text: 'render'.style: 0 },
            1: { text: 'Hello' },
            2: { text: 'haha'.merge: [1.1]}}},8: {
          cells: {
            // Style: 0 is the first element of styles in data
            8: { text: 'border test'.style: 0}}}};var xs = x_spreadsheet('#x-spreadsheet-demo', {
        showToolbar: true.showGrid: true.showBottomBar: true.col: {
          len: 50.width: 100.// indexWidth: 60,
          minWidth: 20,},row: {
          len: 20,},extendToolbar: {
          left: [{tip: 'Save'.icon: saveIcon,
              onClick: (data, sheet) = > {
                console.log('click save button:', data, sheet)
              }
            }
          ],
          right: [{tip: 'Preview'.el: previewEl,
              onClick: (data, sheet) = > {
                console.log('click preview button:', data)
              }
            }
          ],
        }
      })
        .loadData([{
          freeze: 'B3'.styles: [{bgcolor: '#f4f5f8'.textwrap: true.color: '#900b09'.border: {
                top: ['thin'.'#0366d6'].bottom: ['thin'.'#0366d6'].right: ['thin'.'#0366d6'].left: ['thin'.'#0366d6']],}},merges: [
            'C3:D4',].cols: {
            len: 10.2: { width: 200 },
          },
          rows,
        })
Copy the code

The renderings are shown below

! [image-20210710155216048](/Users/zsj/Library/Application Support/typora-user-images/image-20210710155216048.png)

  1. The key here is to re-render by calling dataSheetProxy’s resetData, which then calls the table’s resetData, which then calls the table’s render. At this point the basic rendering is almost complete.

The event system

Table click events

  1. Register events with createEventEmitter, the file directory (SRC/Component /event.js). Registering events here is simple, just the normal publish/subscribe pattern

  2. The outermost mask layer listens for event functions, such as click, such as double click, and when double click, the edit box is displayed.