@editorjs/table
Version:
Table for Editor.js
1,014 lines (1,013 loc) • 40.3 kB
JavaScript
(function(){var r;"use strict";try{if(typeof document<"u"){var o=document.createElement("style");o.nonce=(r=document.head.querySelector("meta[property=csp-nonce]"))==null?void 0:r.content,o.appendChild(document.createTextNode('.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) + var(--toolbox-padding)*2);border-left:0;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);z-index:0}.tc-wrap--readonly{grid-template-columns:100% var(--cell-size)}.tc-wrap svg{vertical-align:top}@media print{.tc-wrap{border-left-color:var(--color-border);border-left-style:solid;border-left-width:1px;grid-template-columns:100% var(--cell-size)}}@media print{.tc-wrap .tc-row:after{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}.tc-table:after{width:calc(var(--cell-size));height:100%;left:calc(var(--cell-size)*-1);top:0}.tc-table:after,.tc-table:before{position:absolute;content:""}.tc-table:before{width:100%;height:var(--toolbox-aiming-field-size);top:calc(var(--toolbox-aiming-field-size)*-1);left:0}.tc-table--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)}.tc-table--heading .tc-row:first-child [contenteditable]:empty:before{content:attr(heading);color:var(--color-text-secondary)}.tc-table--heading .tc-row:first-child:after{bottom:-2px;border-bottom:2px solid var(--color-border)}.tc-add-column,.tc-add-row{display:flex;color:var(--color-text-secondary)}@media print{.tc-add{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}.tc-add-column svg{padding:5px;position:sticky;top:0;background-color:var(--color-background)}.tc-add-column--disabled{visibility:hidden}@media print{.tc-add-column{display:none}}.tc-add-row{height:var(--cell-size);align-items:center;padding-left:4px;position:relative}.tc-add-row--disabled{display:none}.tc-add-row:before{content:"";position:absolute;right:calc(var(--cell-size)*-1);width:var(--cell-size);height:100%}@media print{.tc-add-row{display:none}}.tc-add-column,.tc-add-row{transition:0s;cursor:pointer;will-change:background-color}.tc-add-column:hover,.tc-add-row:hover{transition:background-color .1s ease;background-color:var(--color-background)}.tc-add-row{margin-top:1px}.tc-add-row:hover:before{transition:.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)}.tc-row:after{content:"";pointer-events:none;position:absolute;width:var(--cell-size);height:100%;bottom:-1px;right:calc(var(--cell-size)*-1);border-bottom:1px solid var(--color-border)}.tc-row--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;overflow:hidden;outline:none;line-break:normal}.tc-cell--selected{background:var(--color-background)}.tc-wrap--readonly .tc-row:after{display:none}.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 .1s;will-change:left,opacity}.tc-toolbox--column{top:calc(var(--toggler-click-zone-size)*-1);transform:translate(calc(var(--toggler-click-zone-size)*-1/2));will-change:left,opacity}.tc-toolbox--row{left:calc(var(--popover-margin)*-1);transform:translateY(calc(var(--toggler-click-zone-size)*-1/2));margin-top:-1px;will-change:top,opacity}.tc-toolbox--showed{opacity:1}.tc-toolbox .tc-popover{position:absolute;top:0;left:var(--popover-margin)}.tc-toolbox__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 .15s ease;will-change:opacity}.tc-toolbox__toggler:hover{color:var(--toggler-dots-color-hovered)}.tc-toolbox__toggler svg{fill:currentColor}.tc-wrap:hover .tc-toolbox__toggler{opacity:1}.tc-settings .cdx-settings-button{width:50%;margin:0}.tc-popover{--color-border:#eaeaea;--color-background:#fff;--color-background-hover:rgba(232,232,235,.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 #0d142121;border-radius:6px;padding:6px;display:none;will-change:opacity,transform}.tc-popover--opened{display:block;animation:menuShowing .1s cubic-bezier(.215,.61,.355,1) forwards}.tc-popover__item{display:flex;align-items:center;padding:2px 14px 2px 2px;border-radius:5px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tc-popover__item:hover{background:var(--color-background-hover)}.tc-popover__item:not(:last-of-type){margin-bottom:2px}.tc-popover__item-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}.tc-popover__item-label{line-height:22px;font-size:14px;font-weight:500}.tc-popover__item--confirm{background:var(--color-background-confirm);color:var(--color-text-confirm)}.tc-popover__item--confirm:hover{background-color:var(--color-background-confirm-hover)}.tc-popover__item--confirm .tc-popover__item-icon{background:var(--color-background-confirm);border-color:#0000001a}.tc-popover__item--confirm .tc-popover__item-icon svg{transition:transform .2s ease-in;transform:rotate(90deg) scale(1.2)}.tc-popover__item--hidden{display:none}@keyframes menuShowing{0%{opacity:0;transform:translateY(-8px) scale(.9)}70%{opacity:1;transform:translateY(2px)}to{transform:translateY(0)}}')),document.head.appendChild(o)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
function c(d, t, e = {}) {
const o = document.createElement(d);
Array.isArray(t) ? o.classList.add(...t) : t && o.classList.add(t);
for (const i in e)
Object.prototype.hasOwnProperty.call(e, i) && (o[i] = e[i]);
return o;
}
function f(d) {
const t = d.getBoundingClientRect();
return {
y1: Math.floor(t.top + window.pageYOffset),
x1: Math.floor(t.left + window.pageXOffset),
x2: Math.floor(t.right + window.pageXOffset),
y2: Math.floor(t.bottom + window.pageYOffset)
};
}
function g(d, t) {
const e = f(d), o = f(t);
return {
fromTopBorder: o.y1 - e.y1,
fromLeftBorder: o.x1 - e.x1,
fromRightBorder: e.x2 - o.x2,
fromBottomBorder: e.y2 - o.y2
};
}
function k(d, t) {
const e = d.getBoundingClientRect(), { width: o, height: i, x: n, y: r } = e, { clientX: h, clientY: l } = t;
return {
width: o,
height: i,
x: h - n,
y: l - r
};
}
function m(d, t) {
return t.parentNode.insertBefore(d, t);
}
function C(d, t = !0) {
const e = document.createRange(), o = window.getSelection();
e.selectNodeContents(d), e.collapse(t), o.removeAllRanges(), o.addRange(e);
}
class a {
/**
* @param {object} options - constructor options
* @param {PopoverItem[]} options.items - constructor options
*/
constructor({ items: t }) {
this.items = t, this.wrapper = void 0, 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() {
return this.wrapper = c("div", a.CSS.popover), this.items.forEach((t, e) => {
const o = c("div", a.CSS.item), i = c("div", a.CSS.itemIcon, {
innerHTML: t.icon
}), n = c("div", a.CSS.itemLabel, {
textContent: t.label
});
o.dataset.index = e, o.appendChild(i), o.appendChild(n), this.wrapper.appendChild(o), this.itemEls.push(o);
}), this.wrapper.addEventListener("click", (t) => {
this.popoverClicked(t);
}), this.wrapper;
}
/**
* Popover wrapper click listener
* Used to delegate clicks in items
*
* @returns {void}
*/
popoverClicked(t) {
const e = t.target.closest(`.${a.CSS.item}`);
if (!e)
return;
const o = e.dataset.index, i = this.items[o];
if (i.confirmationRequired && !this.hasConfirmationState(e)) {
this.setConfirmationState(e);
return;
}
i.onClick();
}
/**
* Enable the confirmation state on passed item
*
* @returns {void}
*/
setConfirmationState(t) {
t.classList.add(a.CSS.itemConfirmState);
}
/**
* Disable the confirmation state on passed item
*
* @returns {void}
*/
clearConfirmationState(t) {
t.classList.remove(a.CSS.itemConfirmState);
}
/**
* Check if passed item has the confirmation state
*
* @returns {boolean}
*/
hasConfirmationState(t) {
return t.classList.contains(a.CSS.itemConfirmState);
}
/**
* Return an opening state
*
* @returns {boolean}
*/
get opened() {
return this.wrapper.classList.contains(a.CSS.popoverOpened);
}
/**
* Opens the popover
*
* @returns {void}
*/
open() {
this.items.forEach((t, e) => {
typeof t.hideIf == "function" && this.itemEls[e].classList.toggle(a.CSS.itemHidden, t.hideIf());
}), this.wrapper.classList.add(a.CSS.popoverOpened);
}
/**
* Closes the popover
*
* @returns {void}
*/
close() {
this.wrapper.classList.remove(a.CSS.popoverOpened), this.itemEls.forEach((t) => {
this.clearConfirmationState(t);
});
}
}
const R = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 9L10 12M10 12L7 15M10 12H4"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9L14 12M14 12L17 15M14 12H20"/></svg>', b = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M8 8L12 12M12 12L16 16M12 12L16 8M12 12L8 16"/></svg>', x = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.8833 9.16666L18.2167 12.5M18.2167 12.5L14.8833 15.8333M18.2167 12.5H10.05C9.16594 12.5 8.31809 12.1488 7.69297 11.5237C7.06785 10.8986 6.71666 10.0507 6.71666 9.16666"/></svg>', S = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.9167 14.9167L11.5833 18.25M11.5833 18.25L8.25 14.9167M11.5833 18.25L11.5833 10.0833C11.5833 9.19928 11.9345 8.35143 12.5596 7.72631C13.1848 7.10119 14.0326 6.75 14.9167 6.75"/></svg>', y = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.13333 14.9167L12.4667 18.25M12.4667 18.25L15.8 14.9167M12.4667 18.25L12.4667 10.0833C12.4667 9.19928 12.1155 8.35143 11.4904 7.72631C10.8652 7.10119 10.0174 6.75 9.13333 6.75"/></svg>', L = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.8833 15.8333L18.2167 12.5M18.2167 12.5L14.8833 9.16667M18.2167 12.5L10.05 12.5C9.16595 12.5 8.31811 12.8512 7.69299 13.4763C7.06787 14.1014 6.71667 14.9493 6.71667 15.8333"/></svg>', M = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M9.41 9.66H9.4"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M14.6 9.66H14.59"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M9.31 14.36H9.3"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M14.6 14.36H14.59"/></svg>', v = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 7V12M12 17V12M17 12H12M12 12H7"/></svg>', O = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9L20 12L17 15"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 12H20"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 9L4 12L7 15"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12H10"/></svg>', T = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M5 10H19"/><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/></svg>', H = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M10 5V18.5"/><path stroke="currentColor" stroke-width="2" d="M14 5V18.5"/><path stroke="currentColor" stroke-width="2" d="M5 10H19"/><path stroke="currentColor" stroke-width="2" d="M5 14H19"/><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/></svg>', A = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M10 5V18.5"/><path stroke="currentColor" stroke-width="2" d="M5 10H19"/><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/></svg>';
class w {
/**
* 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: t, items: e, onOpen: o, onClose: i, cssModifier: n = "" }) {
this.api = t, this.items = e, this.onOpen = o, this.onClose = i, this.cssModifier = n, 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 t = c("div", [
w.CSS.toolbox,
this.cssModifier ? `${w.CSS.toolbox}--${this.cssModifier}` : ""
]);
t.dataset.mutationFree = "true";
const e = this.createPopover(), o = this.createToggler();
return t.appendChild(o), t.appendChild(e), t;
}
/**
* Creates the Toggler
*
* @returns {Element}
*/
createToggler() {
const t = c("div", w.CSS.toggler, {
innerHTML: M
});
return t.addEventListener("click", () => {
this.togglerClicked();
}), t;
}
/**
* Creates the Popover instance and render it
*
* @returns {Element}
*/
createPopover() {
return this.popover = new a({
items: this.items
}), this.popover.render();
}
/**
* Toggler click handler. Opens/Closes the popover
*
* @returns {void}
*/
togglerClicked() {
this.popover.opened ? (this.popover.close(), this.onClose()) : (this.popover.open(), this.onOpen());
}
/**
* Shows the Toolbox
*
* @param {function} computePositionMethod - method that returns the position coordinate
* @returns {void}
*/
show(t) {
const e = t();
Object.entries(e).forEach(([o, i]) => {
this.wrapper.style[o] = i;
}), this.wrapper.classList.add(w.CSS.toolboxShowed);
}
/**
* Hides the Toolbox
*
* @returns {void}
*/
hide() {
this.popover.close(), this.wrapper.classList.remove(w.CSS.toolboxShowed);
}
}
function B(d, t) {
let e = 0;
return function(...o) {
const i = (/* @__PURE__ */ new Date()).getTime();
if (!(i - e < d))
return e = i, t(...o);
};
}
const s = {
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"
};
class E {
/**
* 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(t, e, o, i) {
this.readOnly = t, this.api = e, this.data = o, this.config = i, this.wrapper = null, this.table = null, this.toolboxColumn = this.createColumnToolbox(), this.toolboxRow = this.createRowToolbox(), this.createTableWrapper(), this.hoveredRow = 0, this.hoveredColumn = 0, this.selectedRow = 0, this.selectedColumn = 0, this.tunes = {
withHeadings: !1
}, this.resize(), this.fill(), this.focusedCell = {
row: 0,
column: 0
}, this.documentClicked = (n) => {
const r = n.target.closest(`.${s.table}`) !== null, h = n.target.closest(`.${s.wrapper}`) === null;
(r || h) && this.hideToolboxes();
const u = n.target.closest(`.${s.addRow}`), p = n.target.closest(`.${s.addColumn}`);
u && u.parentNode === this.wrapper ? (this.addRow(void 0, !0), this.hideToolboxes()) : p && p.parentNode === this.wrapper && (this.addColumn(void 0, !0), this.hideToolboxes());
}, this.readOnly || this.bindEvents();
}
/**
* Returns the rendered table wrapper
*
* @returns {Element}
*/
getWrapper() {
return this.wrapper;
}
/**
* Hangs the necessary handlers to events
*/
bindEvents() {
document.addEventListener("click", this.documentClicked), this.table.addEventListener("mousemove", B(150, (t) => this.onMouseMoveInTable(t)), { passive: !0 }), this.table.onkeypress = (t) => this.onKeyPressListener(t), this.table.addEventListener("keydown", (t) => this.onKeyDownListener(t)), this.table.addEventListener("focusin", (t) => this.focusInTableListener(t));
}
/**
* Configures and creates the toolbox for manipulating with columns
*
* @returns {Toolbox}
*/
createColumnToolbox() {
return new w({
api: this.api,
cssModifier: "column",
items: [
{
label: this.api.i18n.t("Add column to left"),
icon: S,
hideIf: () => this.numberOfColumns === this.config.maxcols,
onClick: () => {
this.addColumn(this.selectedColumn, !0), this.hideToolboxes();
}
},
{
label: this.api.i18n.t("Add column to right"),
icon: y,
hideIf: () => this.numberOfColumns === this.config.maxcols,
onClick: () => {
this.addColumn(this.selectedColumn + 1, !0), this.hideToolboxes();
}
},
{
label: this.api.i18n.t("Delete column"),
icon: b,
hideIf: () => this.numberOfColumns === 1,
confirmationRequired: !0,
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 w({
api: this.api,
cssModifier: "row",
items: [
{
label: this.api.i18n.t("Add row above"),
icon: L,
hideIf: () => this.numberOfRows === this.config.maxrows,
onClick: () => {
this.addRow(this.selectedRow, !0), this.hideToolboxes();
}
},
{
label: this.api.i18n.t("Add row below"),
icon: x,
hideIf: () => this.numberOfRows === this.config.maxrows,
onClick: () => {
this.addRow(this.selectedRow + 1, !0), this.hideToolboxes();
}
},
{
label: this.api.i18n.t("Delete row"),
icon: b,
hideIf: () => this.numberOfRows === 1,
confirmationRequired: !0,
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() {
this.focusedCell.row !== this.numberOfRows ? (this.focusedCell.row += 1, this.focusCell(this.focusedCell)) : (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(t, e) {
return this.table.querySelectorAll(`.${s.row}:nth-child(${t}) .${s.cell}`)[e - 1];
}
/**
* Get table row by index
*
* @param {number} row - row coordinate
* @returns {HTMLElement}
*/
getRow(t) {
return this.table.querySelector(`.${s.row}:nth-child(${t})`);
}
/**
* The parent of the cell which is the row
*
* @param {HTMLElement} cell - cell element
* @returns {HTMLElement}
*/
getRowByCell(t) {
return t.parentElement;
}
/**
* Ger row's first cell
*
* @param {Element} row - row to find its first cell
* @returns {Element}
*/
getRowFirstCell(t) {
return t.querySelector(`.${s.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(t, e, o) {
const i = this.getCell(t, e);
i.innerHTML = o;
}
/**
* 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(t = -1, e = !1) {
var n;
let o = this.numberOfColumns;
if (this.config && this.config.maxcols && this.numberOfColumns >= this.config.maxcols)
return;
for (let r = 1; r <= this.numberOfRows; r++) {
let h;
const l = this.createCell();
if (t > 0 && t <= o ? (h = this.getCell(r, t), m(l, h)) : h = this.getRow(r).appendChild(l), r === 1) {
const u = this.getCell(r, t > 0 ? t : o + 1);
u && e && C(u);
}
}
const i = this.wrapper.querySelector(`.${s.addColumn}`);
(n = this.config) != null && n.maxcols && this.numberOfColumns > this.config.maxcols - 1 && i && i.classList.add(s.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(t = -1, e = !1) {
let o, i = c("div", s.row);
this.tunes.withHeadings && this.removeHeadingAttrFromFirstRow();
let n = this.numberOfColumns;
if (this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && h)
return;
if (t > 0 && t <= this.numberOfRows) {
let l = this.getRow(t);
o = m(i, l);
} else
o = this.table.appendChild(i);
this.fillRow(o, n), this.tunes.withHeadings && this.addHeadingAttrToFirstRow();
const r = this.getRowFirstCell(o);
r && e && C(r);
const h = this.wrapper.querySelector(`.${s.addRow}`);
return this.config && this.config.maxrows && this.numberOfRows >= this.config.maxrows && h && h.classList.add(s.addRowDisabled), o;
}
/**
* Delete a column by index
*
* @param {number} index
*/
deleteColumn(t) {
for (let o = 1; o <= this.numberOfRows; o++) {
const i = this.getCell(o, t);
if (!i)
return;
i.remove();
}
const e = this.wrapper.querySelector(`.${s.addColumn}`);
e && e.classList.remove(s.addColumnDisabled);
}
/**
* Delete a row by index
*
* @param {number} index
*/
deleteRow(t) {
this.getRow(t).remove();
const e = this.wrapper.querySelector(`.${s.addRow}`);
e && e.classList.remove(s.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() {
if (this.wrapper = c("div", s.wrapper), this.table = c("div", s.table), this.readOnly && this.wrapper.classList.add(s.wrapperReadOnly), this.wrapper.appendChild(this.toolboxRow.element), this.wrapper.appendChild(this.toolboxColumn.element), this.wrapper.appendChild(this.table), !this.readOnly) {
const t = c("div", s.addColumn, {
innerHTML: v
}), e = c("div", s.addRow, {
innerHTML: v
});
this.wrapper.appendChild(t), this.wrapper.appendChild(e);
}
}
/**
* 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 t = this.data && this.data.content, e = Array.isArray(t), o = e ? t.length : !1, i = e ? t.length : void 0, n = o ? t[0].length : void 0, r = Number.parseInt(this.config && this.config.rows), h = Number.parseInt(this.config && this.config.cols), l = !isNaN(r) && r > 0 ? r : void 0, u = !isNaN(h) && h > 0 ? h : void 0;
return {
rows: i || l || 2,
cols: n || u || 2
};
}
/**
* Resize table to match config size or transmitted data size
*
* @return {{rows: number, cols: number}} - number of cols and rows
*/
resize() {
const { rows: t, cols: e } = this.computeInitialSize();
for (let o = 0; o < t; o++)
this.addRow();
for (let o = 0; o < e; o++)
this.addColumn();
}
/**
* Fills the table with data passed to the constructor
*
* @returns {void}
*/
fill() {
const t = this.data;
if (t && t.content)
for (let e = 0; e < t.content.length; e++)
for (let o = 0; o < t.content[e].length; o++)
this.setCellContent(e + 1, o + 1, t.content[e][o]);
}
/**
* Fills a row with cells
*
* @param {HTMLElement} row - row to fill
* @param {number} numberOfColumns - how many cells should be in a row
*/
fillRow(t, e) {
for (let o = 1; o <= e; o++) {
const i = this.createCell();
t.appendChild(i);
}
}
/**
* Creating a cell element
*
* @return {Element}
*/
createCell() {
return c("div", s.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() {
return this.numberOfRows ? this.table.querySelectorAll(`.${s.row}:first-child .${s.cell}`).length : 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(t) {
const { row: e, column: o } = this.getHoveredCell(t);
this.hoveredColumn = o, this.hoveredRow = e, this.updateToolboxesPosition();
}
/**
* Prevents default Enter behaviors
* Adds Shift+Enter processing
*
* @param {KeyboardEvent} event - keypress event
*/
onKeyPressListener(t) {
if (t.key === "Enter") {
if (t.shiftKey)
return !0;
this.moveCursorToNextRow();
}
return t.key !== "Enter";
}
/**
* Prevents tab keydown event from bubbling
* so that it only works inside the table
*
* @param {KeyboardEvent} event - keydown event
*/
onKeyDownListener(t) {
t.key === "Tab" && t.stopPropagation();
}
/**
* Set the coordinates of the cell that the focus has moved to
*
* @param {FocusEvent} event - focusin event
*/
focusInTableListener(t) {
const e = t.target, o = this.getRowByCell(e);
this.focusedCell = {
row: Array.from(this.table.querySelectorAll(`.${s.row}`)).indexOf(o) + 1,
column: Array.from(o.querySelectorAll(`.${s.cell}`)).indexOf(e) + 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: t, column: e } = this.focusedCell;
return this.getCell(t, e);
}
/**
* Update toolboxes position
*
* @param {number} row - hovered row
* @param {number} column - hovered column
*/
updateToolboxesPosition(t = this.hoveredRow, e = this.hoveredColumn) {
this.isColumnMenuShowing || e > 0 && e <= this.numberOfColumns && this.toolboxColumn.show(() => ({
left: `calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${e} - 1) * 2))`
})), this.isRowMenuShowing || t > 0 && t <= this.numberOfRows && this.toolboxRow.show(() => {
const o = this.getRow(t), { fromTopBorder: i } = g(this.table, o), { height: n } = o.getBoundingClientRect();
return {
top: `${Math.ceil(i + n / 2)}px`
};
});
}
/**
* Makes the first row headings
*
* @param {boolean} withHeadings - use headings row or not
*/
setHeadingsSetting(t) {
this.tunes.withHeadings = t, t ? (this.table.classList.add(s.withHeadings), this.addHeadingAttrToFirstRow()) : (this.table.classList.remove(s.withHeadings), this.removeHeadingAttrFromFirstRow());
}
/**
* Adds an attribute for displaying the placeholder in the cell
*/
addHeadingAttrToFirstRow() {
for (let t = 1; t <= this.numberOfColumns; t++) {
let e = this.getCell(1, t);
e && e.setAttribute("heading", this.api.i18n.t("Heading"));
}
}
/**
* Removes an attribute for displaying the placeholder in the cell
*/
removeHeadingAttrFromFirstRow() {
for (let t = 1; t <= this.numberOfColumns; t++) {
let e = this.getCell(1, t);
e && e.removeAttribute("heading");
}
}
/**
* Add effect of a selected row
*
* @param {number} index
*/
selectRow(t) {
const e = this.getRow(t);
e && (this.selectedRow = t, e.classList.add(s.rowSelected));
}
/**
* Remove effect of a selected row
*/
unselectRow() {
if (this.selectedRow <= 0)
return;
const t = this.table.querySelector(`.${s.rowSelected}`);
t && t.classList.remove(s.rowSelected), this.selectedRow = 0;
}
/**
* Add effect of a selected column
*
* @param {number} index
*/
selectColumn(t) {
for (let e = 1; e <= this.numberOfRows; e++) {
const o = this.getCell(e, t);
o && o.classList.add(s.cellSelected);
}
this.selectedColumn = t;
}
/**
* Remove effect of a selected column
*/
unselectColumn() {
if (this.selectedColumn <= 0)
return;
let t = this.table.querySelectorAll(`.${s.cellSelected}`);
Array.from(t).forEach((e) => {
e.classList.remove(s.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(t) {
let e = this.hoveredRow, o = this.hoveredColumn;
const { width: i, height: n, x: r, y: h } = k(this.table, t);
return r >= 0 && (o = this.binSearch(
this.numberOfColumns,
(l) => this.getCell(1, l),
({ fromLeftBorder: l }) => r < l,
({ fromRightBorder: l }) => r > i - l
)), h >= 0 && (e = this.binSearch(
this.numberOfRows,
(l) => this.getCell(l, 1),
({ fromTopBorder: l }) => h < l,
({ fromBottomBorder: l }) => h > n - l
)), {
row: e || this.hoveredRow,
column: o || 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(t, e, o, i) {
let n = 0, r = t + 1, h = 0, l;
for (; n < r - 1 && h < 10; ) {
l = Math.ceil((n + r) / 2);
const u = e(l), p = g(this.table, u);
if (o(p))
r = l;
else if (i(p))
n = l;
else
break;
h++;
}
return l;
}
/**
* Collects data from cells into a two-dimensional array
*
* @returns {string[][]}
*/
getData() {
const t = [];
for (let e = 1; e <= this.numberOfRows; e++) {
const o = this.table.querySelector(`.${s.row}:nth-child(${e})`), i = Array.from(o.querySelectorAll(`.${s.cell}`));
i.every((r) => !r.textContent.trim()) || t.push(i.map((r) => r.innerHTML));
}
return t;
}
/**
* Remove listeners on the document
*/
destroy() {
document.removeEventListener("click", this.documentClicked);
}
}
class F {
/**
* Notify core that read-only mode is supported
*
* @returns {boolean}
*/
static get isReadOnlySupported() {
return !0;
}
/**
* Allow to press Enter inside the CodeTool textarea
*
* @returns {boolean}
* @public
*/
static get enableLineBreaks() {
return !0;
}
/**
* Render plugin`s main Element and fill it with saved data
*
* @param {TableConstructor} init
*/
constructor({ data: t, config: e, api: o, readOnly: i, block: n }) {
this.api = o, this.readOnly = i, this.config = e, this.data = {
withHeadings: this.getConfig("withHeadings", !1, t),
stretched: this.getConfig("stretched", !1, t),
content: t && t.content ? t.content : []
}, this.table = null, this.block = n;
}
/**
* 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: A,
title: "Table"
};
}
/**
* Return Tool's view
*
* @returns {HTMLDivElement}
*/
render() {
return this.table = new E(this.readOnly, this.api, this.data, this.config), this.container = c("div", this.api.styles.block), this.container.appendChild(this.table.getWrapper()), this.table.setHeadingsSetting(this.data.withHeadings), this.container;
}
/**
* Returns plugin settings
*
* @returns {Array}
*/
renderSettings() {
return [
{
label: this.api.i18n.t("With headings"),
icon: T,
isActive: this.data.withHeadings,
closeOnActivate: !0,
toggle: !0,
onActivate: () => {
this.data.withHeadings = !0, this.table.setHeadingsSetting(this.data.withHeadings);
}
},
{
label: this.api.i18n.t("Without headings"),
icon: H,
isActive: !this.data.withHeadings,
closeOnActivate: !0,
toggle: !0,
onActivate: () => {
this.data.withHeadings = !1, this.table.setHeadingsSetting(this.data.withHeadings);
}
},
{
label: this.data.stretched ? this.api.i18n.t("Collapse") : this.api.i18n.t("Stretch"),
icon: this.data.stretched ? R : O,
closeOnActivate: !0,
toggle: !0,
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 t = this.table.getData();
return {
withHeadings: this.data.withHeadings,
stretched: this.data.stretched,
content: t
};
}
/**
* 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(t, e = void 0, o = void 0) {
const i = this.data || o;
return i ? i[t] ? i[t] : e : this.config && this.config[t] ? this.config[t] : e;
}
/**
* 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(t) {
const e = t.detail.data, o = e.querySelector(":scope > thead, tr:first-of-type th"), n = Array.from(e.querySelectorAll("tr")).map((r) => Array.from(r.querySelectorAll("th, td")).map((l) => l.innerHTML));
this.data = {
withHeadings: o !== null,
content: n
}, this.table.wrapper && this.table.wrapper.replaceWith(this.render());
}
}
export {
F as default
};