diff --git a/public/notes.css b/public/notes.css index a6dafb61..215caf21 100644 --- a/public/notes.css +++ b/public/notes.css @@ -1,5 +1,6 @@ :root { --default-sidebar-width: 25%; + --resizing-div-overflow: -10000px; } html { @@ -372,6 +373,7 @@ body.resizing-sidebar-locked-max { cursor: w-resize; } border: var(--table-border); border-spacing: 0; border-collapse: collapse; + overflow: hidden !important; } #content table td { @@ -461,26 +463,33 @@ body.with-control.resizing-img #content { .table-resizing-div { position: absolute; + z-index: 100; } -.table-resizing-div.active { +body:not(.resizing-table) .table-resizing-div:hover, .table-resizing-div.active { background: var(--resizing-line-color); } .table-column-resizing-div { - top: 0; + top: var(--resizing-div-overflow); + bottom: var(--resizing-div-overflow); right: 0; - bottom: 0; + width: var(--table-resizing-div-thickness); +} + +body:not(.resizing-table) .table-column-resizing-div { cursor: col-resize; - width: 4px; } .table-row-resizing-div { - left: 0; - right: 0; + left: var(--resizing-div-overflow); + right: var(--resizing-div-overflow); bottom: 0; + height: var(--table-resizing-div-thickness); +} + +body:not(.resizing-table) .table-row-resizing-div { cursor: row-resize; - height: 4px; } body.resizing-table-column { cursor: col-resize; } diff --git a/public/themes/dark.css b/public/themes/dark.css index 9a34a79a..ed24c72d 100644 --- a/public/themes/dark.css +++ b/public/themes/dark.css @@ -88,6 +88,7 @@ --table-border: 3px solid #454545; --table-td-border: 1px solid #222; --table-td-heading-background-color: #222; + --table-resizing-div-thickness: 4px; --resizing-line-color: #0000ff; diff --git a/public/themes/light.css b/public/themes/light.css index 069a93c8..c79b1d98 100644 --- a/public/themes/light.css +++ b/public/themes/light.css @@ -88,6 +88,7 @@ --table-border: 3px solid #171717; --table-td-border: 1px solid silver; --table-td-heading-background-color: #dddddd; + --table-resizing-div-thickness: 4px; --resizing-line-color: #0000ff; diff --git a/src/notes/content/__tests__/table.test.ts b/src/notes/content/__tests__/table.test.ts deleted file mode 100644 index 3deae2f6..00000000 --- a/src/notes/content/__tests__/table.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { JSDOM } from "jsdom"; -import { - getAllCellsInColumn, - MakeTableResizableProps, - makeTableResizable, - initTable, -} from "../table"; - -// A B C -// D E F -// G H I -const tableHtml = ` - - - - - - - - - - - - - - - -
ABC
DEF
GHI
`; - -const tableHtmlHoverE = ` - - - - - - - - - - - - - - - -
AB
C
D
E
F
GH
I
`; - -const tableHtmlHoverI = ` - - - - - - - - - - - - - - - -
ABC
DEF
G
H
I
`; - -const prepareDom = (tableHtmlToInsert: string): { dom: JSDOM, table: HTMLTableElement } => { - const dom = new JSDOM(); - dom.window.document.body.insertAdjacentHTML("afterbegin", tableHtmlToInsert); - const table = dom.window.document.body.children[0] as HTMLTableElement; - return { dom, table }; -}; - -test("getAllCellsInColumn() returns cells in a column", () => { - const { table } = prepareDom(tableHtml); - - expect(getAllCellsInColumn(table, 0).map((cell) => cell.innerHTML)).toEqual(["A", "D", "G"]); - expect(getAllCellsInColumn(table, 1).map((cell) => cell.innerHTML)).toEqual(["B", "E", "H"]); - expect(getAllCellsInColumn(table, 2).map((cell) => cell.innerHTML)).toEqual(["C", "F", "I"]); - expect(getAllCellsInColumn(table, 99).map((cell) => cell.innerHTML)).toEqual([]); -}); - -describe("initTable", () => { - let dom: JSDOM; - let table: HTMLTableElement; - let resizableProps: MakeTableResizableProps; - let makeTableResizableFunction: jest.Mock; - - beforeEach(() => { - const prepared = prepareDom(tableHtmlHoverE); - dom = prepared.dom; - table = prepared.table; - resizableProps = { - document: dom.window.document, - computedStyleFunction: dom.window.getComputedStyle, - table, - onResize: () => { }, - }; - makeTableResizableFunction = jest.fn(); - initTable({ - table, - resizableProps, - makeTableResizableFunction, - onContextMenu: () => {}, - }); - }); - - it("removes resizing divs if left over", () => { - expect(table.outerHTML).toBe(tableHtml); - }); - - it("calls resizable function", () => { - expect(makeTableResizableFunction).toHaveBeenCalledTimes(1); - expect(makeTableResizableFunction).toHaveBeenCalledWith(resizableProps); - }); -}); - -describe("makeTableResizable()", () => { - let dom: JSDOM; - let table: HTMLTableElement; - let onResize: jest.Mock; - - beforeEach(() => { - const prepared = prepareDom(tableHtml); - dom = prepared.dom; - table = prepared.table; - onResize = jest.fn(); - - makeTableResizable({ - document: dom.window.document, - computedStyleFunction: dom.window.getComputedStyle, - table, - onResize, - }); - }); - - it("keeps table markup unchanged", () => { - expect(table.outerHTML).toBe(tableHtml); - }); - - it("does NOT call onResize on init", () => { - expect(onResize).not.toHaveBeenCalled(); - }); - - it("adds resizing divs on mouseenter, removes them on mouseleave", () => { - const enterAndLeave = (cell: HTMLTableCellElement, expectedTableHtmlOnEnter: string) => { - cell.dispatchEvent(new dom.window.Event("mouseenter")); - expect(table.outerHTML).toBe(expectedTableHtmlOnEnter); - - cell.dispatchEvent(new dom.window.Event("mouseleave")); - expect(table.outerHTML).toBe(tableHtml); - }; - - enterAndLeave(table.rows[1].cells[1], tableHtmlHoverE); // enter and leave E - enterAndLeave(table.rows[2].cells[2], tableHtmlHoverI); // enter and leave I - }); -}); diff --git a/src/notes/content/init.ts b/src/notes/content/init.ts index b4dcc37e..2bf4ed5b 100644 --- a/src/notes/content/init.ts +++ b/src/notes/content/init.ts @@ -1,10 +1,13 @@ +import { dispatchNoteEdited } from "notes/events"; + import { initImgs } from "./img"; import { initTables } from "./table"; -import renderTableContextMenu from "./render-table-context-menu"; +import renderTableContextMenu from "./table/render/render-table-context-menu"; export default () => { initImgs(); initTables({ + onResize: dispatchNoteEdited, contextMenuRenderFunction: renderTableContextMenu, }); }; diff --git a/src/notes/content/table.ts b/src/notes/content/table.ts deleted file mode 100644 index 96c549fc..00000000 --- a/src/notes/content/table.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { dispatchNoteEdited } from "notes/events"; - -const TABLE_RESIZING_DIV_CLASSNAME = "table-resizing-div"; -const TABLE_COLUMN_RESIZING_DIV_CLASSNAME = "table-column-resizing-div"; -const TABLE_ROW_RESIZING_DIV_CLASSNAME = "table-row-resizing-div"; - -type ResizingType = "column" | "row"; -type ComputedStyleFunction = (elt: Element) => CSSStyleDeclaration; -type OnResizeCallback = () => void; - -export const getAllCellsInColumn = (table: HTMLTableElement, referenceCellIndex: number): HTMLTableCellElement[] => { - const cells: HTMLTableCellElement[] = []; - - for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) { - for (let cellIndex = 0; cellIndex < table.rows[rowIndex].cells.length; cellIndex += 1) { - if (cellIndex === referenceCellIndex) { - cells.push(table.rows[rowIndex].cells[cellIndex]); - } - } - } - - return cells; -}; - -const isResizingTable = (document: Document) => document.body.classList.contains("resizing-table"); - -interface CreateResizingDivProps { - document: Document - computedStyleFunction: ComputedStyleFunction - type: ResizingType - table: HTMLTableElement - cell: HTMLTableCellElement - allCells: HTMLTableCellElement[] - onResize: OnResizeCallback -} - -const createResizingDiv = ({ - document, - computedStyleFunction, - type, - table, - cell, - allCells, - onResize, -}: CreateResizingDivProps) => { - const resizingDiv = document.createElement("div"); - const specificClassname = type === "column" ? TABLE_COLUMN_RESIZING_DIV_CLASSNAME : TABLE_ROW_RESIZING_DIV_CLASSNAME; - resizingDiv.classList.add(TABLE_RESIZING_DIV_CLASSNAME, specificClassname); - - const isLast = allCells[allCells.length - 1] === cell; - if (!isLast) { - if (type === "column") { - resizingDiv.style.bottom = `-${computedStyleFunction(cell).borderBottomWidth}`; - } - - if (type === "row") { - resizingDiv.style.right = `-${computedStyleFunction(cell).borderRightWidth}`; - } - } - - const property = type === "column" ? "width" : "height"; - - const toggleActiveCells = (toggle: boolean) => - allCells.forEach((oneCell) => - oneCell.querySelector(`.${TABLE_RESIZING_DIV_CLASSNAME}.${specificClassname}`)?.classList.toggle("active", toggle)); - - const resizeAllCells = (value: number) => allCells.forEach((oneCell) => { - oneCell.style[property] = `${value}px`; - oneCell.style.wordBreak = "break-all"; - }); - - const resetSizeAllCells = () => allCells.forEach((oneCell) => { - oneCell.style[property] = ""; - oneCell.style.wordBreak = ""; - }); - - resizingDiv.addEventListener("mouseenter", () => { - if (isResizingTable(document) || cell.querySelector(`.${TABLE_RESIZING_DIV_CLASSNAME}.active`)) { - return; - } - toggleActiveCells(true); - }); - - const getAchor = (event: MouseEvent) => (type === "column" ? event.x : event.y); - - resizingDiv.addEventListener("mousedown", (mousedownEvent) => { - document.body.classList.add("resizing-table", `resizing-table-${type}`); - table.classList.add("locked"); - - let anchor = getAchor(mousedownEvent); - - const mousemoveListener = (mousemoveEvent: MouseEvent): void => { - const delta = (anchor - getAchor(mousemoveEvent)) * -1; - anchor = getAchor(mousemoveEvent); - - const currentValue = parseInt(computedStyleFunction(cell)[property], 10); - const newValue = currentValue + delta; - resizeAllCells(newValue); - }; - - document.addEventListener("mousemove", mousemoveListener); - - const mouseupListener = () => { - if (isResizingTable(document)) { - toggleActiveCells(false); - } - document.body.classList.remove("resizing-table", "resizing-table-column", "resizing-table-row"); - table.classList.remove("locked"); - document.removeEventListener("mousemove", mousemoveListener); - document.removeEventListener("mouseup", mouseupListener); - onResize(); - }; - - document.addEventListener("mouseup", mouseupListener); - }); - - resizingDiv.addEventListener("dblclick", () => { - resetSizeAllCells(); - onResize(); - }); - - resizingDiv.addEventListener("mouseleave", () => { - if (isResizingTable(document)) { - return; - } - - toggleActiveCells(false); - }); - - return resizingDiv; -}; - -export interface MakeTableResizableProps { - document: Document - computedStyleFunction: ComputedStyleFunction - table: HTMLTableElement - onResize: OnResizeCallback -} - -export const makeTableResizable = ({ - document, - computedStyleFunction, - table, - onResize, -}: MakeTableResizableProps): void => { - for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) { - for (let cellIndex = 0; cellIndex < table.rows[rowIndex].cells.length; cellIndex += 1) { - const cell = table.rows[rowIndex].cells[cellIndex]; - - const allCellsInColumn = getAllCellsInColumn(table, cellIndex); - const allCellsInRow = [...table.rows[rowIndex].cells]; - - cell.onmouseleave = () => { - if (isResizingTable(document)) { - return; - } - [ - ...allCellsInColumn, - ...allCellsInRow, - ].forEach((oneCell) => oneCell.querySelectorAll(`.${TABLE_RESIZING_DIV_CLASSNAME}`).forEach((div) => div.remove())); - }; - - cell.onmouseenter = () => { - if (isResizingTable(document) || cell.querySelector(`.${TABLE_RESIZING_DIV_CLASSNAME}`)) { - return; - } - - allCellsInColumn.forEach((oneCell) => oneCell.appendChild(createResizingDiv({ - document, - computedStyleFunction, - type: "column", - table, - cell: oneCell, - allCells: allCellsInColumn, - onResize, - }))); - - allCellsInRow.forEach((oneCell) => oneCell.appendChild(createResizingDiv({ - document, - computedStyleFunction, - type: "row", - table, - cell: oneCell, - allCells: allCellsInRow, - onResize, - }))); - }; - } - } -}; - -interface InitTableProps { - table: HTMLTableElement - resizableProps: MakeTableResizableProps - makeTableResizableFunction: (props: MakeTableResizableProps) => void - onContextMenu: (event: MouseEvent) => void -} - -export const initTable = ({ - table, resizableProps, makeTableResizableFunction, onContextMenu, -}: InitTableProps): void => { - const cells = resizableProps.table.querySelectorAll("td"); - cells.forEach((oneCell) => { - oneCell.onmouseleave = null; - oneCell.onmouseenter = null; - oneCell.querySelectorAll(`.${TABLE_RESIZING_DIV_CLASSNAME}`).forEach((div) => div.remove()); - }); - - makeTableResizableFunction(resizableProps); - table.oncontextmenu = onContextMenu; -}; - -interface InitTables { - contextMenuRenderFunction: (table: HTMLTableElement, event: MouseEvent) => void -} - -export const initTables = ({ - contextMenuRenderFunction, -}: InitTables): void => { - const tables = document.querySelectorAll("body > #content-container > #content table"); - tables.forEach((table) => initTable({ - table, - resizableProps: { - document, - computedStyleFunction: window.getComputedStyle, - table, - onResize: dispatchNoteEdited, - }, - makeTableResizableFunction: makeTableResizable, - onContextMenu: (event) => { - if (!document.body.classList.contains("with-control")) { - return; - } - - event.preventDefault(); - contextMenuRenderFunction(table, event); - }, - })); -}; diff --git a/src/notes/content/table/__tests__/cells.test.ts b/src/notes/content/table/__tests__/cells.test.ts new file mode 100644 index 00000000..c6905187 --- /dev/null +++ b/src/notes/content/table/__tests__/cells.test.ts @@ -0,0 +1,90 @@ +import { prepareDom, tableHtml } from "./fixtures"; +import { + getCellsInRow, + getCellsInColumn, + + ResizeProperty, + resizeCells, + resetCellsSize, +} from "../cells"; + +const mapFn = (cell: HTMLTableCellElement) => cell.innerHTML; + +test("getCellsInRow() returns cells in a row", () => { + const { table } = prepareDom(tableHtml); + + expect(getCellsInRow(table, 0).map(mapFn)).toEqual(["A", "B", "C"]); + expect(getCellsInRow(table, 1).map(mapFn)).toEqual(["D", "E", "F"]); + expect(getCellsInRow(table, 2).map(mapFn)).toEqual(["G", "H", "I"]); + + expect(getCellsInRow(table, -1).map(mapFn)).toEqual([]); + expect(getCellsInRow(table, 99).map(mapFn)).toEqual([]); +}); + +test("getCellsInColumn() returns cells in a column", () => { + const { table } = prepareDom(tableHtml); + + expect(getCellsInColumn(table, 0).map(mapFn)).toEqual(["A", "D", "G"]); + expect(getCellsInColumn(table, 1).map(mapFn)).toEqual(["B", "E", "H"]); + expect(getCellsInColumn(table, 2).map(mapFn)).toEqual(["C", "F", "I"]); + + expect(getCellsInColumn(table, -1).map(mapFn)).toEqual([]); + expect(getCellsInColumn(table, 99).map(mapFn)).toEqual([]); +}); + +const expectCellsProperty = (cells: HTMLTableCellElement[], property: ResizeProperty, value: string) => ( + cells.forEach((cell) => expect(cell.style.getPropertyValue(property)).toBe(value)) +); + +test("resizeCells() sets cells width or height", () => { + const { table } = prepareDom(tableHtml); + + const columnCells = getCellsInColumn(table, 1); + resizeCells(columnCells, "width", 400); + expectCellsProperty(columnCells, "width", "400px"); + + const rowCells = getCellsInRow(table, 1); + resizeCells(rowCells, "height", 200); + expectCellsProperty(rowCells, "height", "200px"); +}); + +describe("resetCellsSize()", () => { + let columnCells: HTMLTableCellElement[]; + let rowCells: HTMLTableCellElement[]; + + beforeEach(() => { + const { table } = prepareDom(tableHtml); + + columnCells = getCellsInColumn(table, 1); + resizeCells(columnCells, "width", 400); + expectCellsProperty(columnCells, "width", "400px"); + + rowCells = getCellsInRow(table, 1); + resizeCells(rowCells, "height", 200); + expectCellsProperty(rowCells, "height", "200px"); + }); + + it("resets width only", () => { + resetCellsSize(columnCells, "width"); + expectCellsProperty(columnCells, "width", ""); + + // height should remain unchanged + expectCellsProperty(rowCells, "height", "200px"); + }); + + it("resets height only", () => { + resetCellsSize(rowCells, "height"); + expectCellsProperty(rowCells, "height", ""); + + // width should remain unchanged + expectCellsProperty(columnCells, "width", "400px"); + }); + + it("resets width and height", () => { + resetCellsSize(columnCells, "width"); + resetCellsSize(rowCells, "height"); + + expectCellsProperty(columnCells, "width", ""); + expectCellsProperty(rowCells, "height", ""); + }); +}); diff --git a/src/notes/content/table/__tests__/fixtures.ts b/src/notes/content/table/__tests__/fixtures.ts new file mode 100644 index 00000000..d150edd3 --- /dev/null +++ b/src/notes/content/table/__tests__/fixtures.ts @@ -0,0 +1,75 @@ +import { JSDOM } from "jsdom"; + +// A B C +// D E F +// G H I +export const tableHtml = ` + + + + + + + + + + + + + + + +
ABC
DEF
GHI
`; + +const tableRowResizingDiv = ( + '
' +); + +const tableColumnResizingDiv = ( + '
' +); + +const tableRowColumnResizingDivs = `${tableRowResizingDiv}${tableColumnResizingDiv}`; + +export const tableHtmlHoverE = ` + + + + + + + + + + + + + + + +
ABC
DE${tableRowColumnResizingDivs}F
GHI
`; + +export const tableHtmlHoverI = ` + + + + + + + + + + + + + + + +
ABC
DEF
GHI${tableRowColumnResizingDivs}
`; + +export const prepareDom = (tableHtmlToInsert: string): { dom: JSDOM, table: HTMLTableElement } => { + const dom = new JSDOM(); + dom.window.document.body.insertAdjacentHTML("afterbegin", tableHtmlToInsert); + const table = dom.window.document.body.children[0] as HTMLTableElement; + return { dom, table }; +}; diff --git a/src/notes/content/table/__tests__/make-table-resizable.test.ts b/src/notes/content/table/__tests__/make-table-resizable.test.ts new file mode 100644 index 00000000..b0802825 --- /dev/null +++ b/src/notes/content/table/__tests__/make-table-resizable.test.ts @@ -0,0 +1,60 @@ +import { JSDOM } from "jsdom"; +import makeTableResizable from "../make-table-resizable"; +import { + tableHtml, + tableHtmlHoverE, + tableHtmlHoverI, + prepareDom, +} from "./fixtures"; + +describe("makeTableResizable()", () => { + let dom: JSDOM; + let table: HTMLTableElement; + let onResize: jest.Mock; + + const prepareTable = (html: string) => { + const prepared = prepareDom(html); + dom = prepared.dom; + table = prepared.table; + onResize = jest.fn(); + + makeTableResizable({ + document: dom.window.document, + table, + onResize: () => {}, + }); + }; + + it("keeps table markup unchanged", () => { + prepareTable(tableHtml); + expect(table.outerHTML).toBe(tableHtml); + }); + + it("does NOT call onResize on init", () => { + prepareTable(tableHtml); + expect(onResize).not.toHaveBeenCalled(); + }); + + const enterAndLeave = ( + cell: HTMLTableCellElement, + expectedTableHtmlOnEnter: string, + expectedTableHtmlOnLeave: string, + ) => { + cell.dispatchEvent(new dom.window.Event("mouseenter")); + expect(table.outerHTML).toBe(expectedTableHtmlOnEnter); + + cell.dispatchEvent(new dom.window.Event("mouseleave")); + expect(table.outerHTML).toBe(expectedTableHtmlOnLeave); + }; + + it("adds resizing divs on mouseenter, removes them on mouseleave", () => { + prepareTable(tableHtml); + enterAndLeave(table.rows[1].cells[1], tableHtmlHoverE, tableHtml); + enterAndLeave(table.rows[2].cells[2], tableHtmlHoverI, tableHtml); + }); + + it("removes any leftover resizing divs", () => { + prepareTable(tableHtmlHoverE); + enterAndLeave(table.rows[2].cells[2], tableHtmlHoverI, tableHtml); + }); +}); diff --git a/src/notes/content/table/cells.ts b/src/notes/content/table/cells.ts new file mode 100644 index 00000000..205bbcec --- /dev/null +++ b/src/notes/content/table/cells.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-param-reassign */ + +export const getCellsInRow = (table: HTMLTableElement, row: number): HTMLTableCellElement[] => { + const theRow = table.rows[row] as HTMLTableRowElement | undefined; + return theRow ? [...theRow.cells] : []; +}; + +export const getCellsInColumn = (table: HTMLTableElement, column: number): HTMLTableCellElement[] => { + const cells: HTMLTableCellElement[] = []; + + for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) { + for (let cellIndex = 0; cellIndex < table.rows[rowIndex].cells.length; cellIndex += 1) { + if (cellIndex === column) { + cells.push(table.rows[rowIndex].cells[cellIndex]); + } + } + } + + return cells; +}; + +export type ResizeProperty = "width" | "height"; + +const setCellsProperty = (cells: HTMLTableCellElement[], property: ResizeProperty, value: string) => { + cells.forEach((cell) => { + cell.style[property] = value; + }); +}; + +export const resizeCells = (cells: HTMLTableCellElement[], property: ResizeProperty, value: number) => { + setCellsProperty(cells, property, `${value}px`); +}; + +export const resetCellsSize = (cells: HTMLTableCellElement[], property: ResizeProperty) => { + setCellsProperty(cells, property, ""); +}; diff --git a/src/notes/content/table/index.ts b/src/notes/content/table/index.ts new file mode 100644 index 00000000..beb4c00c --- /dev/null +++ b/src/notes/content/table/index.ts @@ -0,0 +1,24 @@ +import makeTableHavingContextMenu, { ContextMenuRenderFunction } from "./make-table-having-context-menu"; +import makeTableResizable, { OnResizeCallback } from "./make-table-resizable"; + +interface InitTables { + onResize: OnResizeCallback + contextMenuRenderFunction: ContextMenuRenderFunction +} + +// eslint-disable-next-line import/prefer-default-export +export const initTables = ({ + onResize, + contextMenuRenderFunction, +}: InitTables): void => { + const tables = document.querySelectorAll("body > #content-container > #content table"); + tables.forEach((table) => { + makeTableHavingContextMenu(table, contextMenuRenderFunction); + + makeTableResizable({ + document, + table, + onResize, + }); + }); +}; diff --git a/src/notes/content/table/make-table-having-context-menu.ts b/src/notes/content/table/make-table-having-context-menu.ts new file mode 100644 index 00000000..3e5a1f36 --- /dev/null +++ b/src/notes/content/table/make-table-having-context-menu.ts @@ -0,0 +1,11 @@ +export type ContextMenuRenderFunction = (table: HTMLTableElement, event: MouseEvent) => void; + +export default (table: HTMLTableElement, renderContextMenu: ContextMenuRenderFunction) => { + // eslint-disable-next-line no-param-reassign + table.oncontextmenu = (event) => { + if (document.body.classList.contains("with-control")) { + event.preventDefault(); + renderContextMenu(table, event); + } + }; +}; diff --git a/src/notes/content/table/make-table-resizable.ts b/src/notes/content/table/make-table-resizable.ts new file mode 100644 index 00000000..5548801b --- /dev/null +++ b/src/notes/content/table/make-table-resizable.ts @@ -0,0 +1,127 @@ +import * as cells from "./cells"; + +const TABLE_RESIZING_DIV_CLASSNAME = "table-resizing-div"; +const TABLE_COLUMN_RESIZING_DIV_CLASSNAME = "table-column-resizing-div"; +const TABLE_ROW_RESIZING_DIV_CLASSNAME = "table-row-resizing-div"; + +type ResizeType = "column" | "row"; +export type OnResizeCallback = () => void; + +interface MakeTableResizableProps { + document: Document + table: HTMLTableElement + onResize: OnResizeCallback +} + +interface CreateResizingDivProps extends MakeTableResizableProps { + type: ResizeType + cell: HTMLTableCellElement + allCells: HTMLTableCellElement[] +} + +const isResizingTable = (document: Document) => document.body.classList.contains("resizing-table"); + +const createResizingDiv = ({ + document, + table, + onResize, + + type, + cell, + allCells, +}: CreateResizingDivProps) => { + const resizingDiv = document.createElement("div"); + const specificClassname = type === "column" ? TABLE_COLUMN_RESIZING_DIV_CLASSNAME : TABLE_ROW_RESIZING_DIV_CLASSNAME; + resizingDiv.classList.add(TABLE_RESIZING_DIV_CLASSNAME, specificClassname); + + const property: cells.ResizeProperty = type === "column" ? "width" : "height"; + + const getAnchor = (event: MouseEvent) => (type === "column" ? event.x : event.y); + + resizingDiv.onmousedown = (mousedownEvent: MouseEvent) => { + resizingDiv.classList.add("active"); + document.body.classList.add("resizing-table", `resizing-table-${type}`); + table.classList.add("locked"); + + let anchor = getAnchor(mousedownEvent); + + const mousemoveListener = (mousemoveEvent: MouseEvent): void => { + const delta = (anchor - getAnchor(mousemoveEvent)) * -1; + anchor = getAnchor(mousemoveEvent); + + const currentValue = parseInt(window.getComputedStyle(cell)[property], 10); + const newValue = currentValue + delta; + cells.resizeCells(allCells, property, newValue); + }; + + document.addEventListener("mousemove", mousemoveListener); + + const mouseupListener = () => { + document.body.classList.remove( + "resizing-table", + "resizing-table-column", + "resizing-table-row", + ); + resizingDiv.classList.remove("active"); + table.classList.remove("locked"); + document.removeEventListener("mousemove", mousemoveListener); + document.removeEventListener("mouseup", mouseupListener); + onResize(); + }; + + document.addEventListener("mouseup", mouseupListener); + }; + + resizingDiv.ondblclick = () => { + cells.resetCellsSize(allCells, property); + onResize(); + }; + + return resizingDiv; +}; + +const removeResizingDivs = (table: HTMLTableElement) => ( + table.querySelectorAll(`.${TABLE_RESIZING_DIV_CLASSNAME}`).forEach((div) => div.remove()) +); + +export default (props: MakeTableResizableProps): void => { + const { document, table } = props; + for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) { + for (let cellIndex = 0; cellIndex < table.rows[rowIndex].cells.length; cellIndex += 1) { + const cell = table.rows[rowIndex].cells[cellIndex]; + + cell.onmouseleave = () => { + if (isResizingTable(document)) { + return; + } + + removeResizingDivs(table); + }; + + const allCellsInRow = cells.getCellsInRow(table, rowIndex); + const allCellsInColumn = cells.getCellsInColumn(table, cellIndex); + + cell.onmouseenter = () => { + if (isResizingTable(document)) { + return; + } + + removeResizingDivs(table); + + cell.appendChild(createResizingDiv({ + ...props, + type: "row", + cell, + allCells: allCellsInRow, + })); + + cell.appendChild(createResizingDiv({ + ...props, + type: "column", + cell, + allCells: allCellsInColumn, + })); + }; + } + } +}; diff --git a/src/notes/content/render-table-context-menu.tsx b/src/notes/content/table/render/render-table-context-menu.tsx similarity index 100% rename from src/notes/content/render-table-context-menu.tsx rename to src/notes/content/table/render/render-table-context-menu.tsx