@ebl-vue/editor-full 1.0.8
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/.postcssrc.yml +33 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +2565 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/postcss.config.js +15 -0
- package/src/components/Editor/Editor.vue +209 -0
- package/src/components/index.ts +27 -0
- package/src/constants/index.ts +1 -0
- package/src/i18n/zh-cn.ts +151 -0
- package/src/icons/index.ts +78 -0
- package/src/index.ts +11 -0
- package/src/installer.ts +22 -0
- package/src/plugins/alert/index.css +150 -0
- package/src/plugins/alert/index.ts +463 -0
- package/src/plugins/block-alignment/index.css +9 -0
- package/src/plugins/block-alignment/index.ts +116 -0
- package/src/plugins/block-alignment/readme.md +1 -0
- package/src/plugins/code/LICENSE +21 -0
- package/src/plugins/code/index.css +120 -0
- package/src/plugins/code/index.ts +530 -0
- package/src/plugins/code/utils/string.ts +34 -0
- package/src/plugins/color-picker/index.ts +138 -0
- package/src/plugins/color-picker/styles.css +27 -0
- package/src/plugins/delimiter/index.css +14 -0
- package/src/plugins/delimiter/index.ts +122 -0
- package/src/plugins/drag-drop/index.css +19 -0
- package/src/plugins/drag-drop/index.ts +151 -0
- package/src/plugins/drag-drop/readme.md +1 -0
- package/src/plugins/header/H1.ts +405 -0
- package/src/plugins/header/H2.ts +403 -0
- package/src/plugins/header/H3.ts +404 -0
- package/src/plugins/header/H4.ts +405 -0
- package/src/plugins/header/H5.ts +405 -0
- package/src/plugins/header/H6.ts +406 -0
- package/src/plugins/header/index.css +20 -0
- package/src/plugins/header/index.ts +15 -0
- package/src/plugins/header/types.d.ts +46 -0
- package/src/plugins/indent/index.css +86 -0
- package/src/plugins/indent/index.ts +697 -0
- package/src/plugins/inline-code/index.css +11 -0
- package/src/plugins/inline-code/index.ts +205 -0
- package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +211 -0
- package/src/plugins/list/ListRenderer/ListRenderer.ts +73 -0
- package/src/plugins/list/ListRenderer/OrderedListRenderer.ts +123 -0
- package/src/plugins/list/ListRenderer/UnorderedListRenderer.ts +123 -0
- package/src/plugins/list/ListRenderer/index.ts +6 -0
- package/src/plugins/list/ListTabulator/index.ts +1179 -0
- package/src/plugins/list/index.ts +502 -0
- package/src/plugins/list/styles/CssPrefix.ts +4 -0
- package/src/plugins/list/styles/icons/index.ts +10 -0
- package/src/plugins/list/styles/input.css +36 -0
- package/src/plugins/list/styles/list.css +165 -0
- package/src/plugins/list/types/Elements.ts +14 -0
- package/src/plugins/list/types/ItemMeta.ts +40 -0
- package/src/plugins/list/types/ListParams.ts +102 -0
- package/src/plugins/list/types/ListRenderer.ts +6 -0
- package/src/plugins/list/types/OlCounterType.ts +63 -0
- package/src/plugins/list/types/index.ts +14 -0
- package/src/plugins/list/utils/focusItem.ts +18 -0
- package/src/plugins/list/utils/getChildItems.ts +40 -0
- package/src/plugins/list/utils/getItemChildWrapper.ts +10 -0
- package/src/plugins/list/utils/getItemContentElement.ts +10 -0
- package/src/plugins/list/utils/getSiblings.ts +52 -0
- package/src/plugins/list/utils/isLastItem.ts +9 -0
- package/src/plugins/list/utils/itemHasSublist.ts +10 -0
- package/src/plugins/list/utils/normalizeData.ts +84 -0
- package/src/plugins/list/utils/removeChildWrapperIfEmpty.ts +31 -0
- package/src/plugins/list/utils/renderToolboxInput.ts +105 -0
- package/src/plugins/list/utils/stripNumbers.ts +7 -0
- package/src/plugins/list/utils/type-guards.ts +8 -0
- package/src/plugins/list.md +15 -0
- package/src/plugins/marker/index.css +4 -0
- package/src/plugins/marker/index.ts +187 -0
- package/src/plugins/paragraph/index.css +23 -0
- package/src/plugins/paragraph/index.ts +380 -0
- package/src/plugins/paragraph/types/icons.d.ts +4 -0
- package/src/plugins/paragraph/utils/makeFragment.ts +17 -0
- package/src/plugins/quote/index.css +26 -0
- package/src/plugins/quote/index.ts +206 -0
- package/src/plugins/table/index.ts +4 -0
- package/src/plugins/table/plugin.ts +254 -0
- package/src/plugins/table/style.css +388 -0
- package/src/plugins/table/table.ts +1192 -0
- package/src/plugins/table/toolbox.ts +165 -0
- package/src/plugins/table/utils/dom.ts +128 -0
- package/src/plugins/table/utils/popover.ts +172 -0
- package/src/plugins/table/utils/throttled.ts +22 -0
- package/src/plugins/underline/index.css +3 -0
- package/src/plugins/underline/index.ts +216 -0
- package/src/plugins/undo/index.ts +509 -0
- package/src/plugins/undo/observer.ts +101 -0
- package/src/style.css +89 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/install.ts +19 -0
- package/tsconfig.json +37 -0
- package/types/index.d.ts +13 -0
- package/types/plugins/index.d.ts +0 -0
- package/vite.config.ts +79 -0
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
import Toolbox from './toolbox';
|
|
2
|
+
import * as $ from './utils/dom';
|
|
3
|
+
import throttled from './utils/throttled';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
IconDirectionLeftDown,
|
|
7
|
+
IconDirectionRightDown,
|
|
8
|
+
IconDirectionUpRight,
|
|
9
|
+
IconDirectionDownRight,
|
|
10
|
+
IconCross,
|
|
11
|
+
IconPlus
|
|
12
|
+
} from '../../icons';
|
|
13
|
+
type TableData = {
|
|
14
|
+
/**
|
|
15
|
+
* - number of rows in the table
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* - number of rows in the table
|
|
19
|
+
*/
|
|
20
|
+
rows: number;
|
|
21
|
+
/**
|
|
22
|
+
* - number of columns in the table
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* - number of columns in the table
|
|
26
|
+
*/
|
|
27
|
+
cols: number;
|
|
28
|
+
colWidth: string[];
|
|
29
|
+
}
|
|
30
|
+
const CSS = {
|
|
31
|
+
wrapper: 'tc-wrap',
|
|
32
|
+
wrapperReadOnly: 'tc-wrap--readonly',
|
|
33
|
+
table: 'tc-table',
|
|
34
|
+
row: 'tc-row',
|
|
35
|
+
withHeadings: 'tc-table--heading',
|
|
36
|
+
rowSelected: 'tc-row--selected',
|
|
37
|
+
cell: 'tc-cell',
|
|
38
|
+
cellSelected: 'tc-cell--selected',
|
|
39
|
+
addRow: 'tc-add-row',
|
|
40
|
+
addRowDisabled: 'tc-add-row--disabled',
|
|
41
|
+
addColumn: 'tc-add-column',
|
|
42
|
+
addColumnDisabled: 'tc-add-column--disabled',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} TableConfig
|
|
47
|
+
* @description Tool's config from Editor
|
|
48
|
+
* @property {boolean} withHeadings — Uses the first line as headings
|
|
49
|
+
* @property {string[][]} withHeadings — two-dimensional array with table contents
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} TableData - object with the data transferred to form a table
|
|
54
|
+
* @property {number} rows - number of rows in the table
|
|
55
|
+
* @property {number} cols - number of columns in the table
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generates and manages table contents.
|
|
61
|
+
*/
|
|
62
|
+
export default class Table {
|
|
63
|
+
|
|
64
|
+
private readOnly: boolean;
|
|
65
|
+
private api: any;
|
|
66
|
+
private data: any;
|
|
67
|
+
private config: any;
|
|
68
|
+
private wrapper: any;
|
|
69
|
+
private table: any;
|
|
70
|
+
private toolboxColumn: any;
|
|
71
|
+
private toolboxRow: any;
|
|
72
|
+
private documentClicked: any;
|
|
73
|
+
private hoveredRow: any;
|
|
74
|
+
private hoveredColumn: any;
|
|
75
|
+
private selectedRow: any;
|
|
76
|
+
private selectedColumn: any;
|
|
77
|
+
private tunes: any;
|
|
78
|
+
private focusedCell: any;
|
|
79
|
+
private numberOfColumns: number;
|
|
80
|
+
private hoveredCell: any;
|
|
81
|
+
private isDragging: boolean;
|
|
82
|
+
private draggingRow: number;
|
|
83
|
+
private draggingColumn: number;
|
|
84
|
+
private draggingColArr: any[];
|
|
85
|
+
private startWidth: number;
|
|
86
|
+
private mouseStartX: number;
|
|
87
|
+
private colWidthArr:any[];
|
|
88
|
+
private minCellWidhth: number=50;
|
|
89
|
+
private defaultCellWidth = 100;
|
|
90
|
+
/**
|
|
91
|
+
* Creates
|
|
92
|
+
*
|
|
93
|
+
* @constructor
|
|
94
|
+
* @param {boolean} readOnly - read-only mode flag
|
|
95
|
+
* @param {object} api - Editor.js API
|
|
96
|
+
* @param {TableData} data - Editor.js API
|
|
97
|
+
* @param {TableConfig} config - Editor.js API
|
|
98
|
+
*/
|
|
99
|
+
constructor(readOnly: boolean, api: any, data: TableData, config: any) {
|
|
100
|
+
this.readOnly = readOnly;
|
|
101
|
+
this.api = api;
|
|
102
|
+
this.data = data;
|
|
103
|
+
this.config = config;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* DOM nodes
|
|
107
|
+
*/
|
|
108
|
+
this.wrapper = null;
|
|
109
|
+
this.table = null;
|
|
110
|
+
this.hoveredCell = null;
|
|
111
|
+
|
|
112
|
+
this.isDragging = false;
|
|
113
|
+
this.draggingColArr = [];
|
|
114
|
+
this.draggingRow = 0;
|
|
115
|
+
this.draggingColumn = 0;
|
|
116
|
+
this.startWidth = 0;
|
|
117
|
+
this.mouseStartX = 0;
|
|
118
|
+
this.colWidthArr = [];
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// 鼠标焦点数据
|
|
122
|
+
this.focusedCell = {
|
|
123
|
+
row: 0,
|
|
124
|
+
col: 0,
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Toolbox for managing of columns
|
|
128
|
+
*/
|
|
129
|
+
this.toolboxColumn = this.createColumnToolbox();
|
|
130
|
+
this.toolboxRow = this.createRowToolbox();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create table and wrapper elements
|
|
134
|
+
*/
|
|
135
|
+
this.createTableWrapper();
|
|
136
|
+
|
|
137
|
+
// Current hovered row index
|
|
138
|
+
this.hoveredRow = 0;
|
|
139
|
+
|
|
140
|
+
// Current hovered column index
|
|
141
|
+
this.hoveredColumn = 0;
|
|
142
|
+
|
|
143
|
+
// Index of last selected row via toolbox
|
|
144
|
+
this.selectedRow = 0;
|
|
145
|
+
|
|
146
|
+
// Index of last selected column via toolbox
|
|
147
|
+
this.selectedColumn = 0;
|
|
148
|
+
|
|
149
|
+
// Additional settings for the table
|
|
150
|
+
this.tunes = {
|
|
151
|
+
withHeadings: false
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resize table to match config/data size
|
|
156
|
+
*/
|
|
157
|
+
this.resize();
|
|
158
|
+
if(this.data.colWidth.length > 0) {
|
|
159
|
+
this.colWidthArr = this.data.colWidth;
|
|
160
|
+
}
|
|
161
|
+
this.updateColWidth();
|
|
162
|
+
/**
|
|
163
|
+
* Fill the table with data
|
|
164
|
+
*/
|
|
165
|
+
this.fill();
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The cell in which the focus is currently located, if 0 and 0 then there is no focus
|
|
169
|
+
* Uses to switch between cells with buttons
|
|
170
|
+
*/
|
|
171
|
+
this.focusedCell = {
|
|
172
|
+
row: 0,
|
|
173
|
+
column: 0
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Global click listener allows to delegate clicks on some elements
|
|
178
|
+
*/
|
|
179
|
+
this.documentClicked = (event:MouseEvent) => {
|
|
180
|
+
const clickedInsideTable = event.target.closest(`.${CSS.table}`) !== null;
|
|
181
|
+
const outsideTableClicked = event.target.closest(`.${CSS.wrapper}`) === null;
|
|
182
|
+
const clickedOutsideToolboxes = clickedInsideTable || outsideTableClicked;
|
|
183
|
+
|
|
184
|
+
if (clickedOutsideToolboxes) {
|
|
185
|
+
this.hideToolboxes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const clickedOnAddRowButton = event.target.closest(`.${CSS.addRow}`);
|
|
189
|
+
const clickedOnAddColumnButton = event.target.closest(`.${CSS.addColumn}`);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Also, check if clicked in current table, not other (because documentClicked bound to the whole document)
|
|
193
|
+
*/
|
|
194
|
+
if (clickedOnAddRowButton && clickedOnAddRowButton.parentNode === this.wrapper) {
|
|
195
|
+
this.addRow(undefined, true);
|
|
196
|
+
this.hideToolboxes();
|
|
197
|
+
} else if (clickedOnAddColumnButton && clickedOnAddColumnButton.parentNode === this.wrapper) {
|
|
198
|
+
this.addColumn(undefined, true);
|
|
199
|
+
this.hideToolboxes();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (!this.readOnly) {
|
|
204
|
+
this.bindEvents();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Returns the rendered table wrapper
|
|
210
|
+
*
|
|
211
|
+
* @returns {Element}
|
|
212
|
+
*/
|
|
213
|
+
getWrapper() {
|
|
214
|
+
return this.wrapper;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Hangs the necessary handlers to events
|
|
219
|
+
*/
|
|
220
|
+
bindEvents() {
|
|
221
|
+
// set the listener to close toolboxes when click outside
|
|
222
|
+
document.addEventListener('click', this.documentClicked);
|
|
223
|
+
// 鼠标点击事件:判定是否在表格内部
|
|
224
|
+
document.addEventListener("mousedown", (e) =>
|
|
225
|
+
this.handleDocumentMousedown(e)
|
|
226
|
+
);
|
|
227
|
+
// Update toolboxes position depending on the mouse movements
|
|
228
|
+
this.table.addEventListener('mousemove', throttled(150, (event:MouseEvent) => this.onMouseMoveInTable(event)), { passive: true });
|
|
229
|
+
|
|
230
|
+
// Controls some of the keyboard buttons inside the table
|
|
231
|
+
this.table.onkeypress = (event:KeyboardEvent) => this.onKeyPressListener(event);
|
|
232
|
+
|
|
233
|
+
// Tab is executed by default before keypress, so it must be intercepted on keydown
|
|
234
|
+
this.table.addEventListener('keydown', (event:KeyboardEvent) => this.onKeyDownListener(event));
|
|
235
|
+
|
|
236
|
+
// Determine the position of the cell in focus
|
|
237
|
+
this.table.addEventListener('focusin', (event:FocusEvent) => this.focusInTableListener(event));
|
|
238
|
+
}
|
|
239
|
+
handleDocumentMousedown = (e:MouseEvent) => {
|
|
240
|
+
// 在表格内部按下鼠标并且处于拖拽位置
|
|
241
|
+
if (this.wrapper.contains(e.target)) {
|
|
242
|
+
const target = e.target as HTMLElement;
|
|
243
|
+
if (target.classList.contains("cell-resizable")) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
//this.hideToolbox(); // 拖拽时隐藏工具栏
|
|
246
|
+
this.isDragging = true;
|
|
247
|
+
this.table.classList.add("table-resizing"); //为整个表格添加光标样式
|
|
248
|
+
document.addEventListener("mousemove", this.handleDocumentMousemove);
|
|
249
|
+
document.addEventListener("mouseup", this.handleDocumentMouseup);
|
|
250
|
+
this.draggingRow = this.hoveredRow;
|
|
251
|
+
this.draggingColumn = this.hoveredColumn;
|
|
252
|
+
this.draggingColArr = this.getCellInCol(this.draggingColumn);
|
|
253
|
+
this.startWidth = this.colWidthArr[this.draggingColumn - 1]; //获取初始宽度
|
|
254
|
+
this.mouseStartX = e.clientX;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
//this.hideToolbox();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
// 获取同一列的所有单元格
|
|
261
|
+
getCellInCol(col:number) {
|
|
262
|
+
const cells:any[] = [];
|
|
263
|
+
const rows = this.table.querySelectorAll(`.${CSS.row}`);
|
|
264
|
+
rows.forEach((row:Element) => {
|
|
265
|
+
const cell = row.querySelector(`.${CSS.cell}:nth-child(${col})`);
|
|
266
|
+
if (cell) cells.push(cell);
|
|
267
|
+
});
|
|
268
|
+
return cells;
|
|
269
|
+
}
|
|
270
|
+
// mousemove事件
|
|
271
|
+
handleDocumentMousemove = (e) => {
|
|
272
|
+
if (!this.isDragging) return;
|
|
273
|
+
const currentMouseX = e.clientX;
|
|
274
|
+
const deltaX = currentMouseX - this.mouseStartX;
|
|
275
|
+
const newWidth = Math.max(this.startWidth + deltaX, this.minCellWidhth);
|
|
276
|
+
|
|
277
|
+
this.colWidthArr[this.draggingColumn - 1] = newWidth;
|
|
278
|
+
this.updateColWidth();
|
|
279
|
+
};
|
|
280
|
+
// mouseup事件
|
|
281
|
+
handleDocumentMouseup = (e) => {
|
|
282
|
+
//this.showToolbox(); // 拖拽结束时重新显示工具栏
|
|
283
|
+
//this.updateToolboxPosition();
|
|
284
|
+
this.updateToolboxesPosition();
|
|
285
|
+
this.isDragging = false;
|
|
286
|
+
this.table.classList.remove("table-resizing");
|
|
287
|
+
document.removeEventListener("mousemove", this.handleDocumentMousemove);
|
|
288
|
+
document.removeEventListener("mouseup", this.handleDocumentMouseup);
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Configures and creates the toolbox for manipulating with columns
|
|
292
|
+
*
|
|
293
|
+
* @returns {Toolbox}
|
|
294
|
+
*/
|
|
295
|
+
createColumnToolbox() {
|
|
296
|
+
return new Toolbox({
|
|
297
|
+
api: this.api,
|
|
298
|
+
cssModifier: 'column',
|
|
299
|
+
items: [
|
|
300
|
+
{
|
|
301
|
+
label: this.api.i18n.t('Add column to left'),
|
|
302
|
+
icon: IconDirectionLeftDown,
|
|
303
|
+
hideIf: () => {
|
|
304
|
+
return this.numberOfColumns === this.config.maxcols
|
|
305
|
+
},
|
|
306
|
+
onClick: () => {
|
|
307
|
+
this.addColumn(this.selectedColumn, true);
|
|
308
|
+
this.hideToolboxes();
|
|
309
|
+
this.updateColWidth();
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
label: this.api.i18n.t('Add column to right'),
|
|
314
|
+
icon: IconDirectionRightDown,
|
|
315
|
+
hideIf: () => {
|
|
316
|
+
return this.numberOfColumns === this.config.maxcols
|
|
317
|
+
},
|
|
318
|
+
onClick: () => {
|
|
319
|
+
this.addColumn(this.selectedColumn + 1, true);
|
|
320
|
+
this.hideToolboxes();
|
|
321
|
+
this.updateColWidth();
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
label: this.api.i18n.t('Delete column'),
|
|
326
|
+
icon: IconCross,
|
|
327
|
+
hideIf: () => {
|
|
328
|
+
return this.numberOfColumns === 1;
|
|
329
|
+
},
|
|
330
|
+
confirmationRequired: true,
|
|
331
|
+
onClick: () => {
|
|
332
|
+
this.deleteColumn(this.selectedColumn);
|
|
333
|
+
this.hideToolboxes();
|
|
334
|
+
this.updateColWidth();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
onOpen: () => {
|
|
339
|
+
this.selectColumn(this.hoveredColumn);
|
|
340
|
+
this.hideRowToolbox();
|
|
341
|
+
},
|
|
342
|
+
onClose: () => {
|
|
343
|
+
this.unselectColumn();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Configures and creates the toolbox for manipulating with rows
|
|
350
|
+
*
|
|
351
|
+
* @returns {Toolbox}
|
|
352
|
+
*/
|
|
353
|
+
createRowToolbox() {
|
|
354
|
+
return new Toolbox({
|
|
355
|
+
api: this.api,
|
|
356
|
+
cssModifier: 'row',
|
|
357
|
+
items: [
|
|
358
|
+
{
|
|
359
|
+
label: this.api.i18n.t('Add row above'),
|
|
360
|
+
icon: IconDirectionUpRight,
|
|
361
|
+
hideIf: () => {
|
|
362
|
+
return this.numberOfRows === this.config.maxrows
|
|
363
|
+
},
|
|
364
|
+
onClick: () => {
|
|
365
|
+
this.addRow(this.selectedRow, true);
|
|
366
|
+
this.hideToolboxes();
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
label: this.api.i18n.t('Add row below'),
|
|
371
|
+
icon: IconDirectionDownRight,
|
|
372
|
+
hideIf: () => {
|
|
373
|
+
return this.numberOfRows === this.config.maxrows
|
|
374
|
+
},
|
|
375
|
+
onClick: () => {
|
|
376
|
+
this.addRow(this.selectedRow + 1, true);
|
|
377
|
+
this.hideToolboxes();
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
label: this.api.i18n.t('Delete row'),
|
|
382
|
+
icon: IconCross,
|
|
383
|
+
hideIf: () => {
|
|
384
|
+
return this.numberOfRows === 1;
|
|
385
|
+
},
|
|
386
|
+
confirmationRequired: true,
|
|
387
|
+
onClick: () => {
|
|
388
|
+
this.deleteRow(this.selectedRow);
|
|
389
|
+
this.hideToolboxes();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
onOpen: () => {
|
|
394
|
+
this.selectRow(this.hoveredRow);
|
|
395
|
+
this.hideColumnToolbox();
|
|
396
|
+
},
|
|
397
|
+
onClose: () => {
|
|
398
|
+
this.unselectRow();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* When you press enter it moves the cursor down to the next row
|
|
405
|
+
* or creates it if the click occurred on the last one
|
|
406
|
+
*/
|
|
407
|
+
moveCursorToNextRow() {
|
|
408
|
+
if (this.focusedCell.row !== this.numberOfRows) {
|
|
409
|
+
this.focusedCell.row += 1;
|
|
410
|
+
//this.focusCell(this.focusedCell);
|
|
411
|
+
this.focusCell();
|
|
412
|
+
} else {
|
|
413
|
+
this.addRow();
|
|
414
|
+
this.focusedCell.row += 1;
|
|
415
|
+
//this.focusCell(this.focusedCell);
|
|
416
|
+
this.focusCell();
|
|
417
|
+
this.updateToolboxesPosition(0, 0);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get table cell by row and col index
|
|
423
|
+
*
|
|
424
|
+
* @param {number} row - cell row coordinate
|
|
425
|
+
* @param {number} column - cell column coordinate
|
|
426
|
+
* @returns {HTMLElement}
|
|
427
|
+
*/
|
|
428
|
+
getCell(row:number, column:number) {
|
|
429
|
+
return this.table.querySelectorAll(`.${CSS.row}:nth-child(${row}) .${CSS.cell}`)[column - 1];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get table row by index
|
|
434
|
+
*
|
|
435
|
+
* @param {number} row - row coordinate
|
|
436
|
+
* @returns {HTMLElement}
|
|
437
|
+
*/
|
|
438
|
+
getRow(row:number) {
|
|
439
|
+
return this.table.querySelector(`.${CSS.row}:nth-child(${row})`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The parent of the cell which is the row
|
|
444
|
+
*
|
|
445
|
+
* @param {HTMLElement} cell - cell element
|
|
446
|
+
* @returns {HTMLElement}
|
|
447
|
+
*/
|
|
448
|
+
getRowByCell(cell:Element) {
|
|
449
|
+
return cell.parentElement;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Ger row's first cell
|
|
454
|
+
*
|
|
455
|
+
* @param {Element} row - row to find its first cell
|
|
456
|
+
* @returns {Element}
|
|
457
|
+
*/
|
|
458
|
+
getRowFirstCell(row:Element) {
|
|
459
|
+
return row.querySelector(`.${CSS.cell}:first-child`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Set the sell's content by row and column numbers
|
|
464
|
+
*
|
|
465
|
+
* @param {number} row - cell row coordinate
|
|
466
|
+
* @param {number} column - cell column coordinate
|
|
467
|
+
* @param {string} content - cell HTML content
|
|
468
|
+
*/
|
|
469
|
+
setCellContent(row:number, column:number, content:string) {
|
|
470
|
+
const cell = this.getCell(row, column);
|
|
471
|
+
|
|
472
|
+
cell.innerHTML = content;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Add column in table on index place
|
|
477
|
+
* Add cells in each row
|
|
478
|
+
*
|
|
479
|
+
* @param {number} columnIndex - number in the array of columns, where new column to insert, -1 if insert at the end
|
|
480
|
+
* @param {boolean} [setFocus] - pass true to focus the first cell
|
|
481
|
+
*/
|
|
482
|
+
addColumn(columnIndex = -1, setFocus = false) {
|
|
483
|
+
let numOfColumns=this.numberOfColumns;
|
|
484
|
+
/**
|
|
485
|
+
* Check if the number of columns has reached the maximum allowed columns specified in the configuration,
|
|
486
|
+
* and if so, exit the function to prevent adding more columns beyond the limit.
|
|
487
|
+
*/
|
|
488
|
+
if (this.config && this.config.maxcols && this.numberOfColumns >= this.config.maxcols) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Iterate all rows and add a new cell to them for creating a column
|
|
494
|
+
*/
|
|
495
|
+
for (let rowIndex = 1; rowIndex <= this.numberOfRows; rowIndex++) {
|
|
496
|
+
let cell;
|
|
497
|
+
const cellElem = this.createCell();
|
|
498
|
+
|
|
499
|
+
if (columnIndex > 0 && columnIndex <= numOfColumns) {
|
|
500
|
+
cell = this.getCell(rowIndex, columnIndex);
|
|
501
|
+
|
|
502
|
+
$.insertBefore(cellElem, cell);
|
|
503
|
+
} else {
|
|
504
|
+
cell = this.getRow(rowIndex).appendChild(cellElem);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Autofocus first cell
|
|
509
|
+
*/
|
|
510
|
+
if (rowIndex === 1) {
|
|
511
|
+
const firstCell = this.getCell(rowIndex, columnIndex > 0 ? columnIndex : numOfColumns + 1);
|
|
512
|
+
|
|
513
|
+
if (firstCell && setFocus) {
|
|
514
|
+
$.focus(firstCell);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const addColButton = this.wrapper.querySelector(`.${CSS.addColumn}`);
|
|
520
|
+
if (this.config?.maxcols && this.numberOfColumns > this.config.maxcols - 1 && addColButton ){
|
|
521
|
+
addColButton.classList.add(CSS.addColumnDisabled);
|
|
522
|
+
}
|
|
523
|
+
this.addHeadingAttrToFirstRow();
|
|
524
|
+
this.colWidthArr.splice(columnIndex - 1, 0, this.defaultCellWidth);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Add row in table on index place
|
|
529
|
+
*
|
|
530
|
+
* @param {number} index - number in the array of rows, where new column to insert, -1 if insert at the end
|
|
531
|
+
* @param {boolean} [setFocus] - pass true to focus the inserted row
|
|
532
|
+
* @returns {HTMLElement} row
|
|
533
|
+
*/
|
|
534
|
+
addRow(index = -1, setFocus = false) {
|
|
535
|
+
let insertedRow;
|
|
536
|
+
let rowElem = $.make('div', CSS.row);
|
|
537
|
+
|
|
538
|
+
if (this.tunes.withHeadings) {
|
|
539
|
+
this.removeHeadingAttrFromFirstRow();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* We remember the number of columns, because it is calculated
|
|
544
|
+
* by the number of cells in the first row
|
|
545
|
+
* It is necessary that the first line is filled in correctly
|
|
546
|
+
*/
|
|
547
|
+
let numberOfColumns = this.numberOfColumns;
|
|
548
|
+
/**
|
|
549
|
+
* Check if the number of rows has reached the maximum allowed rows specified in the configuration,
|
|
550
|
+
* and if so, exit the function to prevent adding more columns beyond the limit.
|
|
551
|
+
*/
|
|
552
|
+
if (this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && addRowButton) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (index > 0 && index <= this.numberOfRows) {
|
|
557
|
+
let row = this.getRow(index);
|
|
558
|
+
|
|
559
|
+
insertedRow = $.insertBefore(rowElem, row);
|
|
560
|
+
} else {
|
|
561
|
+
insertedRow = this.table.appendChild(rowElem);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this.fillRow(insertedRow, numberOfColumns);
|
|
565
|
+
|
|
566
|
+
if (this.tunes.withHeadings) {
|
|
567
|
+
this.addHeadingAttrToFirstRow();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const insertedRowFirstCell = this.getRowFirstCell(insertedRow);
|
|
571
|
+
|
|
572
|
+
if (insertedRowFirstCell && setFocus) {
|
|
573
|
+
$.focus(insertedRowFirstCell);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const addRowButton = this.wrapper.querySelector(`.${CSS.addRow}`);
|
|
577
|
+
if (this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && addRowButton) {
|
|
578
|
+
addRowButton.classList.add(CSS.addRowDisabled);
|
|
579
|
+
}
|
|
580
|
+
return insertedRow;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Delete a column by index
|
|
585
|
+
*
|
|
586
|
+
* @param {number} index
|
|
587
|
+
*/
|
|
588
|
+
deleteColumn(index: number) {
|
|
589
|
+
for (let i = 1; i <= this.numberOfRows; i++) {
|
|
590
|
+
const cell = this.getCell(i, index);
|
|
591
|
+
|
|
592
|
+
if (!cell) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
cell.remove();
|
|
597
|
+
}
|
|
598
|
+
const addColButton = this.wrapper.querySelector(`.${CSS.addColumn}`);
|
|
599
|
+
if (addColButton) {
|
|
600
|
+
addColButton.classList.remove(CSS.addColumnDisabled);
|
|
601
|
+
}
|
|
602
|
+
this.colWidthArr.splice(index - 1, 1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Delete a row by index
|
|
607
|
+
*
|
|
608
|
+
* @param {number} index
|
|
609
|
+
*/
|
|
610
|
+
deleteRow(index) {
|
|
611
|
+
this.getRow(index).remove();
|
|
612
|
+
const addRowButton = this.wrapper.querySelector(`.${CSS.addRow}`);
|
|
613
|
+
if (addRowButton) {
|
|
614
|
+
addRowButton.classList.remove(CSS.addRowDisabled);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.addHeadingAttrToFirstRow();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Create a wrapper containing a table, toolboxes
|
|
622
|
+
* and buttons for adding rows and columns
|
|
623
|
+
*
|
|
624
|
+
* @returns {HTMLElement} wrapper - where all buttons for a table and the table itself will be
|
|
625
|
+
*/
|
|
626
|
+
createTableWrapper() {
|
|
627
|
+
this.wrapper = $.make('div', CSS.wrapper);
|
|
628
|
+
this.table = $.make('div', CSS.table);
|
|
629
|
+
|
|
630
|
+
if (this.readOnly) {
|
|
631
|
+
this.wrapper.classList.add(CSS.wrapperReadOnly);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
this.wrapper.appendChild(this.toolboxRow.element);
|
|
635
|
+
this.wrapper.appendChild(this.toolboxColumn.element);
|
|
636
|
+
this.wrapper.appendChild(this.table);
|
|
637
|
+
|
|
638
|
+
// if (!this.readOnly) {
|
|
639
|
+
// const addColumnButton = $.make('div', CSS.addColumn, {
|
|
640
|
+
// innerHTML: IconPlus
|
|
641
|
+
// });
|
|
642
|
+
// const addRowButton = $.make('div', CSS.addRow, {
|
|
643
|
+
// innerHTML: IconPlus
|
|
644
|
+
// });
|
|
645
|
+
|
|
646
|
+
// this.wrapper.appendChild(addColumnButton);
|
|
647
|
+
// this.wrapper.appendChild(addRowButton);
|
|
648
|
+
// }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Returns the size of the table based on initial data or config "size" property
|
|
653
|
+
*
|
|
654
|
+
* @return {{rows: number, cols: number}} - number of cols and rows
|
|
655
|
+
*/
|
|
656
|
+
computeInitialSize() {
|
|
657
|
+
const content = this.data && this.data.content;
|
|
658
|
+
const isValidArray = Array.isArray(content);
|
|
659
|
+
const isNotEmptyArray = isValidArray ? content.length : false;
|
|
660
|
+
const contentRows = isValidArray ? content.length : undefined;
|
|
661
|
+
const contentCols = isNotEmptyArray ? content[0].length : undefined;
|
|
662
|
+
const parsedRows = Number.parseInt(this.config && this.config.rows);
|
|
663
|
+
const parsedCols = Number.parseInt(this.config && this.config.cols);
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Value of config have to be positive number
|
|
667
|
+
*/
|
|
668
|
+
const configRows = !isNaN(parsedRows) && parsedRows > 0 ? parsedRows : undefined;
|
|
669
|
+
const configCols = !isNaN(parsedCols) && parsedCols > 0 ? parsedCols : undefined;
|
|
670
|
+
const defaultRows = 2;
|
|
671
|
+
const defaultCols = 2;
|
|
672
|
+
const rows = contentRows || configRows || defaultRows;
|
|
673
|
+
const cols = contentCols || configCols || defaultCols;
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
rows: rows,
|
|
677
|
+
cols: cols
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Resize table to match config size or transmitted data size
|
|
683
|
+
*
|
|
684
|
+
* @return {{rows: number, cols: number}} - number of cols and rows
|
|
685
|
+
*/
|
|
686
|
+
resize() {
|
|
687
|
+
const { rows, cols } = this.computeInitialSize();
|
|
688
|
+
|
|
689
|
+
for (let i = 0; i < rows; i++) {
|
|
690
|
+
this.addRow();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
for (let i = 0; i < cols; i++) {
|
|
694
|
+
this.addColumn();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
}
|
|
699
|
+
updateColWidth() {
|
|
700
|
+
//设置各列宽度
|
|
701
|
+
let colWidth = "";
|
|
702
|
+
if (this.colWidthArr && this.colWidthArr.length > 0) {
|
|
703
|
+
for (let i = 0; i < this.colWidthArr.length; i++) {
|
|
704
|
+
colWidth += " " + this.colWidthArr[i] + "px";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (colWidth) {
|
|
708
|
+
this.wrapper.style.setProperty('--col-width', colWidth);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Fills the table with data passed to the constructor
|
|
715
|
+
*
|
|
716
|
+
* @returns {void}
|
|
717
|
+
*/
|
|
718
|
+
fill() {
|
|
719
|
+
const data = this.data;
|
|
720
|
+
|
|
721
|
+
if (data && data.content) {
|
|
722
|
+
for (let i = 0; i < data.content.length; i++) {
|
|
723
|
+
for (let j = 0; j < data.content[i].length; j++) {
|
|
724
|
+
this.setCellContent(i + 1, j + 1, data.content[i][j]);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Fills a row with cells
|
|
732
|
+
*
|
|
733
|
+
* @param {HTMLElement} row - row to fill
|
|
734
|
+
* @param {number} numberOfColumns - how many cells should be in a row
|
|
735
|
+
*/
|
|
736
|
+
fillRow(row, numberOfColumns) {
|
|
737
|
+
for (let i = 1; i <= numberOfColumns; i++) {
|
|
738
|
+
const newCell = this.createCell();
|
|
739
|
+
|
|
740
|
+
row.appendChild(newCell);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Creating a cell element
|
|
746
|
+
*
|
|
747
|
+
* @return {Element}
|
|
748
|
+
*/
|
|
749
|
+
createCell() {
|
|
750
|
+
return $.make('div', CSS.cell, {
|
|
751
|
+
contentEditable: !this.readOnly
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Get number of rows in the table
|
|
757
|
+
*/
|
|
758
|
+
get numberOfRows() {
|
|
759
|
+
return this.table.childElementCount;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Get number of columns in the table
|
|
764
|
+
*/
|
|
765
|
+
get numberOfColumns() {
|
|
766
|
+
if (this.numberOfRows) {
|
|
767
|
+
return this.table.querySelectorAll(`.${CSS.row}:first-child .${CSS.cell}`).length;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Is the column toolbox menu displayed or not
|
|
775
|
+
*
|
|
776
|
+
* @returns {boolean}
|
|
777
|
+
*/
|
|
778
|
+
get isColumnMenuShowing() {
|
|
779
|
+
return this.selectedColumn !== 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Is the row toolbox menu displayed or not
|
|
784
|
+
*
|
|
785
|
+
* @returns {boolean}
|
|
786
|
+
*/
|
|
787
|
+
get isRowMenuShowing() {
|
|
788
|
+
return this.selectedRow !== 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Recalculate position of toolbox icons
|
|
793
|
+
*
|
|
794
|
+
* @param {Event} event - mouse move event
|
|
795
|
+
*/
|
|
796
|
+
onMouseMoveInTable(event) {
|
|
797
|
+
const { row, column, deltaXCell, deltaYCell } = this.getHoveredCell(event);
|
|
798
|
+
|
|
799
|
+
this.hoveredColumn = column;
|
|
800
|
+
this.hoveredRow = row;
|
|
801
|
+
|
|
802
|
+
this.updateToolboxesPosition();
|
|
803
|
+
|
|
804
|
+
if (this.hoveredCell !== null && this.hoveredCell.classList.contains("cell-resizable")) {
|
|
805
|
+
this.hoveredCell.classList.remove("cell-resizable");
|
|
806
|
+
}
|
|
807
|
+
this.hoveredCell = this.getCell(row, column);
|
|
808
|
+
|
|
809
|
+
// 判断是否可以调整列宽
|
|
810
|
+
if (this.cellIsResizable(this.hoveredCell, deltaXCell)) {
|
|
811
|
+
// 将鼠标改为拖拽光标
|
|
812
|
+
if (!this.hoveredCell.classList.contains("cell-resizable"))
|
|
813
|
+
this.hoveredCell.classList.add("cell-resizable");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
}
|
|
817
|
+
cellIsResizable(cell: HTMLElement, deltaX: number) {
|
|
818
|
+
// 判断光标是否在单元格右侧10px的位置
|
|
819
|
+
const cellWidth = cell.clientWidth;
|
|
820
|
+
return cellWidth - deltaX <= 10;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Prevents default Enter behaviors
|
|
824
|
+
* Adds Shift+Enter processing
|
|
825
|
+
*
|
|
826
|
+
* @param {KeyboardEvent} event - keypress event
|
|
827
|
+
*/
|
|
828
|
+
onKeyPressListener(event) {
|
|
829
|
+
if (event.key === 'Enter') {
|
|
830
|
+
if (event.shiftKey) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
this.moveCursorToNextRow();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return event.key !== 'Enter';
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Prevents tab keydown event from bubbling
|
|
842
|
+
* so that it only works inside the table
|
|
843
|
+
*
|
|
844
|
+
* @param {KeyboardEvent} event - keydown event
|
|
845
|
+
*/
|
|
846
|
+
onKeyDownListener(event) {
|
|
847
|
+
if (event.key === 'Tab') {
|
|
848
|
+
event.stopPropagation();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Set the coordinates of the cell that the focus has moved to
|
|
854
|
+
*
|
|
855
|
+
* @param {FocusEvent} event - focusin event
|
|
856
|
+
*/
|
|
857
|
+
focusInTableListener(event) {
|
|
858
|
+
const cell = event.target;
|
|
859
|
+
const row = this.getRowByCell(cell);
|
|
860
|
+
|
|
861
|
+
this.focusedCell = {
|
|
862
|
+
row: Array.from(this.table.querySelectorAll(`.${CSS.row}`)).indexOf(row) + 1,
|
|
863
|
+
column: Array.from(row.querySelectorAll(`.${CSS.cell}`)).indexOf(cell) + 1
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Unselect row/column
|
|
869
|
+
* Close toolbox menu
|
|
870
|
+
* Hide toolboxes
|
|
871
|
+
*
|
|
872
|
+
* @returns {void}
|
|
873
|
+
*/
|
|
874
|
+
hideToolboxes() {
|
|
875
|
+
this.hideRowToolbox();
|
|
876
|
+
this.hideColumnToolbox();
|
|
877
|
+
this.updateToolboxesPosition();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Unselect row, close toolbox
|
|
882
|
+
*
|
|
883
|
+
* @returns {void}
|
|
884
|
+
*/
|
|
885
|
+
hideRowToolbox() {
|
|
886
|
+
this.unselectRow();
|
|
887
|
+
this.toolboxRow.hide();
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Unselect column, close toolbox
|
|
891
|
+
*
|
|
892
|
+
* @returns {void}
|
|
893
|
+
*/
|
|
894
|
+
hideColumnToolbox() {
|
|
895
|
+
this.unselectColumn();
|
|
896
|
+
|
|
897
|
+
this.toolboxColumn.hide();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Set the cursor focus to the focused cell
|
|
902
|
+
*
|
|
903
|
+
* @returns {void}
|
|
904
|
+
*/
|
|
905
|
+
focusCell() {
|
|
906
|
+
this.focusedCellElem.focus();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Get current focused element
|
|
911
|
+
*
|
|
912
|
+
* @returns {HTMLElement} - focused cell
|
|
913
|
+
*/
|
|
914
|
+
get focusedCellElem() {
|
|
915
|
+
const { row, column } = this.focusedCell;
|
|
916
|
+
|
|
917
|
+
return this.getCell(row, column);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Update toolboxes position
|
|
922
|
+
*
|
|
923
|
+
* @param {number} row - hovered row
|
|
924
|
+
* @param {number} column - hovered column
|
|
925
|
+
*/
|
|
926
|
+
updateToolboxesPosition(row = this.hoveredRow, column = this.hoveredColumn) {
|
|
927
|
+
if (!this.isColumnMenuShowing) {
|
|
928
|
+
if (column > 0 && column <= this.numberOfColumns) { // not sure this statement is needed. Maybe it should be fixed in getHoveredCell()
|
|
929
|
+
this.toolboxColumn.show(() => {
|
|
930
|
+
return {
|
|
931
|
+
left: `calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${column} - 1) * 2))`
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!this.isRowMenuShowing) {
|
|
938
|
+
if (row > 0 && row <= this.numberOfRows) { // not sure this statement is needed. Maybe it should be fixed in getHoveredCell()
|
|
939
|
+
this.toolboxRow.show(() => {
|
|
940
|
+
const hoveredRowElement = this.getRow(row);
|
|
941
|
+
const { fromTopBorder } = $.getRelativeCoordsOfTwoElems(this.table, hoveredRowElement);
|
|
942
|
+
const { height } = hoveredRowElement.getBoundingClientRect();
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
top: `${Math.ceil(fromTopBorder + height / 2)}px`
|
|
946
|
+
};
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Makes the first row headings
|
|
954
|
+
*
|
|
955
|
+
* @param {boolean} withHeadings - use headings row or not
|
|
956
|
+
*/
|
|
957
|
+
setHeadingsSetting(withHeadings) {
|
|
958
|
+
this.tunes.withHeadings = withHeadings;
|
|
959
|
+
|
|
960
|
+
if (withHeadings) {
|
|
961
|
+
this.table.classList.add(CSS.withHeadings);
|
|
962
|
+
this.addHeadingAttrToFirstRow();
|
|
963
|
+
} else {
|
|
964
|
+
this.table.classList.remove(CSS.withHeadings);
|
|
965
|
+
this.removeHeadingAttrFromFirstRow();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Adds an attribute for displaying the placeholder in the cell
|
|
971
|
+
*/
|
|
972
|
+
addHeadingAttrToFirstRow() {
|
|
973
|
+
for (let cellIndex = 1; cellIndex <= this.numberOfColumns; cellIndex++) {
|
|
974
|
+
let cell = this.getCell(1, cellIndex);
|
|
975
|
+
|
|
976
|
+
if (cell) {
|
|
977
|
+
cell.setAttribute('heading', this.api.i18n.t('Heading'));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Removes an attribute for displaying the placeholder in the cell
|
|
984
|
+
*/
|
|
985
|
+
removeHeadingAttrFromFirstRow() {
|
|
986
|
+
for (let cellIndex = 1; cellIndex <= this.numberOfColumns; cellIndex++) {
|
|
987
|
+
let cell = this.getCell(1, cellIndex);
|
|
988
|
+
|
|
989
|
+
if (cell) {
|
|
990
|
+
cell.removeAttribute('heading');
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Add effect of a selected row
|
|
997
|
+
*
|
|
998
|
+
* @param {number} index
|
|
999
|
+
*/
|
|
1000
|
+
selectRow(index) {
|
|
1001
|
+
const row = this.getRow(index);
|
|
1002
|
+
|
|
1003
|
+
if (row) {
|
|
1004
|
+
this.selectedRow = index;
|
|
1005
|
+
row.classList.add(CSS.rowSelected);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Remove effect of a selected row
|
|
1011
|
+
*/
|
|
1012
|
+
unselectRow() {
|
|
1013
|
+
if (this.selectedRow <= 0) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const row = this.table.querySelector(`.${CSS.rowSelected}`);
|
|
1018
|
+
|
|
1019
|
+
if (row) {
|
|
1020
|
+
row.classList.remove(CSS.rowSelected);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
this.selectedRow = 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Add effect of a selected column
|
|
1028
|
+
*
|
|
1029
|
+
* @param {number} index
|
|
1030
|
+
*/
|
|
1031
|
+
selectColumn(index) {
|
|
1032
|
+
for (let i = 1; i <= this.numberOfRows; i++) {
|
|
1033
|
+
const cell = this.getCell(i, index);
|
|
1034
|
+
|
|
1035
|
+
if (cell) {
|
|
1036
|
+
cell.classList.add(CSS.cellSelected);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
this.selectedColumn = index;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Remove effect of a selected column
|
|
1045
|
+
*/
|
|
1046
|
+
unselectColumn() {
|
|
1047
|
+
if (this.selectedColumn <= 0) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
let cells = this.table.querySelectorAll(`.${CSS.cellSelected}`);
|
|
1052
|
+
|
|
1053
|
+
Array.from(cells).forEach(column => {
|
|
1054
|
+
column.classList.remove(CSS.cellSelected);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
this.selectedColumn = 0;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Calculates the row and column that the cursor is currently hovering over
|
|
1062
|
+
* The search was optimized from O(n) to O (log n) via bin search to reduce the number of calculations
|
|
1063
|
+
*
|
|
1064
|
+
* @param {Event} event - mousemove event
|
|
1065
|
+
* @returns hovered cell coordinates as an integer row and column
|
|
1066
|
+
*/
|
|
1067
|
+
getHoveredCell(event: MouseEvent) {
|
|
1068
|
+
let hoveredRow = this.hoveredRow;
|
|
1069
|
+
let hoveredColumn = this.hoveredColumn;
|
|
1070
|
+
const { width, height, x, y } = $.getCursorPositionRelativeToElement(this.table, event);
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
// Looking for hovered column
|
|
1075
|
+
if (x >= 0) {
|
|
1076
|
+
const beforeTheLeftBorder1 = ({ fromLeftBorder }: { fromLeftBorder: number }) => x < fromLeftBorder;
|
|
1077
|
+
const afterTheRightBorder1 = ({ fromRightBorder }: { fromRightBorder: number }) => x > (width - fromRightBorder);
|
|
1078
|
+
hoveredColumn = this.binSearch(
|
|
1079
|
+
this.numberOfColumns,
|
|
1080
|
+
(mid) => this.getCell(1, mid),
|
|
1081
|
+
beforeTheLeftBorder1,
|
|
1082
|
+
afterTheRightBorder1
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Looking for hovered row
|
|
1087
|
+
if (y >= 0) {
|
|
1088
|
+
const beforeTheLeftBorder2 = ({ fromTopBorder }: { fromTopBorder: number }) => y < fromTopBorder;
|
|
1089
|
+
const afterTheRightBorder2 = ({ fromBottomBorder }: { fromBottomBorder: number }) => y > (height - fromBottomBorder);
|
|
1090
|
+
hoveredRow = this.binSearch(
|
|
1091
|
+
this.numberOfRows,
|
|
1092
|
+
(mid) => this.getCell(mid, 1),
|
|
1093
|
+
beforeTheLeftBorder2,
|
|
1094
|
+
afterTheRightBorder2
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
const row = hoveredRow || this.hoveredRow;
|
|
1098
|
+
const column = hoveredColumn || this.hoveredColumn;
|
|
1099
|
+
// 获取鼠标在单元格内的坐标
|
|
1100
|
+
const { deltaXCell, deltaYCell } = this.getMousePositionRelateToCell(
|
|
1101
|
+
row,
|
|
1102
|
+
column,
|
|
1103
|
+
x,
|
|
1104
|
+
y
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
row,
|
|
1109
|
+
column,
|
|
1110
|
+
deltaXCell,
|
|
1111
|
+
deltaYCell
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
// 获取鼠标相对于单元格的坐标
|
|
1115
|
+
getMousePositionRelateToCell(row:number, col:number, deltaX:number, deltaY:number) {
|
|
1116
|
+
const cell = this.getCell(row, col);
|
|
1117
|
+
const { fromTopBorder, fromLeftBorder } = $.getRelativeCoordsOfTwoElems(
|
|
1118
|
+
this.table,
|
|
1119
|
+
cell
|
|
1120
|
+
);
|
|
1121
|
+
return {
|
|
1122
|
+
deltaXCell: deltaX - fromLeftBorder,
|
|
1123
|
+
deltaYCell: deltaY - fromTopBorder,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Looks for the index of the cell the mouse is hovering over.
|
|
1128
|
+
* Cells can be represented as ordered intervals with left and
|
|
1129
|
+
* right (upper and lower for rows) borders inside the table, if the mouse enters it, then this is our index
|
|
1130
|
+
*
|
|
1131
|
+
* @param {number} numberOfCells - upper bound of binary search
|
|
1132
|
+
* @param {function} getCell - function to take the currently viewed cell
|
|
1133
|
+
* @param {function} beforeTheLeftBorder - determines the cursor position, to the left of the cell or not
|
|
1134
|
+
* @param {function} afterTheRightBorder - determines the cursor position, to the right of the cell or not
|
|
1135
|
+
* @returns {number}
|
|
1136
|
+
*/
|
|
1137
|
+
binSearch(numberOfCells, getCell, beforeTheLeftBorder, afterTheRightBorder) {
|
|
1138
|
+
let leftBorder = 0;
|
|
1139
|
+
let rightBorder = numberOfCells + 1;
|
|
1140
|
+
let totalIterations = 0;
|
|
1141
|
+
let mid;
|
|
1142
|
+
|
|
1143
|
+
while (leftBorder < rightBorder - 1 && totalIterations < 10) {
|
|
1144
|
+
mid = Math.ceil((leftBorder + rightBorder) / 2);
|
|
1145
|
+
|
|
1146
|
+
const cell = getCell(mid);
|
|
1147
|
+
const relativeCoords = $.getRelativeCoordsOfTwoElems(this.table, cell);
|
|
1148
|
+
|
|
1149
|
+
if (beforeTheLeftBorder(relativeCoords)) {
|
|
1150
|
+
rightBorder = mid;
|
|
1151
|
+
} else if (afterTheRightBorder(relativeCoords)) {
|
|
1152
|
+
leftBorder = mid;
|
|
1153
|
+
} else {
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
totalIterations++;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return mid;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Collects data from cells into a two-dimensional array
|
|
1165
|
+
*
|
|
1166
|
+
* @returns {string[][]}
|
|
1167
|
+
*/
|
|
1168
|
+
getData() {
|
|
1169
|
+
const data = [];
|
|
1170
|
+
|
|
1171
|
+
for (let i = 1; i <= this.numberOfRows; i++) {
|
|
1172
|
+
const row = this.table.querySelector(`.${CSS.row}:nth-child(${i})`);
|
|
1173
|
+
const cells = Array.from(row.querySelectorAll(`.${CSS.cell}`));
|
|
1174
|
+
const isEmptyRow = cells.every(cell => !cell.textContent.trim());
|
|
1175
|
+
|
|
1176
|
+
if (isEmptyRow) {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
data.push(cells.map(cell => cell.innerHTML));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return data;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Remove listeners on the document
|
|
1188
|
+
*/
|
|
1189
|
+
destroy() {
|
|
1190
|
+
document.removeEventListener('click', this.documentClicked);
|
|
1191
|
+
}
|
|
1192
|
+
}
|