Repository: editor-js/table
Branch: master
Commit: 0eff0828e234
Files: 24
Total size: 59.2 KB
Directory structure:
gitextract_psbywiq4/
├── .eslintignore
├── .eslintrc
├── .github/
│ └── workflows/
│ └── npm-publish.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── src/
│ ├── index.js
│ ├── plugin.js
│ ├── styles/
│ │ ├── index.pcss
│ │ ├── popover.pcss
│ │ ├── settings.pcss
│ │ ├── table.pcss
│ │ └── toolboxes.pcss
│ ├── table.js
│ ├── toolbox.js
│ └── utils/
│ ├── dom.js
│ ├── popover.js
│ └── throttled.js
├── tsconfig.json
└── vite.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
dist
node_modules
.github
================================================
FILE: .eslintrc
================================================
{
"extends": [
"codex"
],
"rules": {
"jsdoc/no-undefined-types": "off"
}
}
================================================
FILE: .github/workflows/npm-publish.yml
================================================
name: Publish package to NPM
on:
push:
branches:
- master
jobs:
publish-and-notify:
uses: codex-team/github-workflows/.github/workflows/npm-publish-and-notify-reusable.yml@main
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }}
================================================
FILE: .gitignore
================================================
.idea
node_modules
dist
================================================
FILE: .npmignore
================================================
.idea/
assets/
src/
.eslintrc
postcss.config.js
vite.config.js
test.html
yarn.lock
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 CodeX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Table tool
The Table Block for the [Editor.js](https://editorjs.io). Finally improved.

## Installation
Get the package
```shell
yarn add @editorjs/table
```
Include module at your application
```javascript
import Table from '@editorjs/table'
```
Optionally, you can load this tool from CDN [JsDelivr CDN](https://cdn.jsdelivr.net/npm/@editorjs/table@latest)
## Usage
Add a new Tool to the `tools` property of the Editor.js initial config.
```javascript
import Table from '@editorjs/table';
var editor = EditorJS({
tools: {
table: Table,
}
});
```
Or init the Table tool with additional settings
```javascript
var editor = EditorJS({
tools: {
table: {
class: Table,
inlineToolbar: true,
config: {
rows: 2,
cols: 3,
maxRows: 5,
maxCols: 5,
},
},
},
});
```
## Config Params
| Field | Type | Description |
| ------------------ | -------- | ---------------------------------------- |
| `rows` | `number` | initial number of rows. `2` by default |
| `cols` | `number` | initial number of columns. `2` by default |
| `maxRows` | `number` | maximum number of rows. `5` by params |
| `maxCols` | `number` | maximum number of columns. `5` by params |
| `withHeadings` | `boolean` | toggle table headings. `false` by default |
| `stretched` | `boolean` | whether the table is stretched to fill the full width of the container |
## Output data
This Tool returns `data` in the following format
| Field | Type | Description |
| -------------- | ------------ | ----------------------------------------- |
| `withHeadings` | `boolean` | Uses the first line as headings |
| `stretched` | `boolean` | whether the table is stretched to fill the full width of the container |
| `content` | `string[][]` | two-dimensional array with table contents |
```json
{
"type" : "table",
"data" : {
"withHeadings": true,
"stretched": false,
"content" : [ [ "Kine", "Pigs", "Chicken" ], [ "1 pcs", "3 pcs", "12 pcs" ], [ "100$", "200$", "150$" ] ]
}
}
```
## CSP support
If you're using Content Security Policy (CSP) pass a `nonce` via [`<meta property="csp-nonce" content={{ nonce }} />`](https://github.com/marco-prontera/vite-plugin-css-injected-by-js#usestrictcsp-boolean) in your document head.
# Support maintenance 🎖
If you're using this tool and editor.js in your business, please consider supporting their maintenance and evolution.
[http://opencollective.com/editorjs](http://opencollective.com/editorjs)
# About CodeX
<img align="right" width="120" height="120" src="https://codex.so/public/app/img/codex-logo.svg" hspace="50">
CodeX is a team of digital specialists around the world interested in building high-quality open source products on a global market. We are [open](https://codex.so/join) for young people who want to constantly improve their skills and grow professionally with experiments in leading technologies.
| 🌐 | Join 👋 | Twitter | Instagram |
| -- | -- | -- | -- |
| [codex.so](https://codex.so) | [codex.so/join](https://codex.so/join) |[@codex_team](http://twitter.com/codex_team) | [@codex_team](http://instagram.com/codex_team) |
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test of a New Beautiful Table</title>
<style>
body,
html {
margin: 0;
font-family: Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
body {
display: flex;
flex-direction: column;
align-items: center;
}
.block {
width: 800px;
margin-top: 40px;
}
#editorjs {
width: 900px;
min-height: 100px;
}
</style>
</head>
<body>
<div class="block"></div>
<div id="editorjs"></div>
<button class="save-button">Save</button>
<pre class="output"></pre>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script type="module">
import Table from './src/index'
const editor = new EditorJS({
autofocus: true,
tools: {
table: {
class: Table,
inlineToolbar: true,
config: {
withHeadings: true,
maxRows: 5,
maxCols: 5
}
}
},
data: {
time: 1625072989362,
blocks: [
{
id: "XXVTfnMlcE",
type: "table",
data: {
withHeadings: true,
content: [
["English", "Russian", "Japanese"],
["Sweet", "Сладкий", "あまい"],
["Good morning", "Доброе утро", "おはようございます"]]
}
}
],
version: "2.22.1"
}
});
const saveButton = document.querySelector('.save-button');
const output = document.querySelector('.output');
saveButton.addEventListener('click', () => {
editor.save().then(savedData => {
output.innerHTML = JSON.stringify(savedData, null, 4);
});
});
</script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "@editorjs/table",
"description": "Table for Editor.js",
"version": "2.4.5",
"license": "MIT",
"repository": "https://github.com/editor-js/table",
"files": [
"dist"
],
"main": "./dist/table.umd.js",
"module": "./dist/table.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/table.mjs",
"require": "./dist/table.umd.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint -c ./.eslintrc --ext .js --fix ."
},
"author": {
"name": "CodeX Team",
"email": "team@ifmo.su"
},
"keywords": [
"codex",
"codex-editor",
"table",
"editor.js",
"editorjs"
],
"devDependencies": {
"autoprefixer": "^9.3.1",
"css-loader": "^1.0.0",
"cssnano": "^4.1.7",
"eslint": "^5.8.0",
"eslint-config-codex": "^2.0.1",
"postcss-import": "^12.0.1",
"postcss-nested": "^4.1.0",
"postcss-nesting": "^7.0.0",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-dts": "^3.9.1",
"typescript": "^5.5.4"
},
"dependencies": {
"@codexteam/icons": "^0.0.6"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: [
require('postcss-import'),
require('autoprefixer'),
require('cssnano'),
require('postcss-nested'),
require('postcss-nesting')
]
};
================================================
FILE: src/index.js
================================================
import Plugin from './plugin';
import './styles/index.pcss';
export default Plugin;
================================================
FILE: src/plugin.js
================================================
import Table from './table';
import * as $ from './utils/dom';
import { IconTable, IconTableWithHeadings, IconTableWithoutHeadings, IconStretch, IconCollapse } from '@codexteam/icons';
/**
* @typedef {object} TableData - configuration that the user can set for the table
* @property {number} rows - number of rows in the table
* @property {number} cols - number of columns in the table
*/
/**
* @typedef {object} Tune - setting for the table
* @property {string} name - tune name
* @property {HTMLElement} icon - icon for the tune
* @property {boolean} isActive - default state of the tune
* @property {void} setTune - set tune state to the table data
*/
/**
* @typedef {object} TableConfig - object with the data transferred to form a table
* @property {boolean} withHeading - setting to use cells of the first row as headings
* @property {string[][]} content - two-dimensional array which contains table content
*/
/**
* @typedef {object} TableConstructor
* @property {TableConfig} data — previously saved data
* @property {TableConfig} config - user config for Tool
* @property {object} api - Editor.js API
* @property {boolean} readOnly - read-only mode flag
*/
/**
* @typedef {import('@editorjs/editorjs').PasteEvent} PasteEvent
*/
/**
* Table block for Editor.js
*/
export default class TableBlock {
/**
* Notify core that read-only mode is supported
*
* @returns {boolean}
*/
static get isReadOnlySupported() {
return true;
}
/**
* Allow to press Enter inside the CodeTool textarea
*
* @returns {boolean}
* @public
*/
static get enableLineBreaks() {
return true;
}
/**
* Render plugin`s main Element and fill it with saved data
*
* @param {TableConstructor} init
*/
constructor({data, config, api, readOnly, block}) {
this.api = api;
this.readOnly = readOnly;
this.config = config;
this.data = {
withHeadings: this.getConfig('withHeadings', false, data),
stretched: this.getConfig('stretched', false, data),
content: data && data.content ? data.content : []
};
this.table = null;
this.block = block;
}
/**
* Get Tool toolbox settings
* icon - Tool icon's SVG
* title - title to show in toolbox
*
* @returns {{icon: string, title: string}}
*/
static get toolbox() {
return {
icon: IconTable,
title: 'Table'
};
}
/**
* Return Tool's view
*
* @returns {HTMLDivElement}
*/
render() {
/** creating table */
this.table = new Table(this.readOnly, this.api, this.data, this.config);
/** creating container around table */
this.container = $.make('div', this.api.styles.block);
this.container.appendChild(this.table.getWrapper());
this.table.setHeadingsSetting(this.data.withHeadings);
return this.container;
}
/**
* Returns plugin settings
*
* @returns {Array}
*/
renderSettings() {
return [
{
label: this.api.i18n.t('With headings'),
icon: IconTableWithHeadings,
isActive: this.data.withHeadings,
closeOnActivate: true,
toggle: true,
onActivate: () => {
this.data.withHeadings = true;
this.table.setHeadingsSetting(this.data.withHeadings);
}
}, {
label: this.api.i18n.t('Without headings'),
icon: IconTableWithoutHeadings,
isActive: !this.data.withHeadings,
closeOnActivate: true,
toggle: true,
onActivate: () => {
this.data.withHeadings = false;
this.table.setHeadingsSetting(this.data.withHeadings);
}
}, {
label: this.data.stretched ? this.api.i18n.t('Collapse') : this.api.i18n.t('Stretch'),
icon: this.data.stretched ? IconCollapse : IconStretch,
closeOnActivate: true,
toggle: true,
onActivate: () => {
this.data.stretched = !this.data.stretched;
this.block.stretched = this.data.stretched;
}
}
];
}
/**
* Extract table data from the view
*
* @returns {TableData} - saved data
*/
save() {
const tableContent = this.table.getData();
const result = {
withHeadings: this.data.withHeadings,
stretched: this.data.stretched,
content: tableContent
};
return result;
}
/**
* Plugin destroyer
*
* @returns {void}
*/
destroy() {
this.table.destroy();
}
/**
* A helper to get config value.
*
* @param {string} configName - the key to get from the config.
* @param {any} defaultValue - default value if config doesn't have passed key
* @param {object} savedData - previously saved data. If passed, the key will be got from there, otherwise from the config
* @returns {any} - config value.
*/
getConfig(configName, defaultValue = undefined, savedData = undefined) {
const data = this.data || savedData;
if (data) {
return data[configName] ? data[configName] : defaultValue;
}
return this.config && this.config[configName] ? this.config[configName] : defaultValue;
}
/**
* Table onPaste configuration
*
* @public
*/
static get pasteConfig() {
return { tags: ['TABLE', 'TR', 'TH', 'TD'] };
}
/**
* On paste callback that is fired from Editor
*
* @param {PasteEvent} event - event with pasted data
*/
onPaste(event) {
const table = event.detail.data;
/** Check if the first row is a header */
const firstRowHeading = table.querySelector(':scope > thead, tr:first-of-type th');
/** Get all rows from the table */
const rows = Array.from(table.querySelectorAll('tr'));
/** Generate a content matrix */
const content = rows.map((row) => {
/** Get cells from row */
const cells = Array.from(row.querySelectorAll('th, td'))
/** Return cells content */
return cells.map((cell) => cell.innerHTML);
});
/** Update Tool's data */
this.data = {
withHeadings: firstRowHeading !== null,
content
};
/** Update table block */
if (this.table.wrapper) {
this.table.wrapper.replaceWith(this.render());
}
}
}
================================================
FILE: src/styles/index.pcss
================================================
@import './table.pcss';
@import './toolboxes.pcss';
@import './settings.pcss';
@import './popover.pcss';
================================================
FILE: src/styles/popover.pcss
================================================
.tc-popover {
--color-border: #eaeaea;
--color-background: #fff;
--color-background-hover: rgba(232,232,235,0.49);
--color-background-confirm: #e24a4a;
--color-background-confirm-hover: #d54040;
--color-text-confirm: #fff;
background: var(--color-background);
border: 1px solid var(--color-border);
box-shadow: 0 3px 15px -3px rgba(13,20,33,0.13);
border-radius: 6px;
padding: 6px;
display: none;
will-change: opacity, transform;
&--opened {
display: block;
animation: menuShowing 100ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;
}
&__item {
display: flex;
align-items: center;
padding: 2px 14px 2px 2px;
border-radius: 5px;
cursor: pointer;
white-space: nowrap;
user-select: none;
&:hover {
background: var(--color-background-hover);
}
&:not(:last-of-type){
margin-bottom: 2px;
}
&-icon {
display: inline-flex;
width: 26px;
height: 26px;
align-items: center;
justify-content: center;
background: var(--color-background);
border-radius: 5px;
border: 1px solid var(--color-border);
margin-right: 8px;
}
&-label {
line-height: 22px;
font-size: 14px;
font-weight: 500;
}
&--confirm {
background: var(--color-background-confirm);
color: var(--color-text-confirm);
&:hover {
background-color: var(--color-background-confirm-hover);
}
}
&--confirm &-icon {
background: var(--color-background-confirm);
border-color: rgba(0,0,0,0.1);
svg {
transition: transform 200ms ease-in;
transform: rotate(90deg) scale(1.2);
}
}
&--hidden {
display: none;
}
}
}
@keyframes menuShowing {
0% {
opacity:0;
transform:translateY(-8px) scale(.9)
}
70% {
opacity:1;
transform:translateY(2px)
}
to {
transform:translateY(0)
}
}
================================================
FILE: src/styles/settings.pcss
================================================
.tc-settings {
.cdx-settings-button {
width: 50%;
margin: 0;
}
}
================================================
FILE: src/styles/table.pcss
================================================
/* tc- project's prefix*/
.tc-wrap {
--color-background: #f9f9fb;
--color-text-secondary: #7b7e89;
--color-border: #e8e8eb;
--cell-size: 34px;
--toolbox-icon-size: 18px;
--toolbox-padding: 6px;
--toolbox-aiming-field-size: calc(
var(--toolbox-icon-size) + 2 * var(--toolbox-padding)
);
border-left: 0px;
position: relative;
height: 100%;
width: 100%;
margin-top: var(--toolbox-icon-size);
box-sizing: border-box;
display: grid;
grid-template-columns: calc(100% - var(--cell-size)) var(--cell-size);
/* Bug-fix: https://github.com/editor-js/table/issues/175 */
z-index: 0;
&--readonly {
grid-template-columns: 100% var(--cell-size);
}
svg {
vertical-align: top;
}
@media print {
border-left: 1px solid var(--color-border);
grid-template-columns: 100% var(--cell-size);
}
.tc-row::after {
@media print {
display: none;
}
}
}
.tc-table {
position: relative;
width: 100%;
height: 100%;
display: grid;
font-size: 14px;
border-top: 1px solid var(--color-border);
line-height: 1.4;
&::after {
position: absolute;
content: "";
width: calc(var(--cell-size));
height: 100%;
left: calc(-1 * var(--cell-size));
top: 0;
}
&::before {
position: absolute;
content: "";
width: 100%;
height: var(--toolbox-aiming-field-size);
top: calc(-1 * var(--toolbox-aiming-field-size));
left: 0;
}
&--heading {
& .tc-row:first-child {
font-weight: 600;
border-bottom: 2px solid var(--color-border);
position: sticky;
top: 0;
z-index: 2;
background: var(--color-background);
& [contenteditable]:empty::before {
content: attr(heading);
color: var(--color-text-secondary);
}
&::after {
bottom: -2px;
border-bottom: 2px solid var(--color-border);
}
}
}
}
.tc-add {
&-column,
&-row {
display: flex;
color: var(--color-text-secondary);
}
@media print {
display: none;
}
}
.tc-add-column {
display: grid;
border-top: 1px solid var(--color-border);
grid-template-columns: var(--cell-size);
grid-auto-rows: var(--cell-size);
place-items: center;
svg {
padding: 5px;
position: sticky;
top: 0;
background-color: var(--color-background);
}
&--disabled {
visibility: hidden;
}
@media print {
display: none;
}
}
.tc-add-row {
height: var(--cell-size);
align-items: center;
padding-left: 4px;
position: relative;
&--disabled {
display: none;
}
&::before {
content: "";
position: absolute;
right: calc(-1 * var(--cell-size));
width: var(--cell-size);
height: 100%;
}
@media print {
display: none;
}
}
.tc-add {
&-column,
&-row {
transition: 0s;
cursor: pointer;
will-change: background-color;
&:hover {
transition: background-color 0.1s ease;
background-color: var(--color-background);
}
}
&-row {
margin-top: 1px;
&:hover::before {
transition: 0.1s;
background-color: var(--color-background);
}
}
}
.tc-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10px, 1fr));
position: relative;
border-bottom: 1px solid var(--color-border);
&::after {
content: "";
pointer-events: none;
position: absolute;
width: var(--cell-size);
height: 100%;
bottom: -1px;
right: calc(-1 * var(--cell-size));
border-bottom: 1px solid var(--color-border);
}
&--selected {
background: var(--color-background);
}
}
.tc-row--selected {
&::after {
background: var(--color-background);
}
}
.tc-cell {
border-right: 1px solid var(--color-border);
padding: 6px 12px 6px 12px;
overflow: hidden;
outline: none;
line-break: normal;
&--selected {
background: var(--color-background);
}
}
.tc-wrap--readonly .tc-row::after {
display: none;
}
================================================
FILE: src/styles/toolboxes.pcss
================================================
.tc-toolbox {
--toolbox-padding: 6px;
--popover-margin: 30px;
--toggler-click-zone-size: 30px;
--toggler-dots-color: #7B7E89;
--toggler-dots-color-hovered: #1D202B;
position: absolute;
cursor: pointer;
z-index: 1;
opacity: 0;
transition: opacity 0.1s;
will-change: left, opacity;
&--column {
top: calc(-1 * (var(--toggler-click-zone-size)));
transform: translateX(calc(-1 * var(--toggler-click-zone-size) / 2));
will-change: left, opacity;
}
&--row {
left: calc(-1 * var(--popover-margin));
transform: translateY(calc(-1 * var(--toggler-click-zone-size) / 2));
margin-top: -1px; /* because of top border */
will-change: top, opacity;
}
&--showed {
opacity: 1;
}
.tc-popover {
position: absolute;
top: 0;
left: var(--popover-margin)
}
&__toggler {
display: flex;
align-items: center;
justify-content: center;
width: var(--toggler-click-zone-size);
height: var(--toggler-click-zone-size);
color: var(--toggler-dots-color);
opacity: 0;
transition: opacity 150ms ease;
will-change: opacity;
&:hover {
color: var(--toggler-dots-color-hovered);
}
svg {
fill: currentColor;
}
}
}
.tc-wrap:hover .tc-toolbox__toggler {
opacity: 1;
}
================================================
FILE: src/table.js
================================================
import Toolbox from './toolbox';
import * as $ from './utils/dom';
import throttled from './utils/throttled';
import {
IconDirectionLeftDown,
IconDirectionRightDown,
IconDirectionUpRight,
IconDirectionDownRight,
IconCross,
IconPlus
} from '@codexteam/icons';
const CSS = {
wrapper: 'tc-wrap',
wrapperReadOnly: 'tc-wrap--readonly',
table: 'tc-table',
row: 'tc-row',
withHeadings: 'tc-table--heading',
rowSelected: 'tc-row--selected',
cell: 'tc-cell',
cellSelected: 'tc-cell--selected',
addRow: 'tc-add-row',
addRowDisabled: 'tc-add-row--disabled',
addColumn: 'tc-add-column',
addColumnDisabled: 'tc-add-column--disabled',
};
/**
* @typedef {object} TableConfig
* @description Tool's config from Editor
* @property {boolean} withHeadings — Uses the first line as headings
* @property {string[][]} withHeadings — two-dimensional array with table contents
*/
/**
* @typedef {object} TableData - object with the data transferred to form a table
* @property {number} rows - number of rows in the table
* @property {number} cols - number of columns in the table
*/
/**
* Generates and manages table contents.
*/
export default class Table {
/**
* Creates
*
* @constructor
* @param {boolean} readOnly - read-only mode flag
* @param {object} api - Editor.js API
* @param {TableData} data - Editor.js API
* @param {TableConfig} config - Editor.js API
*/
constructor(readOnly, api, data, config) {
this.readOnly = readOnly;
this.api = api;
this.data = data;
this.config = config;
/**
* DOM nodes
*/
this.wrapper = null;
this.table = null;
/**
* Toolbox for managing of columns
*/
this.toolboxColumn = this.createColumnToolbox();
this.toolboxRow = this.createRowToolbox();
/**
* Create table and wrapper elements
*/
this.createTableWrapper();
// Current hovered row index
this.hoveredRow = 0;
// Current hovered column index
this.hoveredColumn = 0;
// Index of last selected row via toolbox
this.selectedRow = 0;
// Index of last selected column via toolbox
this.selectedColumn = 0;
// Additional settings for the table
this.tunes = {
withHeadings: false
};
/**
* Resize table to match config/data size
*/
this.resize();
/**
* Fill the table with data
*/
this.fill();
/**
* The cell in which the focus is currently located, if 0 and 0 then there is no focus
* Uses to switch between cells with buttons
*/
this.focusedCell = {
row: 0,
column: 0
};
/**
* Global click listener allows to delegate clicks on some elements
*/
this.documentClicked = (event) => {
const clickedInsideTable = event.target.closest(`.${CSS.table}`) !== null;
const outsideTableClicked = event.target.closest(`.${CSS.wrapper}`) === null;
const clickedOutsideToolboxes = clickedInsideTable || outsideTableClicked;
if (clickedOutsideToolboxes) {
this.hideToolboxes();
}
const clickedOnAddRowButton = event.target.closest(`.${CSS.addRow}`);
const clickedOnAddColumnButton = event.target.closest(`.${CSS.addColumn}`);
/**
* Also, check if clicked in current table, not other (because documentClicked bound to the whole document)
*/
if (clickedOnAddRowButton && clickedOnAddRowButton.parentNode === this.wrapper) {
this.addRow(undefined, true);
this.hideToolboxes();
} else if (clickedOnAddColumnButton && clickedOnAddColumnButton.parentNode === this.wrapper) {
this.addColumn(undefined, true);
this.hideToolboxes();
}
};
if (!this.readOnly) {
this.bindEvents();
}
}
/**
* Returns the rendered table wrapper
*
* @returns {Element}
*/
getWrapper() {
return this.wrapper;
}
/**
* Hangs the necessary handlers to events
*/
bindEvents() {
// set the listener to close toolboxes when click outside
document.addEventListener('click', this.documentClicked);
// Update toolboxes position depending on the mouse movements
this.table.addEventListener('mousemove', throttled(150, (event) => this.onMouseMoveInTable(event)), { passive: true });
// Controls some of the keyboard buttons inside the table
this.table.onkeypress = (event) => this.onKeyPressListener(event);
// Tab is executed by default before keypress, so it must be intercepted on keydown
this.table.addEventListener('keydown', (event) => this.onKeyDownListener(event));
// Determine the position of the cell in focus
this.table.addEventListener('focusin', event => this.focusInTableListener(event));
}
/**
* Configures and creates the toolbox for manipulating with columns
*
* @returns {Toolbox}
*/
createColumnToolbox() {
return new Toolbox({
api: this.api,
cssModifier: 'column',
items: [
{
label: this.api.i18n.t('Add column to left'),
icon: IconDirectionLeftDown,
hideIf: () => {
return this.numberOfColumns === this.config.maxcols
},
onClick: () => {
this.addColumn(this.selectedColumn, true);
this.hideToolboxes();
}
},
{
label: this.api.i18n.t('Add column to right'),
icon: IconDirectionRightDown,
hideIf: () => {
return this.numberOfColumns === this.config.maxcols
},
onClick: () => {
this.addColumn(this.selectedColumn + 1, true);
this.hideToolboxes();
}
},
{
label: this.api.i18n.t('Delete column'),
icon: IconCross,
hideIf: () => {
return this.numberOfColumns === 1;
},
confirmationRequired: true,
onClick: () => {
this.deleteColumn(this.selectedColumn);
this.hideToolboxes();
}
}
],
onOpen: () => {
this.selectColumn(this.hoveredColumn);
this.hideRowToolbox();
},
onClose: () => {
this.unselectColumn();
}
});
}
/**
* Configures and creates the toolbox for manipulating with rows
*
* @returns {Toolbox}
*/
createRowToolbox() {
return new Toolbox({
api: this.api,
cssModifier: 'row',
items: [
{
label: this.api.i18n.t('Add row above'),
icon: IconDirectionUpRight,
hideIf: () => {
return this.numberOfRows === this.config.maxrows
},
onClick: () => {
this.addRow(this.selectedRow, true);
this.hideToolboxes();
}
},
{
label: this.api.i18n.t('Add row below'),
icon: IconDirectionDownRight,
hideIf: () => {
return this.numberOfRows === this.config.maxrows
},
onClick: () => {
this.addRow(this.selectedRow + 1, true);
this.hideToolboxes();
}
},
{
label: this.api.i18n.t('Delete row'),
icon: IconCross,
hideIf: () => {
return this.numberOfRows === 1;
},
confirmationRequired: true,
onClick: () => {
this.deleteRow(this.selectedRow);
this.hideToolboxes();
}
}
],
onOpen: () => {
this.selectRow(this.hoveredRow);
this.hideColumnToolbox();
},
onClose: () => {
this.unselectRow();
}
});
}
/**
* When you press enter it moves the cursor down to the next row
* or creates it if the click occurred on the last one
*/
moveCursorToNextRow() {
if (this.focusedCell.row !== this.numberOfRows) {
this.focusedCell.row += 1;
this.focusCell(this.focusedCell);
} else {
this.addRow();
this.focusedCell.row += 1;
this.focusCell(this.focusedCell);
this.updateToolboxesPosition(0, 0);
}
}
/**
* Get table cell by row and col index
*
* @param {number} row - cell row coordinate
* @param {number} column - cell column coordinate
* @returns {HTMLElement}
*/
getCell(row, column) {
return this.table.querySelectorAll(`.${CSS.row}:nth-child(${row}) .${CSS.cell}`)[column - 1];
}
/**
* Get table row by index
*
* @param {number} row - row coordinate
* @returns {HTMLElement}
*/
getRow(row) {
return this.table.querySelector(`.${CSS.row}:nth-child(${row})`);
}
/**
* The parent of the cell which is the row
*
* @param {HTMLElement} cell - cell element
* @returns {HTMLElement}
*/
getRowByCell(cell) {
return cell.parentElement;
}
/**
* Ger row's first cell
*
* @param {Element} row - row to find its first cell
* @returns {Element}
*/
getRowFirstCell(row) {
return row.querySelector(`.${CSS.cell}:first-child`);
}
/**
* Set the sell's content by row and column numbers
*
* @param {number} row - cell row coordinate
* @param {number} column - cell column coordinate
* @param {string} content - cell HTML content
*/
setCellContent(row, column, content) {
const cell = this.getCell(row, column);
cell.innerHTML = content;
}
/**
* Add column in table on index place
* Add cells in each row
*
* @param {number} columnIndex - number in the array of columns, where new column to insert, -1 if insert at the end
* @param {boolean} [setFocus] - pass true to focus the first cell
*/
addColumn(columnIndex = -1, setFocus = false) {
let numberOfColumns = this.numberOfColumns;
/**
* Check if the number of columns has reached the maximum allowed columns specified in the configuration,
* and if so, exit the function to prevent adding more columns beyond the limit.
*/
if (this.config && this.config.maxcols && this.numberOfColumns >= this.config.maxcols) {
return;
}
/**
* Iterate all rows and add a new cell to them for creating a column
*/
for (let rowIndex = 1; rowIndex <= this.numberOfRows; rowIndex++) {
let cell;
const cellElem = this.createCell();
if (columnIndex > 0 && columnIndex <= numberOfColumns) {
cell = this.getCell(rowIndex, columnIndex);
$.insertBefore(cellElem, cell);
} else {
cell = this.getRow(rowIndex).appendChild(cellElem);
}
/**
* Autofocus first cell
*/
if (rowIndex === 1) {
const firstCell = this.getCell(rowIndex, columnIndex > 0 ? columnIndex : numberOfColumns + 1);
if (firstCell && setFocus) {
$.focus(firstCell);
}
}
}
const addColButton = this.wrapper.querySelector(`.${CSS.addColumn}`);
if (this.config?.maxcols && this.numberOfColumns > this.config.maxcols - 1 && addColButton ){
addColButton.classList.add(CSS.addColumnDisabled);
}
this.addHeadingAttrToFirstRow();
};
/**
* Add row in table on index place
*
* @param {number} index - number in the array of rows, where new column to insert, -1 if insert at the end
* @param {boolean} [setFocus] - pass true to focus the inserted row
* @returns {HTMLElement} row
*/
addRow(index = -1, setFocus = false) {
let insertedRow;
let rowElem = $.make('div', CSS.row);
if (this.tunes.withHeadings) {
this.removeHeadingAttrFromFirstRow();
}
/**
* We remember the number of columns, because it is calculated
* by the number of cells in the first row
* It is necessary that the first line is filled in correctly
*/
let numberOfColumns = this.numberOfColumns;
/**
* Check if the number of rows has reached the maximum allowed rows specified in the configuration,
* and if so, exit the function to prevent adding more columns beyond the limit.
*/
if (this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && addRowButton) {
return;
}
if (index > 0 && index <= this.numberOfRows) {
let row = this.getRow(index);
insertedRow = $.insertBefore(rowElem, row);
} else {
insertedRow = this.table.appendChild(rowElem);
}
this.fillRow(insertedRow, numberOfColumns);
if (this.tunes.withHeadings) {
this.addHeadingAttrToFirstRow();
}
const insertedRowFirstCell = this.getRowFirstCell(insertedRow);
if (insertedRowFirstCell && setFocus) {
$.focus(insertedRowFirstCell);
}
const addRowButton = this.wrapper.querySelector(`.${CSS.addRow}`);
if (this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && addRowButton) {
addRowButton.classList.add(CSS.addRowDisabled);
}
return insertedRow;
};
/**
* Delete a column by index
*
* @param {number} index
*/
deleteColumn(index) {
for (let i = 1; i <= this.numberOfRows; i++) {
const cell = this.getCell(i, index);
if (!cell) {
return;
}
cell.remove();
}
const addColButton = this.wrapper.querySelector(`.${CSS.addColumn}`);
if (addColButton) {
addColButton.classList.remove(CSS.addColumnDisabled);
}
}
/**
* Delete a row by index
*
* @param {number} index
*/
deleteRow(index) {
this.getRow(index).remove();
const addRowButton = this.wrapper.querySelector(`.${CSS.addRow}`);
if (addRowButton) {
addRowButton.classList.remove(CSS.addRowDisabled);
}
this.addHeadingAttrToFirstRow();
}
/**
* Create a wrapper containing a table, toolboxes
* and buttons for adding rows and columns
*
* @returns {HTMLElement} wrapper - where all buttons for a table and the table itself will be
*/
createTableWrapper() {
this.wrapper = $.make('div', CSS.wrapper);
this.table = $.make('div', CSS.table);
if (this.readOnly) {
this.wrapper.classList.add(CSS.wrapperReadOnly);
}
this.wrapper.appendChild(this.toolboxRow.element);
this.wrapper.appendChild(this.toolboxColumn.element);
this.wrapper.appendChild(this.table);
if (!this.readOnly) {
const addColumnButton = $.make('div', CSS.addColumn, {
innerHTML: IconPlus
});
const addRowButton = $.make('div', CSS.addRow, {
innerHTML: IconPlus
});
this.wrapper.appendChild(addColumnButton);
this.wrapper.appendChild(addRowButton);
}
}
/**
* Returns the size of the table based on initial data or config "size" property
*
* @return {{rows: number, cols: number}} - number of cols and rows
*/
computeInitialSize() {
const content = this.data && this.data.content;
const isValidArray = Array.isArray(content);
const isNotEmptyArray = isValidArray ? content.length : false;
const contentRows = isValidArray ? content.length : undefined;
const contentCols = isNotEmptyArray ? content[0].length : undefined;
const parsedRows = Number.parseInt(this.config && this.config.rows);
const parsedCols = Number.parseInt(this.config && this.config.cols);
/**
* Value of config have to be positive number
*/
const configRows = !isNaN(parsedRows) && parsedRows > 0 ? parsedRows : undefined;
const configCols = !isNaN(parsedCols) && parsedCols > 0 ? parsedCols : undefined;
const defaultRows = 2;
const defaultCols = 2;
const rows = contentRows || configRows || defaultRows;
const cols = contentCols || configCols || defaultCols;
return {
rows: rows,
cols: cols
};
}
/**
* Resize table to match config size or transmitted data size
*
* @return {{rows: number, cols: number}} - number of cols and rows
*/
resize() {
const { rows, cols } = this.computeInitialSize();
for (let i = 0; i < rows; i++) {
this.addRow();
}
for (let i = 0; i < cols; i++) {
this.addColumn();
}
}
/**
* Fills the table with data passed to the constructor
*
* @returns {void}
*/
fill() {
const data = this.data;
if (data && data.content) {
for (let i = 0; i < data.content.length; i++) {
for (let j = 0; j < data.content[i].length; j++) {
this.setCellContent(i + 1, j + 1, data.content[i][j]);
}
}
}
}
/**
* Fills a row with cells
*
* @param {HTMLElement} row - row to fill
* @param {number} numberOfColumns - how many cells should be in a row
*/
fillRow(row, numberOfColumns) {
for (let i = 1; i <= numberOfColumns; i++) {
const newCell = this.createCell();
row.appendChild(newCell);
}
}
/**
* Creating a cell element
*
* @return {Element}
*/
createCell() {
return $.make('div', CSS.cell, {
contentEditable: !this.readOnly
});
}
/**
* Get number of rows in the table
*/
get numberOfRows() {
return this.table.childElementCount;
}
/**
* Get number of columns in the table
*/
get numberOfColumns() {
if (this.numberOfRows) {
return this.table.querySelectorAll(`.${CSS.row}:first-child .${CSS.cell}`).length;
}
return 0;
}
/**
* Is the column toolbox menu displayed or not
*
* @returns {boolean}
*/
get isColumnMenuShowing() {
return this.selectedColumn !== 0;
}
/**
* Is the row toolbox menu displayed or not
*
* @returns {boolean}
*/
get isRowMenuShowing() {
return this.selectedRow !== 0;
}
/**
* Recalculate position of toolbox icons
*
* @param {Event} event - mouse move event
*/
onMouseMoveInTable(event) {
const { row, column } = this.getHoveredCell(event);
this.hoveredColumn = column;
this.hoveredRow = row;
this.updateToolboxesPosition();
}
/**
* Prevents default Enter behaviors
* Adds Shift+Enter processing
*
* @param {KeyboardEvent} event - keypress event
*/
onKeyPressListener(event) {
if (event.key === 'Enter') {
if (event.shiftKey) {
return true;
}
this.moveCursorToNextRow();
}
return event.key !== 'Enter';
};
/**
* Prevents tab keydown event from bubbling
* so that it only works inside the table
*
* @param {KeyboardEvent} event - keydown event
*/
onKeyDownListener(event) {
if (event.key === 'Tab') {
event.stopPropagation();
}
}
/**
* Set the coordinates of the cell that the focus has moved to
*
* @param {FocusEvent} event - focusin event
*/
focusInTableListener(event) {
const cell = event.target;
const row = this.getRowByCell(cell);
this.focusedCell = {
row: Array.from(this.table.querySelectorAll(`.${CSS.row}`)).indexOf(row) + 1,
column: Array.from(row.querySelectorAll(`.${CSS.cell}`)).indexOf(cell) + 1
};
}
/**
* Unselect row/column
* Close toolbox menu
* Hide toolboxes
*
* @returns {void}
*/
hideToolboxes() {
this.hideRowToolbox();
this.hideColumnToolbox();
this.updateToolboxesPosition();
}
/**
* Unselect row, close toolbox
*
* @returns {void}
*/
hideRowToolbox() {
this.unselectRow();
this.toolboxRow.hide();
}
/**
* Unselect column, close toolbox
*
* @returns {void}
*/
hideColumnToolbox() {
this.unselectColumn();
this.toolboxColumn.hide();
}
/**
* Set the cursor focus to the focused cell
*
* @returns {void}
*/
focusCell() {
this.focusedCellElem.focus();
}
/**
* Get current focused element
*
* @returns {HTMLElement} - focused cell
*/
get focusedCellElem() {
const { row, column } = this.focusedCell;
return this.getCell(row, column);
}
/**
* Update toolboxes position
*
* @param {number} row - hovered row
* @param {number} column - hovered column
*/
updateToolboxesPosition(row = this.hoveredRow, column = this.hoveredColumn) {
if (!this.isColumnMenuShowing) {
if (column > 0 && column <= this.numberOfColumns) { // not sure this statement is needed. Maybe it should be fixed in getHoveredCell()
this.toolboxColumn.show(() => {
return {
left: `calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${column} - 1) * 2))`
};
});
}
}
if (!this.isRowMenuShowing) {
if (row > 0 && row <= this.numberOfRows) { // not sure this statement is needed. Maybe it should be fixed in getHoveredCell()
this.toolboxRow.show(() => {
const hoveredRowElement = this.getRow(row);
const { fromTopBorder } = $.getRelativeCoordsOfTwoElems(this.table, hoveredRowElement);
const { height } = hoveredRowElement.getBoundingClientRect();
return {
top: `${Math.ceil(fromTopBorder + height / 2)}px`
};
});
}
}
}
/**
* Makes the first row headings
*
* @param {boolean} withHeadings - use headings row or not
*/
setHeadingsSetting(withHeadings) {
this.tunes.withHeadings = withHeadings;
if (withHeadings) {
this.table.classList.add(CSS.withHeadings);
this.addHeadingAttrToFirstRow();
} else {
this.table.classList.remove(CSS.withHeadings);
this.removeHeadingAttrFromFirstRow();
}
}
/**
* Adds an attribute for displaying the placeholder in the cell
*/
addHeadingAttrToFirstRow() {
for (let cellIndex = 1; cellIndex <= this.numberOfColumns; cellIndex++) {
let cell = this.getCell(1, cellIndex);
if (cell) {
cell.setAttribute('heading', this.api.i18n.t('Heading'));
}
}
}
/**
* Removes an attribute for displaying the placeholder in the cell
*/
removeHeadingAttrFromFirstRow() {
for (let cellIndex = 1; cellIndex <= this.numberOfColumns; cellIndex++) {
let cell = this.getCell(1, cellIndex);
if (cell) {
cell.removeAttribute('heading');
}
}
}
/**
* Add effect of a selected row
*
* @param {number} index
*/
selectRow(index) {
const row = this.getRow(index);
if (row) {
this.selectedRow = index;
row.classList.add(CSS.rowSelected);
}
}
/**
* Remove effect of a selected row
*/
unselectRow() {
if (this.selectedRow <= 0) {
return;
}
const row = this.table.querySelector(`.${CSS.rowSelected}`);
if (row) {
row.classList.remove(CSS.rowSelected);
}
this.selectedRow = 0;
}
/**
* Add effect of a selected column
*
* @param {number} index
*/
selectColumn(index) {
for (let i = 1; i <= this.numberOfRows; i++) {
const cell = this.getCell(i, index);
if (cell) {
cell.classList.add(CSS.cellSelected);
}
}
this.selectedColumn = index;
}
/**
* Remove effect of a selected column
*/
unselectColumn() {
if (this.selectedColumn <= 0) {
return;
}
let cells = this.table.querySelectorAll(`.${CSS.cellSelected}`);
Array.from(cells).forEach(column => {
column.classList.remove(CSS.cellSelected);
});
this.selectedColumn = 0;
}
/**
* Calculates the row and column that the cursor is currently hovering over
* The search was optimized from O(n) to O (log n) via bin search to reduce the number of calculations
*
* @param {Event} event - mousemove event
* @returns hovered cell coordinates as an integer row and column
*/
getHoveredCell(event) {
let hoveredRow = this.hoveredRow;
let hoveredColumn = this.hoveredColumn;
const { width, height, x, y } = $.getCursorPositionRelativeToElement(this.table, event);
// Looking for hovered column
if (x >= 0) {
hoveredColumn = this.binSearch(
this.numberOfColumns,
(mid) => this.getCell(1, mid),
({ fromLeftBorder }) => x < fromLeftBorder,
({ fromRightBorder }) => x > (width - fromRightBorder)
);
}
// Looking for hovered row
if (y >= 0) {
hoveredRow = this.binSearch(
this.numberOfRows,
(mid) => this.getCell(mid, 1),
({ fromTopBorder }) => y < fromTopBorder,
({ fromBottomBorder }) => y > (height - fromBottomBorder)
);
}
return {
row: hoveredRow || this.hoveredRow,
column: hoveredColumn || this.hoveredColumn
};
}
/**
* Looks for the index of the cell the mouse is hovering over.
* Cells can be represented as ordered intervals with left and
* right (upper and lower for rows) borders inside the table, if the mouse enters it, then this is our index
*
* @param {number} numberOfCells - upper bound of binary search
* @param {function} getCell - function to take the currently viewed cell
* @param {function} beforeTheLeftBorder - determines the cursor position, to the left of the cell or not
* @param {function} afterTheRightBorder - determines the cursor position, to the right of the cell or not
* @returns {number}
*/
binSearch(numberOfCells, getCell, beforeTheLeftBorder, afterTheRightBorder) {
let leftBorder = 0;
let rightBorder = numberOfCells + 1;
let totalIterations = 0;
let mid;
while (leftBorder < rightBorder - 1 && totalIterations < 10) {
mid = Math.ceil((leftBorder + rightBorder) / 2);
const cell = getCell(mid);
const relativeCoords = $.getRelativeCoordsOfTwoElems(this.table, cell);
if (beforeTheLeftBorder(relativeCoords)) {
rightBorder = mid;
} else if (afterTheRightBorder(relativeCoords)) {
leftBorder = mid;
} else {
break;
}
totalIterations++;
}
return mid;
}
/**
* Collects data from cells into a two-dimensional array
*
* @returns {string[][]}
*/
getData() {
const data = [];
for (let i = 1; i <= this.numberOfRows; i++) {
const row = this.table.querySelector(`.${CSS.row}:nth-child(${i})`);
const cells = Array.from(row.querySelectorAll(`.${CSS.cell}`));
const isEmptyRow = cells.every(cell => !cell.textContent.trim());
if (isEmptyRow) {
continue;
}
data.push(cells.map(cell => cell.innerHTML));
}
return data;
}
/**
* Remove listeners on the document
*/
destroy() {
document.removeEventListener('click', this.documentClicked);
}
}
================================================
FILE: src/toolbox.js
================================================
import Popover from "./utils/popover";
import * as $ from "./utils/dom";
import { IconMenuSmall } from "@codexteam/icons";
/**
* @typedef {object} PopoverItem
* @property {string} label - button text
* @property {string} icon - button icon
* @property {boolean} confirmationRequired - if true, a confirmation state will be applied on the first click
* @property {function} hideIf - if provided, item will be hid, if this method returns true
* @property {function} onClick - click callback
*/
/**
* Toolbox is a menu for manipulation of rows/cols
*
* It contains toggler and Popover:
* <toolbox>
* <toolbox-toggler />
* <popover />
* <toolbox>
*/
export default class Toolbox {
/**
* Creates toolbox buttons and toolbox menus
*
* @param {Object} config
* @param {any} config.api - Editor.js api
* @param {PopoverItem[]} config.items - Editor.js api
* @param {function} config.onOpen - callback fired when the Popover is opening
* @param {function} config.onClose - callback fired when the Popover is closing
* @param {string} config.cssModifier - the modifier for the Toolbox. Allows to add some specific styles.
*/
constructor({ api, items, onOpen, onClose, cssModifier = "" }) {
this.api = api;
this.items = items;
this.onOpen = onOpen;
this.onClose = onClose;
this.cssModifier = cssModifier;
this.popover = null;
this.wrapper = this.createToolbox();
}
/**
* Style classes
*/
static get CSS() {
return {
toolbox: "tc-toolbox",
toolboxShowed: "tc-toolbox--showed",
toggler: "tc-toolbox__toggler",
};
}
/**
* Returns rendered Toolbox element
*/
get element() {
return this.wrapper;
}
/**
* Creating a toolbox to open menu for a manipulating columns
*
* @returns {Element}
*/
createToolbox() {
const wrapper = $.make("div", [
Toolbox.CSS.toolbox,
this.cssModifier ? `${Toolbox.CSS.toolbox}--${this.cssModifier}` : "",
]);
wrapper.dataset.mutationFree = "true";
const popover = this.createPopover();
const toggler = this.createToggler();
wrapper.appendChild(toggler);
wrapper.appendChild(popover);
return wrapper;
}
/**
* Creates the Toggler
*
* @returns {Element}
*/
createToggler() {
const toggler = $.make("div", Toolbox.CSS.toggler, {
innerHTML: IconMenuSmall,
});
toggler.addEventListener("click", () => {
this.togglerClicked();
});
return toggler;
}
/**
* Creates the Popover instance and render it
*
* @returns {Element}
*/
createPopover() {
this.popover = new Popover({
items: this.items,
});
return this.popover.render();
}
/**
* Toggler click handler. Opens/Closes the popover
*
* @returns {void}
*/
togglerClicked() {
if (this.popover.opened) {
this.popover.close();
this.onClose();
} else {
this.popover.open();
this.onOpen();
}
}
/**
* Shows the Toolbox
*
* @param {function} computePositionMethod - method that returns the position coordinate
* @returns {void}
*/
show(computePositionMethod) {
const position = computePositionMethod();
/**
* Set 'top' or 'left' style
*/
Object.entries(position).forEach(([prop, value]) => {
this.wrapper.style[prop] = value;
});
this.wrapper.classList.add(Toolbox.CSS.toolboxShowed);
}
/**
* Hides the Toolbox
*
* @returns {void}
*/
hide() {
this.popover.close();
this.wrapper.classList.remove(Toolbox.CSS.toolboxShowed);
}
}
================================================
FILE: src/utils/dom.js
================================================
/**
* Helper for making Elements with attributes
*
* @param {string} tagName - new Element tag name
* @param {string|string[]} classNames - list or name of CSS classname(s)
* @param {object} attributes - any attributes
* @returns {Element}
*/
export function make(
tagName,
classNames,
attributes = {}
) {
const el = document.createElement(tagName);
if (Array.isArray(classNames)) {
el.classList.add(...classNames);
} else if (classNames) {
el.classList.add(classNames);
}
for (const attrName in attributes) {
if (!Object.prototype.hasOwnProperty.call(attributes, attrName)) {
continue;
}
el[attrName] = attributes[attrName];
}
return el;
}
/**
* Get item position relative to document
*
* @param {HTMLElement} elem - item
* @returns {{x1: number, y1: number, x2: number, y2: number}} coordinates of the upper left (x1,y1) and lower right(x2,y2) corners
*/
export function getCoords(elem) {
const rect = elem.getBoundingClientRect();
return {
y1: Math.floor(rect.top + window.pageYOffset),
x1: Math.floor(rect.left + window.pageXOffset),
x2: Math.floor(rect.right + window.pageXOffset),
y2: Math.floor(rect.bottom + window.pageYOffset)
};
}
/**
* Calculate paddings of the first element relative to the second
*
* @param {HTMLElement} firstElem - outer element, if the second element is inside it, then all padding will be positive
* @param {HTMLElement} secondElem - inner element, if its borders go beyond the first, then the paddings will be considered negative
* @returns {{fromTopBorder: number, fromLeftBorder: number, fromRightBorder: number, fromBottomBorder: number}}
*/
export function getRelativeCoordsOfTwoElems(firstElem, secondElem) {
const firstCoords = getCoords(firstElem);
const secondCoords = getCoords(secondElem);
return {
fromTopBorder: secondCoords.y1 - firstCoords.y1,
fromLeftBorder: secondCoords.x1 - firstCoords.x1,
fromRightBorder: firstCoords.x2 - secondCoords.x2,
fromBottomBorder: firstCoords.y2 - secondCoords.y2
};
}
/**
* Get the width and height of an element and the position of the cursor relative to it
*
* @param {HTMLElement} elem - element relative to which the coordinates will be calculated
* @param {Event} event - mouse event
*/
export function getCursorPositionRelativeToElement(elem, event) {
const rect = elem.getBoundingClientRect();
const { width, height, x, y } = rect;
const { clientX, clientY } = event;
return {
width,
height,
x: clientX - x,
y: clientY - y
};
}
/**
* Insert element after the referenced
*
* @param {HTMLElement} newNode
* @param {HTMLElement} referenceNode
* @returns {HTMLElement}
*/
export function insertAfter(newNode, referenceNode) {
return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
/**
* Insert element after the referenced
*
* @param {HTMLElement} newNode
* @param {HTMLElement} referenceNode
* @returns {HTMLElement}
*/
export function insertBefore(newNode, referenceNode) {
return referenceNode.parentNode.insertBefore(newNode, referenceNode);
}
/**
* Set focus to contenteditable or native input element
*
* @param {Element} element - element where to set focus
* @param {boolean} atStart - where to set focus: at the start or at the end
*
* @returns {void}
*/
export function focus(element, atStart = true) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(atStart);
selection.removeAllRanges();
selection.addRange(range);
}
================================================
FILE: src/utils/popover.js
================================================
import * as $ from './dom';
/**
* @typedef {object} PopoverItem
* @property {string} label - button text
* @property {string} icon - button icon
* @property {boolean} confirmationRequired - if true, a confirmation state will be applied on the first click
* @property {function} hideIf - if provided, item will be hid, if this method returns true
* @property {function} onClick - click callback
*/
/**
* This cass provides a popover rendering
*/
export default class Popover {
/**
* @param {object} options - constructor options
* @param {PopoverItem[]} options.items - constructor options
*/
constructor({items}) {
this.items = items;
this.wrapper = undefined;
this.itemEls = [];
}
/**
* Set of CSS classnames used in popover
*
* @returns {object}
*/
static get CSS() {
return {
popover: 'tc-popover',
popoverOpened: 'tc-popover--opened',
item: 'tc-popover__item',
itemHidden: 'tc-popover__item--hidden',
itemConfirmState: 'tc-popover__item--confirm',
itemIcon: 'tc-popover__item-icon',
itemLabel: 'tc-popover__item-label'
};
}
/**
* Returns the popover element
*
* @returns {Element}
*/
render() {
this.wrapper = $.make('div', Popover.CSS.popover);
this.items.forEach((item, index) => {
const itemEl = $.make('div', Popover.CSS.item);
const icon = $.make('div', Popover.CSS.itemIcon, {
innerHTML: item.icon
});
const label = $.make('div', Popover.CSS.itemLabel, {
textContent: item.label
});
itemEl.dataset.index = index;
itemEl.appendChild(icon);
itemEl.appendChild(label);
this.wrapper.appendChild(itemEl);
this.itemEls.push(itemEl);
});
/**
* Delegate click
*/
this.wrapper.addEventListener('click', (event) => {
this.popoverClicked(event);
});
return this.wrapper;
}
/**
* Popover wrapper click listener
* Used to delegate clicks in items
*
* @returns {void}
*/
popoverClicked(event) {
const clickedItem = event.target.closest(`.${Popover.CSS.item}`);
/**
* Clicks outside or between item
*/
if (!clickedItem) {
return;
}
const clickedItemIndex = clickedItem.dataset.index;
const item = this.items[clickedItemIndex];
if (item.confirmationRequired && !this.hasConfirmationState(clickedItem)) {
this.setConfirmationState(clickedItem);
return;
}
item.onClick();
}
/**
* Enable the confirmation state on passed item
*
* @returns {void}
*/
setConfirmationState(itemEl) {
itemEl.classList.add(Popover.CSS.itemConfirmState);
}
/**
* Disable the confirmation state on passed item
*
* @returns {void}
*/
clearConfirmationState(itemEl) {
itemEl.classList.remove(Popover.CSS.itemConfirmState);
}
/**
* Check if passed item has the confirmation state
*
* @returns {boolean}
*/
hasConfirmationState(itemEl) {
return itemEl.classList.contains(Popover.CSS.itemConfirmState);
}
/**
* Return an opening state
*
* @returns {boolean}
*/
get opened() {
return this.wrapper.classList.contains(Popover.CSS.popoverOpened);
}
/**
* Opens the popover
*
* @returns {void}
*/
open() {
/**
* If item provides 'hideIf()' method that returns true, hide item
*/
this.items.forEach((item, index) => {
if (typeof item.hideIf === 'function') {
this.itemEls[index].classList.toggle(Popover.CSS.itemHidden, item.hideIf());
}
});
this.wrapper.classList.add(Popover.CSS.popoverOpened);
}
/**
* Closes the popover
*
* @returns {void}
*/
close() {
this.wrapper.classList.remove(Popover.CSS.popoverOpened);
this.itemEls.forEach(el => {
this.clearConfirmationState(el);
});
}
}
================================================
FILE: src/utils/throttled.js
================================================
/**
* Limits the frequency of calling a function
*
* @param {number} delay - delay between calls in milliseconds
* @param {function} fn - function to be throttled
*/
export default function throttled(delay, fn) {
let lastCall = 0;
return function (...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return fn(...args);
};
}
================================================
FILE: tsconfig.json
================================================
{
"include": ["src/**/*"],
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist",
}
}
================================================
FILE: vite.config.js
================================================
import path from "path";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import * as pkg from "./package.json";
import dts from 'vite-plugin-dts';
const NODE_ENV = process.argv.mode || "development";
const VERSION = pkg.version;
export default {
build: {
copyPublicDir: false,
lib: {
entry: path.resolve(__dirname, "src", "index.js"),
name: "Table",
fileName: "table",
},
},
define: {
NODE_ENV: JSON.stringify(NODE_ENV),
VERSION: JSON.stringify(VERSION),
},
server: {
open: true,
watch: {
usePolling: true,
},
},
plugins: [
cssInjectedByJsPlugin({ useStrictCSP: true }),
dts({ tsconfigPath: './tsconfig.json' })
],
};
gitextract_psbywiq4/ ├── .eslintignore ├── .eslintrc ├── .github/ │ └── workflows/ │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── src/ │ ├── index.js │ ├── plugin.js │ ├── styles/ │ │ ├── index.pcss │ │ ├── popover.pcss │ │ ├── settings.pcss │ │ ├── table.pcss │ │ └── toolboxes.pcss │ ├── table.js │ ├── toolbox.js │ └── utils/ │ ├── dom.js │ ├── popover.js │ └── throttled.js ├── tsconfig.json └── vite.config.js
SYMBOL INDEX (91 symbols across 7 files)
FILE: src/plugin.js
class TableBlock (line 37) | class TableBlock {
method isReadOnlySupported (line 43) | static get isReadOnlySupported() {
method enableLineBreaks (line 53) | static get enableLineBreaks() {
method constructor (line 62) | constructor({data, config, api, readOnly, block}) {
method toolbox (line 82) | static get toolbox() {
method render (line 94) | render() {
method renderSettings (line 112) | renderSettings() {
method save (line 151) | save() {
method destroy (line 168) | destroy() {
method getConfig (line 180) | getConfig(configName, defaultValue = undefined, savedData = undefined) {
method pasteConfig (line 195) | static get pasteConfig() {
method onPaste (line 204) | onPaste(event) {
FILE: src/table.js
constant CSS (line 14) | const CSS = {
class Table (line 46) | class Table {
method constructor (line 56) | constructor(readOnly, api, data, config) {
method getWrapper (line 152) | getWrapper() {
method bindEvents (line 159) | bindEvents() {
method createColumnToolbox (line 181) | createColumnToolbox() {
method createRowToolbox (line 236) | createRowToolbox() {
method moveCursorToNextRow (line 290) | moveCursorToNextRow() {
method getCell (line 309) | getCell(row, column) {
method getRow (line 319) | getRow(row) {
method getRowByCell (line 329) | getRowByCell(cell) {
method getRowFirstCell (line 339) | getRowFirstCell(row) {
method setCellContent (line 350) | setCellContent(row, column, content) {
method addColumn (line 363) | addColumn(columnIndex = -1, setFocus = false) {
method addRow (line 414) | addRow(index = -1, setFocus = false) {
method deleteColumn (line 468) | deleteColumn(index) {
method deleteRow (line 489) | deleteRow(index) {
method createTableWrapper (line 505) | createTableWrapper() {
method computeInitialSize (line 535) | computeInitialSize() {
method resize (line 565) | resize() {
method fill (line 582) | fill() {
method fillRow (line 600) | fillRow(row, numberOfColumns) {
method createCell (line 613) | createCell() {
method numberOfRows (line 622) | get numberOfRows() {
method numberOfColumns (line 629) | get numberOfColumns() {
method isColumnMenuShowing (line 642) | get isColumnMenuShowing() {
method isRowMenuShowing (line 651) | get isRowMenuShowing() {
method onMouseMoveInTable (line 660) | onMouseMoveInTable(event) {
method onKeyPressListener (line 675) | onKeyPressListener(event) {
method onKeyDownListener (line 693) | onKeyDownListener(event) {
method focusInTableListener (line 704) | focusInTableListener(event) {
method hideToolboxes (line 721) | hideToolboxes() {
method hideRowToolbox (line 732) | hideRowToolbox() {
method hideColumnToolbox (line 741) | hideColumnToolbox() {
method focusCell (line 752) | focusCell() {
method focusedCellElem (line 761) | get focusedCellElem() {
method updateToolboxesPosition (line 773) | updateToolboxesPosition(row = this.hoveredRow, column = this.hoveredCo...
method setHeadingsSetting (line 804) | setHeadingsSetting(withHeadings) {
method addHeadingAttrToFirstRow (line 819) | addHeadingAttrToFirstRow() {
method removeHeadingAttrFromFirstRow (line 832) | removeHeadingAttrFromFirstRow() {
method selectRow (line 847) | selectRow(index) {
method unselectRow (line 859) | unselectRow() {
method selectColumn (line 878) | selectColumn(index) {
method unselectColumn (line 893) | unselectColumn() {
method getHoveredCell (line 914) | getHoveredCell(event) {
method binSearch (line 956) | binSearch(numberOfCells, getCell, beforeTheLeftBorder, afterTheRightBo...
method getData (line 987) | getData() {
method destroy (line 1008) | destroy() {
FILE: src/toolbox.js
class Toolbox (line 23) | class Toolbox {
method constructor (line 34) | constructor({ api, items, onOpen, onClose, cssModifier = "" }) {
method CSS (line 49) | static get CSS() {
method element (line 60) | get element() {
method createToolbox (line 69) | createToolbox() {
method createToggler (line 90) | createToggler() {
method createPopover (line 107) | createPopover() {
method togglerClicked (line 120) | togglerClicked() {
method show (line 136) | show(computePositionMethod) {
method hide (line 154) | hide() {
FILE: src/utils/dom.js
function make (line 9) | function make(
function getCoords (line 39) | function getCoords(elem) {
function getRelativeCoordsOfTwoElems (line 57) | function getRelativeCoordsOfTwoElems(firstElem, secondElem) {
function getCursorPositionRelativeToElement (line 75) | function getCursorPositionRelativeToElement(elem, event) {
function insertAfter (line 95) | function insertAfter(newNode, referenceNode) {
function insertBefore (line 106) | function insertBefore(newNode, referenceNode) {
function focus (line 119) | function focus(element, atStart = true) {
FILE: src/utils/popover.js
class Popover (line 15) | class Popover {
method constructor (line 20) | constructor({items}) {
method CSS (line 31) | static get CSS() {
method render (line 48) | render() {
method popoverClicked (line 85) | popoverClicked(event) {
method setConfirmationState (line 112) | setConfirmationState(itemEl) {
method clearConfirmationState (line 121) | clearConfirmationState(itemEl) {
method hasConfirmationState (line 130) | hasConfirmationState(itemEl) {
method opened (line 139) | get opened() {
method open (line 148) | open() {
method close (line 166) | close() {
FILE: src/utils/throttled.js
function throttled (line 8) | function throttled(delay, fn) {
FILE: vite.config.js
constant NODE_ENV (line 6) | const NODE_ENV = process.argv.mode || "development";
constant VERSION (line 7) | const VERSION = pkg.version;
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (65K chars).
[
{
"path": ".eslintignore",
"chars": 26,
"preview": "dist\nnode_modules\n.github\n"
},
{
"path": ".eslintrc",
"chars": 91,
"preview": "{\n \"extends\": [\n \"codex\"\n ],\n \"rules\": {\n \"jsdoc/no-undefined-types\": \"off\"\n }\n}\n"
},
{
"path": ".github/workflows/npm-publish.yml",
"chars": 350,
"preview": "name: Publish package to NPM\n\non:\n push:\n branches:\n - master\n\njobs:\n publish-and-notify:\n uses: codex-team"
},
{
"path": ".gitignore",
"chars": 24,
"preview": ".idea\nnode_modules\ndist\n"
},
{
"path": ".npmignore",
"chars": 83,
"preview": ".idea/\nassets/\nsrc/\n.eslintrc\npostcss.config.js\nvite.config.js\ntest.html\nyarn.lock\n"
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2022 CodeX\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.md",
"chars": 3422,
"preview": "# Table tool\n\nThe Table Block for the [Editor.js](https://editorjs.io). Finally improved.\n\n,\n require('autoprefixer'),\n require('cssnano'),\n "
},
{
"path": "src/index.js",
"chars": 85,
"preview": "import Plugin from './plugin';\nimport './styles/index.pcss';\n\nexport default Plugin;\n"
},
{
"path": "src/plugin.js",
"chars": 6162,
"preview": "import Table from './table';\nimport * as $ from './utils/dom';\n\nimport { IconTable, IconTableWithHeadings, IconTableWith"
},
{
"path": "src/styles/index.pcss",
"chars": 105,
"preview": "@import './table.pcss';\n@import './toolboxes.pcss';\n@import './settings.pcss';\n@import './popover.pcss';\n"
},
{
"path": "src/styles/popover.pcss",
"chars": 1939,
"preview": ".tc-popover {\n --color-border: #eaeaea;\n --color-background: #fff;\n --color-background-hover: rgba(232,232,235,0.49);"
},
{
"path": "src/styles/settings.pcss",
"chars": 77,
"preview": ".tc-settings {\n .cdx-settings-button {\n width: 50%;\n margin: 0;\n }\n}\n"
},
{
"path": "src/styles/table.pcss",
"chars": 3933,
"preview": "/* tc- project's prefix*/\n.tc-wrap {\n --color-background: #f9f9fb;\n --color-text-secondary: #7b7e89;\n --color-border:"
},
{
"path": "src/styles/toolboxes.pcss",
"chars": 1287,
"preview": ".tc-toolbox {\n --toolbox-padding: 6px;\n --popover-margin: 30px;\n --toggler-click-zone-size: 30px;\n --toggler-dots-co"
},
{
"path": "src/table.js",
"chars": 26334,
"preview": "import Toolbox from './toolbox';\nimport * as $ from './utils/dom';\nimport throttled from './utils/throttled';\n\nimport {\n"
},
{
"path": "src/toolbox.js",
"chars": 3618,
"preview": "import Popover from \"./utils/popover\";\nimport * as $ from \"./utils/dom\";\nimport { IconMenuSmall } from \"@codexteam/icons"
},
{
"path": "src/utils/dom.js",
"chars": 3637,
"preview": "/**\n * Helper for making Elements with attributes\n *\n * @param {string} tagName - new Element tag name\n * @pa"
},
{
"path": "src/utils/popover.js",
"chars": 3879,
"preview": "import * as $ from './dom';\n\n/**\n * @typedef {object} PopoverItem\n * @property {string} label - button text\n * @property"
},
{
"path": "src/utils/throttled.js",
"chars": 416,
"preview": "\n/**\n * Limits the frequency of calling a function\n *\n * @param {number} delay - delay between calls in milliseconds\n * "
},
{
"path": "tsconfig.json",
"chars": 159,
"preview": "{\n \"include\": [\"src/**/*\"],\n \"compilerOptions\": {\n \"allowJs\": true,\n \"declaration\": true,\n \"emitDeclarationOn"
},
{
"path": "vite.config.js",
"chars": 722,
"preview": "import path from \"path\";\nimport cssInjectedByJsPlugin from \"vite-plugin-css-injected-by-js\";\nimport * as pkg from \"./pac"
}
]
About this extraction
This page contains the full source code of the editor-js/table GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (59.2 KB), approximately 16.3k tokens, and a symbol index with 91 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.