diff --git a/.gitignore b/.gitignore index 2fd83a6..7a1537b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .idea node_modules -dist diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..48a9a9c --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,2 @@ +import { default as Plugin } from './plugin'; +export default Plugin; diff --git a/dist/plugin.d.ts b/dist/plugin.d.ts new file mode 100644 index 0000000..2409c83 --- /dev/null +++ b/dist/plugin.d.ts @@ -0,0 +1,224 @@ +import { default as Table } from './table'; +/** + * @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(): boolean; + /** + * Allow to press Enter inside the CodeTool textarea + * + * @returns {boolean} + * @public + */ + public static get enableLineBreaks(): boolean; + /** + * Get Tool toolbox settings + * icon - Tool icon's SVG + * title - title to show in toolbox + * + * @returns {{icon: string, title: string}} + */ + static get toolbox(): { + icon: string; + title: string; + }; + /** + * Table onPaste configuration + * + * @public + */ + public static get pasteConfig(): { + tags: string[]; + }; + /** + * Render plugin`s main Element and fill it with saved data + * + * @param {TableConstructor} init + */ + constructor({ data, config, api, readOnly, block }: TableConstructor); + api: any; + readOnly: boolean; + config: TableConfig; + data: { + withHeadings: any; + stretched: any; + content: string[][]; + }; + table: Table; + block: any; + /** + * Return Tool's view + * + * @returns {HTMLDivElement} + */ + render(): HTMLDivElement; + /** creating container around table */ + container: Element; + /** + * Returns plugin settings + * + * @returns {Array} + */ + renderSettings(): any[]; + /** + * Extract table data from the view + * + * @returns {TableData} - saved data + */ + save(): TableData; + /** + * Plugin destroyer + * + * @returns {void} + */ + destroy(): void; + /** + * 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: string, defaultValue?: any, savedData?: object): any; + /** + * On paste callback that is fired from Editor + * + * @param {PasteEvent} event - event with pasted data + */ + onPaste(event: PasteEvent): void; +} +/** + * - configuration that the user can set for the table + */ +export type TableData = { + /** + * - number of rows in the table + */ + /** + * - number of rows in the table + */ + rows: number; + /** + * - number of columns in the table + */ + /** + * - number of columns in the table + */ + cols: number; +}; +/** + * - setting for the table + */ +export type Tune = { + /** + * - tune name + */ + /** + * - tune name + */ + name: string; + /** + * - icon for the tune + */ + /** + * - icon for the tune + */ + icon: HTMLElement; + /** + * - default state of the tune + */ + /** + * - default state of the tune + */ + isActive: boolean; + /** + * - set tune state to the table data + */ + /** + * - set tune state to the table data + */ + setTune: void; +}; +/** + * - object with the data transferred to form a table + */ +export type TableConfig = { + /** + * - setting to use cells of the first row as headings + */ + /** + * - setting to use cells of the first row as headings + */ + withHeading: boolean; + /** + * - two-dimensional array which contains table content + */ + /** + * - two-dimensional array which contains table content + */ + content: string[][]; +}; +export type TableConstructor = { + /** + * — previously saved data + */ + /** + * — previously saved data + */ + data: TableConfig; + /** + * - user config for Tool + */ + /** + * - user config for Tool + */ + config: TableConfig; + /** + * - Editor.js API + */ + /** + * - Editor.js API + */ + api: object; + /** + * - read-only mode flag + */ + /** + * - read-only mode flag + */ + readOnly: boolean; +}; +export type PasteEvent = any; diff --git a/dist/table.d.ts b/dist/table.d.ts new file mode 100644 index 0000000..dac362e --- /dev/null +++ b/dist/table.d.ts @@ -0,0 +1,368 @@ +import { default as Toolbox } from './toolbox'; +/** + * @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: boolean, api: object, data: TableData, config: TableConfig); + readOnly: boolean; + api: any; + data: TableData; + config: any; + /** + * DOM nodes + */ + wrapper: Element; + table: Element; + /** + * Toolbox for managing of columns + */ + toolboxColumn: Toolbox; + toolboxRow: Toolbox; + hoveredRow: number; + hoveredColumn: number; + selectedRow: number; + selectedColumn: number; + tunes: { + withHeadings: boolean; + }; + /** + * 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 + */ + focusedCell: { + row: number; + column: number; + }; + /** + * Global click listener allows to delegate clicks on some elements + */ + documentClicked: (event: any) => void; + /** + * Returns the rendered table wrapper + * + * @returns {Element} + */ + getWrapper(): Element; + /** + * Hangs the necessary handlers to events + */ + bindEvents(): void; + /** + * Configures and creates the toolbox for manipulating with columns + * + * @returns {Toolbox} + */ + createColumnToolbox(): Toolbox; + /** + * Configures and creates the toolbox for manipulating with rows + * + * @returns {Toolbox} + */ + createRowToolbox(): Toolbox; + /** + * 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(): void; + /** + * Get table cell by row and col index + * + * @param {number} row - cell row coordinate + * @param {number} column - cell column coordinate + * @returns {HTMLElement} + */ + getCell(row: number, column: number): HTMLElement; + /** + * Get table row by index + * + * @param {number} row - row coordinate + * @returns {HTMLElement} + */ + getRow(row: number): HTMLElement; + /** + * The parent of the cell which is the row + * + * @param {HTMLElement} cell - cell element + * @returns {HTMLElement} + */ + getRowByCell(cell: HTMLElement): HTMLElement; + /** + * Ger row's first cell + * + * @param {Element} row - row to find its first cell + * @returns {Element} + */ + getRowFirstCell(row: Element): Element; + /** + * 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: number, column: number, content: string): void; + /** + * 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?: number, setFocus?: boolean): void; + /** + * 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?: number, setFocus?: boolean): HTMLElement; + /** + * Delete a column by index + * + * @param {number} index + */ + deleteColumn(index: number): void; + /** + * Delete a row by index + * + * @param {number} index + */ + deleteRow(index: number): void; + /** + * 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(): HTMLElement; + /** + * 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(): { + rows: number; + cols: number; + }; + /** + * Resize table to match config size or transmitted data size + * + * @return {{rows: number, cols: number}} - number of cols and rows + */ + resize(): { + rows: number; + cols: number; + }; + /** + * Fills the table with data passed to the constructor + * + * @returns {void} + */ + fill(): void; + /** + * Fills a row with cells + * + * @param {HTMLElement} row - row to fill + * @param {number} numberOfColumns - how many cells should be in a row + */ + fillRow(row: HTMLElement, numberOfColumns: number): void; + /** + * Creating a cell element + * + * @return {Element} + */ + createCell(): Element; + /** + * Get number of rows in the table + */ + get numberOfRows(): number; + /** + * Get number of columns in the table + */ + get numberOfColumns(): number; + /** + * Is the column toolbox menu displayed or not + * + * @returns {boolean} + */ + get isColumnMenuShowing(): boolean; + /** + * Is the row toolbox menu displayed or not + * + * @returns {boolean} + */ + get isRowMenuShowing(): boolean; + /** + * Recalculate position of toolbox icons + * + * @param {Event} event - mouse move event + */ + onMouseMoveInTable(event: Event): void; + /** + * Prevents default Enter behaviors + * Adds Shift+Enter processing + * + * @param {KeyboardEvent} event - keypress event + */ + onKeyPressListener(event: KeyboardEvent): boolean; + /** + * Prevents tab keydown event from bubbling + * so that it only works inside the table + * + * @param {KeyboardEvent} event - keydown event + */ + onKeyDownListener(event: KeyboardEvent): void; + /** + * Set the coordinates of the cell that the focus has moved to + * + * @param {FocusEvent} event - focusin event + */ + focusInTableListener(event: FocusEvent): void; + /** + * Unselect row/column + * Close toolbox menu + * Hide toolboxes + * + * @returns {void} + */ + hideToolboxes(): void; + /** + * Unselect row, close toolbox + * + * @returns {void} + */ + hideRowToolbox(): void; + /** + * Unselect column, close toolbox + * + * @returns {void} + */ + hideColumnToolbox(): void; + /** + * Set the cursor focus to the focused cell + * + * @returns {void} + */ + focusCell(): void; + /** + * Get current focused element + * + * @returns {HTMLElement} - focused cell + */ + get focusedCellElem(): HTMLElement; + /** + * Update toolboxes position + * + * @param {number} row - hovered row + * @param {number} column - hovered column + */ + updateToolboxesPosition(row?: number, column?: number): void; + /** + * Makes the first row headings + * + * @param {boolean} withHeadings - use headings row or not + */ + setHeadingsSetting(withHeadings: boolean): void; + /** + * Adds an attribute for displaying the placeholder in the cell + */ + addHeadingAttrToFirstRow(): void; + /** + * Removes an attribute for displaying the placeholder in the cell + */ + removeHeadingAttrFromFirstRow(): void; + /** + * Add effect of a selected row + * + * @param {number} index + */ + selectRow(index: number): void; + /** + * Remove effect of a selected row + */ + unselectRow(): void; + /** + * Add effect of a selected column + * + * @param {number} index + */ + selectColumn(index: number): void; + /** + * Remove effect of a selected column + */ + unselectColumn(): void; + /** + * 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: Event): { + row: number; + column: number; + }; + /** + * 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: number, getCell: Function, beforeTheLeftBorder: Function, afterTheRightBorder: Function): number; + /** + * Collects data from cells into a two-dimensional array + * + * @returns {string[][]} + */ + getData(): string[][]; + /** + * Remove listeners on the document + */ + destroy(): void; +} +export type TableConfig = object; +/** + * - object with the data transferred to form a table + */ +export type TableData = { + /** + * - number of rows in the table + */ + /** + * - number of rows in the table + */ + rows: number; + /** + * - number of columns in the table + */ + /** + * - number of columns in the table + */ + cols: number; +}; diff --git a/dist/table.mjs b/dist/table.mjs new file mode 100644 index 0000000..0ad96e6 --- /dev/null +++ b/dist/table.mjs @@ -0,0 +1,1024 @@ +(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;box-sizing:border-box;display:grid;grid-template-columns:calc(100% - var(--cell-size)) var(--cell-size);height:100%;margin-top:var(--toolbox-icon-size);position:relative;width:100%}.tc-wrap--readonly{grid-template-columns:100% var(--cell-size)}.tc-wrap svg{vertical-align:top}@media print{.tc-wrap{border-left:1px solid var(--color-border);grid-template-columns:100% var(--cell-size)}.tc-wrap .tc-row:after{display:none}}.tc-table{border-top:1px solid var(--color-border);display:grid;font-size:14px;height:100%;line-height:1.4;position:relative;width:100%}.tc-table:after{content:"";height:100%;left:calc(var(--cell-size)*-1);position:absolute;top:0;width:calc(var(--cell-size))}.tc-table:before{content:"";height:var(--toolbox-aiming-field-size);left:0;position:absolute;top:calc(var(--toolbox-aiming-field-size)*-1);width:100%}.tc-table--heading .tc-row:first-child{border-bottom:2px solid var(--color-border);font-weight:600}.tc-table--heading .tc-row:first-child [contenteditable]:empty:before{color:var(--color-text-secondary);content:attr(heading)}.tc-table--heading .tc-row:first-child:after{border-bottom:2px solid var(--color-border);bottom:-2px}.tc-add-column,.tc-add-row{color:var(--color-text-secondary);display:flex}@media print{.tc-add{display:none}}.tc-add-column{border-top:1px solid var(--color-border);justify-content:center;padding:4px 0}.tc-add-column--disabled{visibility:hidden}@media print{.tc-add-column{display:none}}.tc-add-row{align-items:center;height:var(--cell-size);padding-left:4px;position:relative}.tc-add-row--disabled{display:none}.tc-add-row:before{content:"";height:100%;position:absolute;right:calc(var(--cell-size)*-1);width:var(--cell-size)}@media print{.tc-add-row{display:none}}.tc-add-column,.tc-add-row{cursor:pointer;transition:0s;will-change:background-color}.tc-add-column:hover,.tc-add-row:hover{background-color:var(--color-background);transition:background-color .1s ease}.tc-add-row{margin-top:1px}.tc-add-row:hover:before{background-color:var(--color-background);transition:.1s}.tc-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(10px,1fr));position:relative}.tc-row,.tc-row:after{border-bottom:1px solid var(--color-border)}.tc-row:after{bottom:-1px;content:"";height:100%;pointer-events:none;position:absolute;right:calc(var(--cell-size)*-1);width:var(--cell-size)}.tc-row--selected,.tc-row--selected:after{background:var(--color-background)}.tc-cell{border-right:1px solid var(--color-border);line-break:normal;outline:none;overflow:hidden;padding:6px 12px}.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;cursor:pointer;opacity:0;position:absolute;transition:opacity .1s;will-change:left,opacity;z-index:1}.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);margin-top:-1px;transform:translateY(calc(var(--toggler-click-zone-size)*-1/2));will-change:top,opacity}.tc-toolbox--showed{opacity:1}.tc-toolbox .tc-popover{position:absolute}.tc-toolbox__toggler{align-items:center;color:var(--toggler-dots-color);display:flex;height:var(--toggler-click-zone-size);justify-content:center;opacity:0;transition:opacity .15s ease;width:var(--toggler-click-zone-size);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{margin:0;width:50%}.tc-popover{--color-border:#eaeaea;--color-background:#fff;--color-background-hover:hsla(240,7%,92%,.49);--color-background-confirm:#e24a4a;--color-background-confirm-hover:#d54040;--color-text-confirm:#fff;background:var(--color-background);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 3px 15px -3px #0d142121;display:none;padding:6px;will-change:opacity,transform}.tc-popover--opened{animation:menuShowing .1s cubic-bezier(.215,.61,.355,1) forwards;display:block}.tc-popover__item{align-items:center;border-radius:5px;cursor:pointer;display:flex;padding:2px 14px 2px 2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.tc-popover__item:hover{background:var(--color-background-hover)}.tc-popover__item:not(:last-of-type){margin-bottom:2px}.tc-popover__item-icon{align-items:center;background:var(--color-background);border:1px solid var(--color-border);border-radius:5px;display:inline-flex;height:26px;justify-content:center;margin-right:8px;width:26px}.tc-popover__item-label{font-size:14px;font-weight:500;line-height:22px}.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{transform:rotate(90deg) scale(1.2);transition:transform .2s ease-in}.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 m(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 R(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 g(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 k = '', b = '', x = '', S = '', y = '', O = '', M = '', v = '', L = '', T = '', H = '', A = ''; +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(), this.numberOfColumns = 0, this.numberOfRows = 0, this.currentColumn = 0, this.currentRow = 0; + } + /** + * 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() { + let t = {}; + console.log(this.currentColumn, Math.ceil(this.numberOfColumns / 2)), this.currentColumn > Math.ceil(this.numberOfColumns / 2) ? (t.right = "var(--popover-margin)", t.left = "auto") : (t.left = "var(--popover-margin)", t.right = "auto"), this.currentRow > Math.ceil(this.numberOfRows / 2) ? (t.bottom = 0, t.top = "auto") : (t.top = 0, t.bottom = "auto"), Object.entries(t).forEach(([e, o]) => { + this.popover.wrapper.style[e] = o; + }), 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.style).forEach(([o, i]) => { + this.wrapper.style[o] = i; + }), console.log(e, this.cssModifier), this.cssModifier == "row" ? (this.numberOfRows = e.numberOfRows, this.currentRow = e.currentRow) : this.cssModifier == "column" && (this.numberOfColumns = e.numberOfColumns, this.currentColumn = e.currentColumn), 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: O, + 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), g(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 = g(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(() => ({ + style: { + left: `calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${e} - 1) * 2))` + }, + numberOfColumns: this.numberOfColumns, + currentColumn: e + })), this.isRowMenuShowing || t > 0 && t <= this.numberOfRows && this.toolboxRow.show(() => { + const o = this.getRow(t), { fromTopBorder: i } = m(this.table, o), { height: n } = o.getBoundingClientRect(); + return { + style: { + top: `${Math.ceil(i + n / 2)}px` + }, + numberOfRows: this.numberOfRows, + currentRow: t + }; + }); + } + /** + * 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 } = R(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 = m(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 j { + /** + * 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 ? k : L, + 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 { + j as default +}; diff --git a/dist/table.umd.js b/dist/table.umd.js new file mode 100644 index 0000000..0092a65 --- /dev/null +++ b/dist/table.umd.js @@ -0,0 +1,2 @@ +(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;box-sizing:border-box;display:grid;grid-template-columns:calc(100% - var(--cell-size)) var(--cell-size);height:100%;margin-top:var(--toolbox-icon-size);position:relative;width:100%}.tc-wrap--readonly{grid-template-columns:100% var(--cell-size)}.tc-wrap svg{vertical-align:top}@media print{.tc-wrap{border-left:1px solid var(--color-border);grid-template-columns:100% var(--cell-size)}.tc-wrap .tc-row:after{display:none}}.tc-table{border-top:1px solid var(--color-border);display:grid;font-size:14px;height:100%;line-height:1.4;position:relative;width:100%}.tc-table:after{content:"";height:100%;left:calc(var(--cell-size)*-1);position:absolute;top:0;width:calc(var(--cell-size))}.tc-table:before{content:"";height:var(--toolbox-aiming-field-size);left:0;position:absolute;top:calc(var(--toolbox-aiming-field-size)*-1);width:100%}.tc-table--heading .tc-row:first-child{border-bottom:2px solid var(--color-border);font-weight:600}.tc-table--heading .tc-row:first-child [contenteditable]:empty:before{color:var(--color-text-secondary);content:attr(heading)}.tc-table--heading .tc-row:first-child:after{border-bottom:2px solid var(--color-border);bottom:-2px}.tc-add-column,.tc-add-row{color:var(--color-text-secondary);display:flex}@media print{.tc-add{display:none}}.tc-add-column{border-top:1px solid var(--color-border);justify-content:center;padding:4px 0}.tc-add-column--disabled{visibility:hidden}@media print{.tc-add-column{display:none}}.tc-add-row{align-items:center;height:var(--cell-size);padding-left:4px;position:relative}.tc-add-row--disabled{display:none}.tc-add-row:before{content:"";height:100%;position:absolute;right:calc(var(--cell-size)*-1);width:var(--cell-size)}@media print{.tc-add-row{display:none}}.tc-add-column,.tc-add-row{cursor:pointer;transition:0s;will-change:background-color}.tc-add-column:hover,.tc-add-row:hover{background-color:var(--color-background);transition:background-color .1s ease}.tc-add-row{margin-top:1px}.tc-add-row:hover:before{background-color:var(--color-background);transition:.1s}.tc-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(10px,1fr));position:relative}.tc-row,.tc-row:after{border-bottom:1px solid var(--color-border)}.tc-row:after{bottom:-1px;content:"";height:100%;pointer-events:none;position:absolute;right:calc(var(--cell-size)*-1);width:var(--cell-size)}.tc-row--selected,.tc-row--selected:after{background:var(--color-background)}.tc-cell{border-right:1px solid var(--color-border);line-break:normal;outline:none;overflow:hidden;padding:6px 12px}.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;cursor:pointer;opacity:0;position:absolute;transition:opacity .1s;will-change:left,opacity;z-index:1}.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);margin-top:-1px;transform:translateY(calc(var(--toggler-click-zone-size)*-1/2));will-change:top,opacity}.tc-toolbox--showed{opacity:1}.tc-toolbox .tc-popover{position:absolute}.tc-toolbox__toggler{align-items:center;color:var(--toggler-dots-color);display:flex;height:var(--toggler-click-zone-size);justify-content:center;opacity:0;transition:opacity .15s ease;width:var(--toggler-click-zone-size);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{margin:0;width:50%}.tc-popover{--color-border:#eaeaea;--color-background:#fff;--color-background-hover:hsla(240,7%,92%,.49);--color-background-confirm:#e24a4a;--color-background-confirm-hover:#d54040;--color-text-confirm:#fff;background:var(--color-background);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 3px 15px -3px #0d142121;display:none;padding:6px;will-change:opacity,transform}.tc-popover--opened{animation:menuShowing .1s cubic-bezier(.215,.61,.355,1) forwards;display:block}.tc-popover__item{align-items:center;border-radius:5px;cursor:pointer;display:flex;padding:2px 14px 2px 2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.tc-popover__item:hover{background:var(--color-background-hover)}.tc-popover__item:not(:last-of-type){margin-bottom:2px}.tc-popover__item-icon{align-items:center;background:var(--color-background);border:1px solid var(--color-border);border-radius:5px;display:inline-flex;height:26px;justify-content:center;margin-right:8px;width:26px}.tc-popover__item-label{font-size:14px;font-weight:500;line-height:22px}.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{transform:rotate(90deg) scale(1.2);transition:transform .2s ease-in}.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(d,p){typeof exports=="object"&&typeof module<"u"?module.exports=p():typeof define=="function"&&define.amd?define(p):(d=typeof globalThis<"u"?globalThis:d||self,d.Table=p())})(this,function(){"use strict";function d(a,t,e={}){const o=document.createElement(a);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 p(a){const t=a.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 m(a,t){const e=p(a),o=p(t);return{fromTopBorder:o.y1-e.y1,fromLeftBorder:o.x1-e.x1,fromRightBorder:e.x2-o.x2,fromBottomBorder:e.y2-o.y2}}function R(a,t){const e=a.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 g(a,t){return t.parentNode.insertBefore(a,t)}function C(a,t=!0){const e=document.createRange(),o=window.getSelection();e.selectNodeContents(a),e.collapse(t),o.removeAllRanges(),o.addRange(e)}class c{constructor({items:t}){this.items=t,this.wrapper=void 0,this.itemEls=[]}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"}}render(){return this.wrapper=d("div",c.CSS.popover),this.items.forEach((t,e)=>{const o=d("div",c.CSS.item),i=d("div",c.CSS.itemIcon,{innerHTML:t.icon}),n=d("div",c.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}popoverClicked(t){const e=t.target.closest(`.${c.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()}setConfirmationState(t){t.classList.add(c.CSS.itemConfirmState)}clearConfirmationState(t){t.classList.remove(c.CSS.itemConfirmState)}hasConfirmationState(t){return t.classList.contains(c.CSS.itemConfirmState)}get opened(){return this.wrapper.classList.contains(c.CSS.popoverOpened)}open(){this.items.forEach((t,e)=>{typeof t.hideIf=="function"&&this.itemEls[e].classList.toggle(c.CSS.itemHidden,t.hideIf())}),this.wrapper.classList.add(c.CSS.popoverOpened)}close(){this.wrapper.classList.remove(c.CSS.popoverOpened),this.itemEls.forEach(t=>{this.clearConfirmationState(t)})}}const k='',b='',x='',S='',y='',O='',M='',v='',L='',T='',H='',A='';class w{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(),this.numberOfColumns=0,this.numberOfRows=0,this.currentColumn=0,this.currentRow=0}static get CSS(){return{toolbox:"tc-toolbox",toolboxShowed:"tc-toolbox--showed",toggler:"tc-toolbox__toggler"}}get element(){return this.wrapper}createToolbox(){const t=d("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}createToggler(){const t=d("div",w.CSS.toggler,{innerHTML:M});return t.addEventListener("click",()=>{this.togglerClicked()}),t}createPopover(){return this.popover=new c({items:this.items}),this.popover.render()}togglerClicked(){let t={};console.log(this.currentColumn,Math.ceil(this.numberOfColumns/2)),this.currentColumn>Math.ceil(this.numberOfColumns/2)?(t.right="var(--popover-margin)",t.left="auto"):(t.left="var(--popover-margin)",t.right="auto"),this.currentRow>Math.ceil(this.numberOfRows/2)?(t.bottom=0,t.top="auto"):(t.top=0,t.bottom="auto"),Object.entries(t).forEach(([e,o])=>{this.popover.wrapper.style[e]=o}),this.popover.opened?(this.popover.close(),this.onClose()):(this.popover.open(),this.onOpen())}show(t){const e=t();Object.entries(e.style).forEach(([o,i])=>{this.wrapper.style[o]=i}),console.log(e,this.cssModifier),this.cssModifier=="row"?(this.numberOfRows=e.numberOfRows,this.currentRow=e.currentRow):this.cssModifier=="column"&&(this.numberOfColumns=e.numberOfColumns,this.currentColumn=e.currentColumn),this.wrapper.classList.add(w.CSS.toolboxShowed)}hide(){this.popover.close(),this.wrapper.classList.remove(w.CSS.toolboxShowed)}}function B(a,t){let e=0;return function(...o){const i=new Date().getTime();if(!(i-e{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}`),f=n.target.closest(`.${s.addColumn}`);u&&u.parentNode===this.wrapper?(this.addRow(void 0,!0),this.hideToolboxes()):f&&f.parentNode===this.wrapper&&(this.addColumn(void 0,!0),this.hideToolboxes())},this.readOnly||this.bindEvents()}getWrapper(){return this.wrapper}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))}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()}})}createRowToolbox(){return new w({api:this.api,cssModifier:"row",items:[{label:this.api.i18n.t("Add row above"),icon:O,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()}})}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))}getCell(t,e){return this.table.querySelectorAll(`.${s.row}:nth-child(${t}) .${s.cell}`)[e-1]}getRow(t){return this.table.querySelector(`.${s.row}:nth-child(${t})`)}getRowByCell(t){return t.parentElement}getRowFirstCell(t){return t.querySelector(`.${s.cell}:first-child`)}setCellContent(t,e,o){const i=this.getCell(t,e);i.innerHTML=o}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),g(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()}addRow(t=-1,e=!1){let o,i=d("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=g(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}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)}deleteRow(t){this.getRow(t).remove();const e=this.wrapper.querySelector(`.${s.addRow}`);e&&e.classList.remove(s.addRowDisabled),this.addHeadingAttrToFirstRow()}createTableWrapper(){if(this.wrapper=d("div",s.wrapper),this.table=d("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=d("div",s.addColumn,{innerHTML:v}),e=d("div",s.addRow,{innerHTML:v});this.wrapper.appendChild(t),this.wrapper.appendChild(e)}}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(){const{rows:t,cols:e}=this.computeInitialSize();for(let o=0;o0&&e<=this.numberOfColumns&&this.toolboxColumn.show(()=>({style:{left:`calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${e} - 1) * 2))`},numberOfColumns:this.numberOfColumns,currentColumn:e})),this.isRowMenuShowing||t>0&&t<=this.numberOfRows&&this.toolboxRow.show(()=>{const o=this.getRow(t),{fromTopBorder:i}=m(this.table,o),{height:n}=o.getBoundingClientRect();return{style:{top:`${Math.ceil(i+n/2)}px`},numberOfRows:this.numberOfRows,currentRow:t}})}setHeadingsSetting(t){this.tunes.withHeadings=t,t?(this.table.classList.add(s.withHeadings),this.addHeadingAttrToFirstRow()):(this.table.classList.remove(s.withHeadings),this.removeHeadingAttrFromFirstRow())}addHeadingAttrToFirstRow(){for(let t=1;t<=this.numberOfColumns;t++){let e=this.getCell(1,t);e&&e.setAttribute("heading",this.api.i18n.t("Heading"))}}removeHeadingAttrFromFirstRow(){for(let t=1;t<=this.numberOfColumns;t++){let e=this.getCell(1,t);e&&e.removeAttribute("heading")}}selectRow(t){const e=this.getRow(t);e&&(this.selectedRow=t,e.classList.add(s.rowSelected))}unselectRow(){if(this.selectedRow<=0)return;const t=this.table.querySelector(`.${s.rowSelected}`);t&&t.classList.remove(s.rowSelected),this.selectedRow=0}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}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}getHoveredCell(t){let e=this.hoveredRow,o=this.hoveredColumn;const{width:i,height:n,x:r,y:h}=R(this.table,t);return r>=0&&(o=this.binSearch(this.numberOfColumns,l=>this.getCell(1,l),({fromLeftBorder:l})=>rr>i-l)),h>=0&&(e=this.binSearch(this.numberOfRows,l=>this.getCell(l,1),({fromTopBorder:l})=>hh>n-l)),{row:e||this.hoveredRow,column:o||this.hoveredColumn}}binSearch(t,e,o,i){let n=0,r=t+1,h=0,l;for(;n!r.textContent.trim())||t.push(i.map(r=>r.innerHTML))}return t}destroy(){document.removeEventListener("click",this.documentClicked)}}class ${static get isReadOnlySupported(){return!0}static get enableLineBreaks(){return!0}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}static get toolbox(){return{icon:A,title:"Table"}}render(){return this.table=new E(this.readOnly,this.api,this.data,this.config),this.container=d("div",this.api.styles.block),this.container.appendChild(this.table.getWrapper()),this.table.setHeadingsSetting(this.data.withHeadings),this.container}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?k:L,closeOnActivate:!0,toggle:!0,onActivate:()=>{this.data.stretched=!this.data.stretched,this.block.stretched=this.data.stretched}}]}save(){const t=this.table.getData();return{withHeadings:this.data.withHeadings,stretched:this.data.stretched,content:t}}destroy(){this.table.destroy()}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}static get pasteConfig(){return{tags:["TABLE","TR","TH","TD"]}}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())}}const I="";return $}); diff --git a/dist/toolbox.d.ts b/dist/toolbox.d.ts new file mode 100644 index 0000000..63da96a --- /dev/null +++ b/dist/toolbox.d.ts @@ -0,0 +1,134 @@ +import { default as Popover } from './utils/popover'; +/** + * @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: + * + * + * + * + */ +export default class Toolbox { + /** + * Style classes + */ + static get CSS(): { + toolbox: string; + toolboxShowed: string; + toggler: string; + }; + /** + * 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 }: { + api: any; + items: PopoverItem[]; + onOpen: Function; + onClose: Function; + cssModifier: string; + }); + api: any; + items: PopoverItem[]; + onOpen: Function; + onClose: Function; + cssModifier: string; + popover: Popover; + wrapper: Element; + numberOfColumns: number; + numberOfRows: number; + currentColumn: number; + currentRow: number; + /** + * Returns rendered Toolbox element + */ + get element(): Element; + /** + * Creating a toolbox to open menu for a manipulating columns + * + * @returns {Element} + */ + createToolbox(): Element; + /** + * Creates the Toggler + * + * @returns {Element} + */ + createToggler(): Element; + /** + * Creates the Popover instance and render it + * + * @returns {Element} + */ + createPopover(): Element; + /** + * Toggler click handler. Opens/Closes the popover + * + * @returns {void} + */ + togglerClicked(): void; + /** + * Shows the Toolbox + * + * @param {function} computePositionMethod - method that returns the position coordinate + * @returns {void} + */ + show(computePositionMethod: Function): void; + /** + * Hides the Toolbox + * + * @returns {void} + */ + hide(): void; +} +export type PopoverItem = { + /** + * - button text + */ + /** + * - button text + */ + label: string; + /** + * - button icon + */ + /** + * - button icon + */ + icon: string; + /** + * - if true, a confirmation state will be applied on the first click + */ + /** + * - if true, a confirmation state will be applied on the first click + */ + confirmationRequired: boolean; + /** + * - if provided, item will be hid, if this method returns true + */ + /** + * - if provided, item will be hid, if this method returns true + */ + hideIf: Function; + /** + * - click callback + */ + /** + * - click callback + */ + onClick: Function; +}; diff --git a/dist/utils/dom.d.ts b/dist/utils/dom.d.ts new file mode 100644 index 0000000..0200a84 --- /dev/null +++ b/dist/utils/dom.d.ts @@ -0,0 +1,71 @@ +/** + * 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: string, classNames: string | string[], attributes?: object): Element; +/** + * 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: HTMLElement): { + x1: number; + y1: number; + x2: number; + y2: number; +}; +/** + * 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: HTMLElement, secondElem: HTMLElement): { + fromTopBorder: number; + fromLeftBorder: number; + fromRightBorder: number; + fromBottomBorder: number; +}; +/** + * 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: HTMLElement, event: Event): { + width: number; + height: number; + x: number; + y: number; +}; +/** + * Insert element after the referenced + * + * @param {HTMLElement} newNode + * @param {HTMLElement} referenceNode + * @returns {HTMLElement} + */ +export function insertAfter(newNode: HTMLElement, referenceNode: HTMLElement): HTMLElement; +/** + * Insert element after the referenced + * + * @param {HTMLElement} newNode + * @param {HTMLElement} referenceNode + * @returns {HTMLElement} + */ +export function insertBefore(newNode: HTMLElement, referenceNode: HTMLElement): HTMLElement; +/** + * 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: Element, atStart?: boolean): void; diff --git a/dist/utils/popover.d.ts b/dist/utils/popover.d.ts new file mode 100644 index 0000000..3732acc --- /dev/null +++ b/dist/utils/popover.d.ts @@ -0,0 +1,115 @@ +/** + * @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 { + /** + * Set of CSS classnames used in popover + * + * @returns {object} + */ + static get CSS(): any; + /** + * @param {object} options - constructor options + * @param {PopoverItem[]} options.items - constructor options + */ + constructor({ items }: { + items: PopoverItem[]; + }); + items: PopoverItem[]; + wrapper: Element; + itemEls: any[]; + /** + * Returns the popover element + * + * @returns {Element} + */ + render(): Element; + /** + * Popover wrapper click listener + * Used to delegate clicks in items + * + * @returns {void} + */ + popoverClicked(event: any): void; + /** + * Enable the confirmation state on passed item + * + * @returns {void} + */ + setConfirmationState(itemEl: any): void; + /** + * Disable the confirmation state on passed item + * + * @returns {void} + */ + clearConfirmationState(itemEl: any): void; + /** + * Check if passed item has the confirmation state + * + * @returns {boolean} + */ + hasConfirmationState(itemEl: any): boolean; + /** + * Return an opening state + * + * @returns {boolean} + */ + get opened(): boolean; + /** + * Opens the popover + * + * @returns {void} + */ + open(): void; + /** + * Closes the popover + * + * @returns {void} + */ + close(): void; +} +export type PopoverItem = { + /** + * - button text + */ + /** + * - button text + */ + label: string; + /** + * - button icon + */ + /** + * - button icon + */ + icon: string; + /** + * - if true, a confirmation state will be applied on the first click + */ + /** + * - if true, a confirmation state will be applied on the first click + */ + confirmationRequired: boolean; + /** + * - if provided, item will be hid, if this method returns true + */ + /** + * - if provided, item will be hid, if this method returns true + */ + hideIf: Function; + /** + * - click callback + */ + /** + * - click callback + */ + onClick: Function; +}; diff --git a/dist/utils/throttled.d.ts b/dist/utils/throttled.d.ts new file mode 100644 index 0000000..c758ba0 --- /dev/null +++ b/dist/utils/throttled.d.ts @@ -0,0 +1,7 @@ +/** + * 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: number, fn: Function): (...args: any[]) => any; diff --git a/src/styles/toolboxes.pcss b/src/styles/toolboxes.pcss index 038bab3..f82d9b4 100644 --- a/src/styles/toolboxes.pcss +++ b/src/styles/toolboxes.pcss @@ -2,8 +2,8 @@ --toolbox-padding: 6px; --popover-margin: 30px; --toggler-click-zone-size: 30px; - --toggler-dots-color: #7B7E89; - --toggler-dots-color-hovered: #1D202B; + --toggler-dots-color: #7b7e89; + --toggler-dots-color-hovered: #1d202b; position: absolute; cursor: pointer; @@ -32,8 +32,6 @@ .tc-popover { position: absolute; - top: 0; - left: var(--popover-margin) } &__toggler { diff --git a/src/table.js b/src/table.js index 7dc31fd..a48001b 100644 --- a/src/table.js +++ b/src/table.js @@ -775,7 +775,11 @@ export default class Table { 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))` + style: { + left: `calc((100% - var(--cell-size)) / (${this.numberOfColumns} * 2) * (1 + (${column} - 1) * 2))` + }, + numberOfColumns: this.numberOfColumns, + currentColumn: column }; }); } @@ -789,7 +793,11 @@ export default class Table { const { height } = hoveredRowElement.getBoundingClientRect(); return { - top: `${Math.ceil(fromTopBorder + height / 2)}px` + style: { + top: `${Math.ceil(fromTopBorder + height / 2)}px` + }, + numberOfRows: this.numberOfRows, + currentRow: row }; }); } diff --git a/src/toolbox.js b/src/toolbox.js index 453cd14..04f5785 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -41,6 +41,11 @@ export default class Toolbox { this.popover = null; this.wrapper = this.createToolbox(); + + this.numberOfColumns = 0; + this.numberOfRows = 0; + this.currentColumn = 0; + this.currentRow = 0; } /** @@ -118,6 +123,35 @@ export default class Toolbox { * @returns {void} */ togglerClicked() { + // default: + // left: var(--popover-margin) + // top: 0 + let styles = {}; + + if (this.currentColumn > Math.ceil(this.numberOfColumns / 2)) { + styles.right = "var(--popover-margin)"; + styles.left = "auto"; + } else { + styles.left = "var(--popover-margin)"; + styles.right = "auto"; + } + + if (this.currentRow > Math.ceil(this.numberOfRows / 2)) { + styles.bottom = 0; + styles.top = 'auto'; + } else { + styles.top = 0; + styles.bottom = 'auto'; + } + + /** + * Set 'top','bottom' style + * Set 'left','right' style + */ + Object.entries(styles).forEach(([prop, value]) => { + this.popover.wrapper.style[prop] = value; + }); + if (this.popover.opened) { this.popover.close(); this.onClose(); @@ -135,14 +169,24 @@ export default class Toolbox { */ show(computePositionMethod) { const position = computePositionMethod(); - /** * Set 'top' or 'left' style */ - Object.entries(position).forEach(([prop, value]) => { + Object.entries(position.style).forEach(([prop, value]) => { this.wrapper.style[prop] = value; }); + console.log(position, this.cssModifier) + if (this.cssModifier == 'row') { + // row + this.numberOfRows = position.numberOfRows; + this.currentRow = position.currentRow; + } else if (this.cssModifier == 'column') { + // column + this.numberOfColumns = position.numberOfColumns; + this.currentColumn = position.currentColumn; + } + this.wrapper.classList.add(Toolbox.CSS.toolboxShowed); }