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
-
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)
-
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
-
Register events with createEventEmitter, the file directory (SRC/Component /event.js). Registering events here is simple, just the normal publish/subscribe pattern
-
The outermost mask layer listens for event functions, such as click, such as double click, and when double click, the edit box is displayed.