Javascript-форум (https://javascript.ru/forum/)
-   (X)HTML/CSS (https://javascript.ru/forum/xhtml-html-css/)
-   -   Редактор и html-таблица (https://javascript.ru/forum/xhtml-html-css/75679-redaktor-i-html-tablica.html)

MC-XOBAHCK 29.10.2018 01:00

Редактор и html-таблица
 
Здравствуйте!
Расширяю функционал редактора, чтоб в модальном окне удобно заполнять таблицы.
Основное получилось быстро написать, а вот на целый день застрял с объединением ячеек таблицы.

Не знаю как выделять ячейки таблицы, чтоб мышкой с нажатой левой клавишей можно было выделять ячейки, а потом нажав кнопку "Объединить ячейки" они объединились.

Накидал вот такое, знаю что не правильно. У меня пока нет идей как это сделать. Не могу выделить ячейки, чтоб дойти до того какой атрибут ставить colspan или rowspan.
<style>td.cell__active {background:#7fffd4}table, th, td{min-width:50px;border:solid 1px #ccc} th, td{min-width:50px;min-height:15px}button{margin-top:10px}</style>


<table contenteditable="true">
	<tbody>
		<tr><td></td><td></td><td></td><td></td></tr>
		<tr><td></td><td></td><td></td><td></td></tr>
		<tr><td></td><td></td><td></td><td></td></tr>
		<tr><td></td><td></td><td></td><td></td></tr>
	</tbody>
</table>

<button id="margeTd">Объединить ячейки</button>


<script>
  let table = document.querySelector('table');

  // Добавить класс ячейкам таблицы
  table.addEventListener('mouseover', function(e) {
      if (e.target.tagName == 'TD') {
          e.target.className = 'cell__active';
      }
  });

  // Объединение ячеек таблицы
  document.querySelector('#margeTd').addEventListener('click', function () {
      let marg = table.querySelectorAll('td.cell__active');

      if (marg.length > 1) {
          for (let i = 0; i < marg.length; i++) {
              i == 0 ? marg[i].setAttribute('colspan', marg.length, 0) : marg[i].remove();
          }
      }
  });
</script>

Может кто накинет идею, как мне выделить ячейки. Тестово поставил - мышкой поводить добавляет класс и по классу объединяет. Но это неправильно.

laimas 29.10.2018 03:14

Цитата:

Сообщение от MC-XOBAHCK
выделять ячейки, а потом нажав кнопку "Объединить ячейки" они объединились

Проще выделить строку, хотя можно и несколько ячеек в строке так как посчитать будет не сложно, а вот со столбцами не так просто.

MC-XOBAHCK 29.10.2018 04:12

Цитата:

Сообщение от laimas (Сообщение 497312)
Проще выделить строку, хотя можно и несколько ячеек в строке так как посчитать будет не сложно, а вот со столбцами не так просто.

В HTML можно объединять соседние ячейки. Точнее не объединять (объединять нельзя), а задавать размеры для отдельных ячеек при помощи атрибутов colspan или rowspan. Объединять можно уже при помощи js.

Проблема в том, что у меня нет идеи как выделять ячейки, при этом нужно выделять прямоугольной областью не вылазя за пределы tbody, thead, tfooter.

При объединении по горизонтали моё решение подходит, а вот в высоту пока только мысли проставить атрибуты с номерами строк или может по родителю ячеек как то можно.
Но я не могу выделить ячейки, поэтому в высоту пока не развиваю идею.

laimas 29.10.2018 05:58

Цитата:

Сообщение от MC-XOBAHCK
объединять нельзя

Можно.

Цитата:

Сообщение от MC-XOBAHCK
задавать размеры для отдельных ячеек при помощи атрибутов colspan или rowspan

Это как раз и есть указание, что данная ячейка равна N ячеек в строке или столбце, то есть в данной ячейке объединены N ячеек.

Откройте любое ПО, которое может создавать таблицы, объединять ячейки, тот же Word. Сохраните код как html и посмотрите структуру таблицы с объединенными ячейками, в особенности если в строке есть объединение в столбце. Это даст понимание того, что при объединении ячеек столбца не все так просто будет как с объединением в строке.

рони 29.10.2018 09:17

выделятор блоков и обьединение ячеек
 
MC-XOBAHCK,
макет, обьединение ячеек(mergeTd) расчитано на первый клик!!!(требует доработки).
выделить ячейки и нажать кнопку
<!DOCTYPE html>

<html>
<head>
  <title>Untitled</title>
  <meta charset="utf-8">
  <style type="text/css">
   table{
           height: 250px;
           width: 250px;
           border-collapse: separate;
       }
   td{
      border: 1px #0000CD solid;
      background-color: #800080;
   }
   td.sel{
    opacity:.6;
  }

  #line{
    position:absolute;
    width:0px;
    height:0px;
    background:rgba(0,90,255,0.25);
    border:1px solid rgba(0,114,255,0.5);
    box-sizing:border-box;
  }
  </style>

    <script>
window.addEventListener("DOMContentLoaded", function() {
    var target = document.createElement("div"),
        x, y;
    target.id = "line";
    document.addEventListener("mousedown", function(e) {
        if (e.which > 1) return;
        x = e.pageX;
        y = e.pageY;
        document.body.appendChild(target);
        selectBlock();
        document.addEventListener("mousemove", move, false);
        return false
    }, false);

    function move(e) {
        e.preventDefault();
        target.style.width = Math.abs(e.pageX - x) + "px";
        target.style.height = Math.abs(e.pageY - y) + "px";
        target.style.left = (e.pageX < x ? e.pageX : x) + "px";
        target.style.top =
            (e.pageY < y ? e.pageY : y) + "px";
        selectBlock(true)
    }
    document.addEventListener("mouseup", function() {
        target.style.width = "0px";
        target.style.height = "0px";
        document.removeEventListener("mousemove", move);
        target.parentNode && document.body.removeChild(target)
    });

    function collisionDetection(x1, y1, w1, h1, x2, y2, w2, h2) {
        return x1 < x2 + w2 && y1 < y2 + h2 && x1 + w1 > x2 && y1 + h1 > y2
    }

    function coordinate(el) {
        var pos = el.getBoundingClientRect(),
            top = pos.top,
            left = pos.left,
            right = pos.right,
            bottom = pos.bottom,
            height = bottom - top,
            width = right - left;
        return [left,
            top, width, height
        ]
    }

    function selectBlock(add) {
        [].forEach.call(document.querySelectorAll("td"), function(el) {
            var data = coordinate(el).concat(coordinate(target)),
                modify = add && collisionDetection.apply(null, data) ? "add" : "remove";
            el.classList[modify]("sel")
        })
    }

    function mergeTd(event) {
        var tds = document.querySelectorAll("td.sel"),
            len = tds.length,
            colspan = 1,
            parent;
        if (len) {
            [].forEach.call(tds, function(el, i) {
                if (i) {
                    var elParent = el.parentNode;
                    parent == elParent && colspan++;
                    elParent.removeChild(el)
                } else parent = el.parentNode
            });
            tds[0].colSpan = colspan;
            tds[0].rowSpan = len / colspan
        }
    }
    document.querySelector(".merge").addEventListener("mousedown", mergeTd)
});
</script>
</head>

<body>
<table>
    <tbody>
        <tr><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td></tr>
    </tbody>
</table>
<input name="" type="button" value="go" class="merge">


</body>
</html>

MC-XOBAHCK 29.10.2018 18:03

рони, Спасибо вам!
Вижу выделение ячеек происходит так как нужно, и вижу баг, что вы указали как требует доработки.

Я с утра приболел, вот начинаю приходить в себя и разбирать код.
Я код понимаю, но не весь. Если можно - есть вопросы.

В строке 33 создаётся элемент - createElement. Это прямоугольник области выделения мышью?

37 строка if (e.which > 1) return; - я правильно понял - это условие останавливает выполнение функции если код клавиши > 1 ? То есть вместе с событием mousedown это и есть нажатая левая кнопка мыши.

рони 29.10.2018 18:05

MC-XOBAHCK,
да и да :)

рони 29.10.2018 18:13

MC-XOBAHCK,
Цитата:

Сообщение от ruslan_mart
рони, по правой кнопке мыши выделение тоже срабатывает и при отпускании не убирается. Нужно сделать дополнительную проверку event.which

https://javascript.ru/forum/misc/519...tml#post343149

MC-XOBAHCK 29.10.2018 21:52

Цитата:

Сообщение от рони (Сообщение 497359)
да и да

Ещё раз большое вам СПАСИБИЩЕ! Я разобрался с вашим кодом.

Получается, что выделение ячеек вы реализовали, а нужно доработать последнюю функцию - вот эту:
function mergeTd(event) {
    var tds = document.querySelectorAll("td.sel"),
        len = tds.length,
        colspan = 1,
        parent;

    if (len) {
        [].forEach.call(tds, function (el, i) {
            if (i) {
                var elParent = el.parentNode;
                parent == elParent && colspan++;
                elParent.removeChild(el)
            } else parent = el.parentNode
        });
        tds[0].colSpan = colspan;
        tds[0].rowSpan = len / colspan
    }
}
document.querySelector("#margeTd").addEventListener("mousedown", mergeTd);


1. У функции принимающий параметр event можно убрать или лучше оставить? Я понимаю так, что он вообще не нужен и никакие события в функции не отслеживаются. Работа происходит только с массивом выделенных ячеек.

2. Последние две строчки функции
tds[0].colSpan = colspan;
tds[0].rowSpan = len / colspan

Это тоже самое что:
tds[0].setAttribute('colspan', colspan, 0);
tds[0].setAttribute('rowspan', len / colspan, 0);

?

3. Идея как считать высоту и ширину для объединённой ячейки у меня появилась. Буду пробовать. Нужно немного посидеть поисследовать. Думаю у меня должно получиться, основная логика всё таки уже написана.

4. А вот что мне сложно будет доработать, это сделать правильное выделение. Вот только что обнаружил возможность вот такого выделения ячеек:
<style>table, th, td{min-width:50px;min-height:100px;border:solid 1px #ccc} th, td{min-width:50px;min-height:15px}</style>

<table>
    <tr><td></td><td></td><td></td></tr>
    <tr><td></td><td></td><td rowspan="2" style="background:red"></td></tr>
    <tr><td></td><td style="background:red"></td></tr>
</table>

вот же блин...

рони 29.10.2018 22:29

MC-XOBAHCK,
да
1.можно убрать
2.свойство и атрибут, в данном случае изменение свойства равно назначению атрибута, такое не всегда, например свойство value можно изменить, атрибут останется прежним. подробнее тут
https://learn.javascript.ru/attribut...nachenie-value

3. удачи!!!
4. не знаю, выбор за вами, смотря какой нужен результат.

MC-XOBAHCK 29.10.2018 23:44

Я понял.
Цитата:

Сообщение от рони
4. не знаю, выбор за вами, смотря какой нужен результат.

Сейчас получается есть варианты выделения ячеек лесенками вверх, вниз, влево, вправо, когда в таблице уже есть объединённая ячейка/и. По идее нужно чтоб ячейки выделяло выравнивая с объединёнными ячейками, по фигуре прямоугольника без вариантов лесенок.
Не думаю что у меня получится с этим разобраться. Буду что то думать.

Спасибо что помогли.

MC-XOBAHCK 30.10.2018 14:06

Доработал расчёт размеров объединённой ячейки:
<style>table{min-height: 250px;border-collapse: separate}th, td{min-width:50px;min-height:15px}td{border: 1px #0000CD solid;background-color: #800080} td.sel{opacity:.6} #line{position:absolute;width:0px;height:0px;background:rgba(0,90,255,0.25);border:1px solid rgba(0,114,255,0.5);box-sizing:border-box}</style>

<script>
window.addEventListener("DOMContentLoaded", function() {
    var target = document.createElement("div"),
        x, y;
    target.id = "line";
    document.addEventListener("mousedown", function(e) {
        if (e.which > 1) return;
        x = e.pageX;
        y = e.pageY;
        document.body.appendChild(target);
        selectBlock();
        document.addEventListener("mousemove", move, false);
        return false
    }, false);

    function move(e) {
        e.preventDefault();
        target.style.width = Math.abs(e.pageX - x) + "px";
        target.style.height = Math.abs(e.pageY - y) + "px";
        target.style.left = (e.pageX < x ? e.pageX : x) + "px";
        target.style.top =
            (e.pageY < y ? e.pageY : y) + "px";
        selectBlock(true)
    }
    document.addEventListener("mouseup", function() {
        target.style.width = "0px";
        target.style.height = "0px";
        document.removeEventListener("mousemove", move);
        target.parentNode && document.body.removeChild(target)
    });

    function collisionDetection(x1, y1, w1, h1, x2, y2, w2, h2) {
        return x1 < x2 + w2 && y1 < y2 + h2 && x1 + w1 > x2 && y1 + h1 > y2
    }

    function coordinate(el) {
        var pos = el.getBoundingClientRect(),
            top = pos.top,
            left = pos.left,
            right = pos.right,
            bottom = pos.bottom,
            height = bottom - top,
            width = right - left;
        return [left, top, width, height]
    }

    function selectBlock(add) {
        [].forEach.call(document.querySelectorAll("td"), function(el) {
            var data = coordinate(el).concat(coordinate(target)),
                modify = add && collisionDetection.apply(null, data) ? "add" : "remove";
            el.classList[modify]("sel")
        })
    }

    function mergeTd() {
        var tds = document.querySelectorAll('td.sel'),
            len = tds.length,
            parent;

        let col = [[]];
        let row = 0;
        let startArr = 0;

        if (len) {
            [].forEach.call(tds, function (el, i) {
                var elParent = el.parentNode;

                if (i == 0) {
                    parent = el.parentNode;
                    el.hasAttribute('rowspan') ? row = row + +el.getAttribute('rowspan') : row++;
                }
                else if (parent == elParent) {
                    
                }
                else {
                    parent = el.parentNode;
                    startArr++;
                    col[startArr] = [];
                    el.hasAttribute('rowspan') ? row = row + +el.getAttribute('rowspan') : row++;
                }
                
                el.hasAttribute('colspan') ? col[startArr].push(+el.getAttribute('colspan')) : col[startArr].push(1);
            });

            // console.log(col);    // массив размеров colspan


            let col1 = (function() {
                let n = [];
                for (let i = 0; i < col.length; i++) {
                    n[i] = col[i].reduce((a, b) => a + b);
                }
                return Math.max.apply(null, n);
            })();

            
            for (let i = 0; i < tds.length; i++) {
                if (i == 0) {
                    col1 > 1 ? tds[i].setAttribute('colspan', col1, 0) : 0;
                    row > 1 ? tds[i].setAttribute('rowspan', row, 0) : 0;
                }
                else tds[i].remove();
            }
        }
    };
    document.querySelector(".merge").addEventListener("mousedown", mergeTd)
});
</script>


<table>
    <tbody>
        <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
        <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
    </tbody>
</table>
<input type="button" value="go" class="merge">


Расчёт colspan можно делать гораздо проще и короче. Знаю как, но пока оставил так чтобы можно было вывести в консоль и посмотреть размеры colspan выделенных ячеек в виде многомерного массива.

Есть баг при выделении ячеек "лесенкой". Как доработать этот момент, пока не знаю.
Есть мысля увеличивать размер создаваемого прямоугольника выделения, до крайних координат уже выделенных ячеек min top, min left, max reigth, max bottom.
При этом нужно и про уменьшение области выделения не забывать.

Какая то длинная портянка в условии нарисовуется... осталось "вырисовать" куда и о чём написать это условие : )


Если можно - ВОПРОС по таблице
У меня у таблицы стоит атрибут contenteditable="true", то есть можно ячейки редактировать.
Можно ли как то определить в какой ячейке установлен курсор? Может добавляется к ячейке фокус или ещё что?

Malleys 02.11.2018 11:37

Вложений: 2
Цитата:

Сообщение от MC-XOBAHCK
Идея как считать высоту и ширину для объединённой ячейки у меня появилась. Буду пробовать.

MC-XOBAHCK, рони, возможно вы захотите проверить, как работает объединение ячеек при следующих условиях.

1. Пусть дана таблица 5 на 5 ячеек. Некоторые ячейки уже объединены: (Это не сложно)
Вложение 4022
Если теперь объединить ячейки A и B, то таблица должна по прежнему остаться прямоугольной таблицей!

2. Дана таблица:
Вложение 4023
Объединив любую ячейку с соседней, у вас по прежнему должна остаться прямоугольная таблица!

Цитата:

Сообщение от MC-XOBAHCK
сделать правильное выделение

По сути у вас может быть две области выделения. Одна показывает область выделенную при помощи мыши (исходное выделение), другая вычисляется на основе первой: она показывает какие ячейки таблицы будут объединены (вычисленное выделение). Поскольку ячейка таблицы прямоугольная, то и выделенные ячейки представляют из себя прямоугольную область. Отсюда следует, что вам нужно найти такую область, которая больше, чем исходное выделение и равна наименьшей области среди всевозможных выделении, которые целиком вмещают в себя любые ячейки таблицы (т. е. граница вычисленного выделения не проходит по грани какой-либо ячейки, но только по ребрам ячеек, и содержит в себе исходное выделение)

Цитата:

Сообщение от рони
смотря какой нужен результат.

Поскольку ячейка таблицы прямоугольная, то и выделенные ячейки (из которых можно получить объединённую ячейку) представляют из себя прямоугольную область.

Вот мой вариант. Я ввёл вспомогательный класс Box, который представляет прямоугольные области исходного или вычисленного выделения. После того, как на основе области исходного выделения была найдена область вычисленного выделения, все ячейки кроме одной удаляются, а у оставшейся устанавливаются соответствующие атрибуты colspan и rowspan таким образом, что она целиком заполняет область вычисленного выделения.
<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width">
	<style type="text/css">
		
		table {
			height: 500px;
			width: 500px;
			border-collapse: collapse;
			position: relative;
			user-select: none;
			cursor: crosshair;
			table-layout: fixed;
			font: 1em Helvetica Neue, Segoe UI, Roboto, Ubuntu, sans-serif;
			text-align: center;
		}

		td {
			border: 1px gray solid;
		}
		
	</style>
	<script>
		
class Box {
	constructor(x = 0, y = 0, width = 0, height = 0) {
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
	}

	contains({ x, y, width, height }) {				
		return (
			x > this.x &&
			y > this.y &&
			(x + width) < (this.x + this.width) &&
			(y + height) < (this.y + this.height)
		);
	}

	overlaps({ x, y, width, height }) {
		return (
			1.001 * Math.max(x, this.x) < 0.998 * Math.min(x + width, this.x + this.width) &&
			1.001 * Math.max(y, this.y) < 0.998 * Math.min(y + height, this.y + this.height)
		);
	}

	equals({ x, y, width, height }) {
		return (
			x === this.x &&
			y === this.y &&
			width === this.width &&
			height === this.height
		);
	}

	add({x, y, width, height}) {
		return new Box(
			Math.min(x, this.x),
			Math.min(y, this.y),
			Math.max(x + width, this.x + this.width) - Math.min(x, this.x),
			Math.max(y + height, this.y + this.height) - Math.min(y, this.y)
		);
	}

	relativeTo({ x, y }) {
		return new Box(
			this.x - x,
			this.y - y,
			this.width,
			this.height
		);
	}

	relativeToElement(element) {
		return this.relativeTo(element.getBoundingClientRect());
	}

	static from({ x = 0, y = 0, width = 0, height = 0 } = {}) {
		return new Box(x, y, width, height);
	}
}

class EditableTable extends HTMLTableElement {
	constructor() {
		super();

		const document = this.ownerDocument;
		this.selectionElement = document.createElement("div");
		this.selectionElement2 = document.createElement("div");
		this.selectionElement.style.cssText = `
			position: absolute;
			background: rgba(111, 168, 220, 0.66);
			pointer-events: none;
		`;
		this.selectionElement2.style.cssText = `
			position: absolute;
			outline: 1px dashed rgba(0, 0, 0, 0.5);
			pointer-events: none;
		`;
		
		this.mergeButton = document.createElement("button");
		this.mergeButton.textContent = "объединить";
		this.mergeButton.addEventListener("click", this.merge.bind(this));

		this.addEventListener("mousedown", this.startSelect.bind(this));
		document.addEventListener("mousemove", this.moveSelect.bind(this));
		document.addEventListener("mouseup", this.endSelect.bind(this));
	}
	
	connectedCallback() {
		this.parentNode.insertBefore(this.mergeButton, this);
	}

	startSelect(event) {		
		this._x = event.pageX;
		this._y = event.pageY;
		this._pointerIsMoving = true;
		this.appendChild(this.selectionElement);
		this.appendChild(this.selectionElement2);
				
		this.cellBoxes = [];

		for(const cell of this.querySelectorAll("td")) {
			const box = Box.from(cell.getBoundingClientRect())
				.relativeToElement(this);
			this.cellBoxes.push(box);
		}
		
		this.moveSelect(event);
	}

	moveSelect(event) {
		if(!this._pointerIsMoving) return;

		event.preventDefault();

		const { pageX: x, pageY: y } = event;
		const selectionBox = new Box(
			Math.min(x, this._x), Math.min(y, this._y),
			Math.abs(x - this._x), Math.abs(y - this._y)
		).relativeToElement(this);

		let actualSelectionBox = this.constructor.getActualSelection(selectionBox, this.cellBoxes);

		this.constructor.transferBoxToElement(actualSelectionBox, this.selectionElement);
		this.constructor.transferBoxToElement(selectionBox, this.selectionElement2);
	}

	static transferBoxToElement({x, y, width, height }, { style }) {
		style.left = `${x}px`;
		style.top = `${y}px`;
		style.width = `${width}px`;
		style.height = `${height}px`;
	}

	static getActualSelection(selectionBox, cellBoxes) {
		const box = getActualSelectionHelper(selectionBox, cellBoxes);
		return box.equals(getActualSelectionHelper(box, cellBoxes)) ? box : this.getActualSelection(box, cellBoxes);

		function getActualSelectionHelper(selectionBox, cellBoxes) {
			let actualSelectionBox = Box.from(selectionBox)

			for(const box of cellBoxes) {
				if(selectionBox.overlaps(box)) {
					actualSelectionBox = actualSelectionBox.add(box);
				}
			}

			return actualSelectionBox;
		}
	}
	
	merge() {
		let actualSelectionBox = this.constructor.getActualSelection(this._selectionBox, this.cellBoxes);

		this.constructor.transferBoxToElement(actualSelectionBox, this.selectionElement);
		this.constructor.transferBoxToElement(this._selectionBox, this.selectionElement2);

		const cellsMap = [];
		function addToMap(x, y, r = 0) {
			loop: for(var j = r; true; j++) {
				if(!cellsMap[j]) cellsMap[j] = [];
				
				for(var i = 0; true; i++) {
					if(!cellsMap[j][i]) {
						for(var v = 0; v < y; v++) {
							for(var u = 0; u < x; u++) {
								if(!cellsMap[j + v]) cellsMap[j + v] = [];
								
								cellsMap[j + v][i + u] = 1;
							}
						}

						break loop;
					}
				}
			}
		}
		let selectedCells = [];
		let index;
		let isFirstSelectedRowProcessed = false;

		for(const row of this.rows) {
			let isRowAffectedBySelection = false;

			for(const cell of row.cells) {
				const cellBox = Box.from(cell.getBoundingClientRect()).relativeToElement(this);

				if(actualSelectionBox.overlaps(cellBox)) {
					if(!isRowAffectedBySelection) {
						isRowAffectedBySelection = true;
					}

					if(!isFirstSelectedRowProcessed) {
						isFirstSelectedRowProcessed = true;
						index = 0;
					}

					selectedCells.push(cell);
					addToMap(cell.colSpan, cell.rowSpan, index);
				}
			}

			index++;
		}

		const span = {
			colSpan: "0" in cellsMap ? cellsMap[0].length : 0,
			rowSpan: cellsMap.length
		};
		
		const selectedCell = selectedCells.shift();

		Object.assign(selectedCell || {}, span);
		selectedCells.forEach(cell => {
			selectedCell.append(...cell.childNodes);
			cell.remove()
		});
		this.selectionElement.remove();
	}
	
	split() {
		// TODO если нужно
	}

	endSelect(event) {
		if(!this._pointerIsMoving) return;
		this._pointerIsMoving = false;

		const { pageX: x, pageY: y } = event;
		this._selectionBox = new Box(
			Math.min(x, this._x), Math.min(y, this._y),
			Math.abs(x - this._x), Math.abs(y - this._y)
		).relativeToElement(this);

		this.selectionElement2.remove();
	}
}

customElements.define("editable-table", EditableTable, { extends: "table" });

// firefox + contenteditable + table !== ❤
//document.addEventListener("DOMContentLoaded", event => {
//	for(const cell of document.querySelectorAll("td")) {
//		cell.contentEditable = true;
//	}
//});
</script>
</head>
<body>
	<table is="editable-table">
		<tbody>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
		</tbody>
	</table>
</body>
</html>


Цитата:

Сообщение от MC-XOBAHCK
У меня у таблицы стоит атрибут contenteditable="true", то есть можно ячейки редактировать.
Можно ли как то определить в какой ячейке установлен курсор? Может добавляется к ячейке фокус или ещё что?

Когда у таблицы стоит атрибут contenteditable, то всё редактируется, что внутри элемента table. Например, вы можете вписать текст в две соседние ячейки, затем обе выделить, скопировать и вставить в третью ячейку. Если нужно редактировать только содержимое ячеек, то нужно прописать к каждой ячейке contenteditable
for(const cell of document.querySelectorAll("td")) {
	cell.contentEditable = true;
}

рони 02.11.2018 12:19

Malleys,
выделение странно работает, захват ячеек больше чем нужно, особенно если начать с 2 или 3 колонки.

Malleys 02.11.2018 12:28

Вложений: 1
Цитата:

Сообщение от рони
выделение странно работает, захват ячеек больше чем нужно, особенно если начать с 2 или 3 колонки

Ещё раз, поскольку ячейка таблицы прямоугольная, то и выделенные ячейки (из которых можно получить объединённую ячейку) представляют из себя прямоугольную область. Ячейка не может быть не прямоугольной! Или вы нашли какой-то баг, который я не смог обнаружить! До сих пор не могу обнаружить!

UPD Пусть исходная таблица выглядит так. Вы хотите объединить ячейки A и B. Вы выделяете мышью область, которая затрагивает эти ячейки(отмечено красным). Вы могли решить, что объединённая ячейка должна занимать область, отмеченную жёлтым цветом, но это не так. Если предположить, что область, отмеченная жёлтым цветом и есть наименьшая область, которая может быть заменена одной ячейкой, то ячейка С удаляется, что приводит к смещению ячейки следующей за ней в той же колонке. Но это противоречит утверждению «ячейка таблицы прямоугольная, ведь выделенные ячейки (из которых можно получить объединённую ячейку) представляют из себя прямоугольную область», а значит выбранные (A, B и C) ячейки на самом деле не составляли прямоугольную область. Синяя область, равным образом, как и область всей таблицы, соответствует этому утверждению, но только она является наименьшей областью включающей в себя исходное выделение и граница которой проходить по рёбрам ячеек.

рони 02.11.2018 13:28

Malleys,
ещё нет обьединённых ячеек, таблица изначальна,
я не могу выделить одну ячейку!!! или 2!!! выделяется вместо одной две, вместо двух четыре.

рони 02.11.2018 13:47

Malleys,
2 и 3 колонка для примера с глюком, 4 нормально

Nexus 02.11.2018 13:51

рони, а какой у вас браузер?
У меня, к слову, в Chrome@70.0.3538.77 подобного поведения нет, все корректно работает.

Upd. баг есть, если zoom страницы в браузере отличен от 100%.

рони 02.11.2018 14:12

Цитата:

Сообщение от Nexus
zoom страницы в браузере отличен от 100%.

да хром последний и zoom
и ещё в Firefox таблица странно выглядит.

Malleys 02.11.2018 16:03

Цитата:

Сообщение от Nexus
баг есть, если zoom страницы в браузере отличен от 100%

Цитата:

Сообщение от рони
и zoom

Уменьшил область (см. исправленный пост №13) ячейки при вычислении пересечения, берётся 99.8% от реального размера. Дело в том, что метод Element.prototype.getBoundingClientRect при масштабировании (> 100%)может возвращать величины, которые отличаются на...
// помасштабируй и запускай
alert(1 - 1 / devicePixelRatio)


Или как исправить этот костыль? (99.8% от реального размера)

Цитата:

Сообщение от рони
в Firefox таблица странно выглядит.

contenteditable действует так

рони 02.11.2018 16:38

Malleys,
ок сейчас нормально, жаль что ничего не понимаю, ваш код выше моего уровня знаний, теоретически всё понятно из описания, но как это работает, для меня загадка. :) :thanks:

MC-XOBAHCK 02.11.2018 17:15

Malleys, Спасибо вам!
Да, идея выделения ячеек именно так должно быть.
При зуме баг видел, сейчас уже исправили - работает правильно. Смотрю в хроме.

Цитата:

Сообщение от Malleys
Когда у таблицы стоит атрибут contenteditable, то всё редактируется, что внутри элемента table. Например, вы можете вписать текст в две соседние ячейки, затем обе выделить, скопировать и вставить в третью ячейку. Если нужно редактировать только содержимое ячеек, то нужно прописать к каждой ячейке contenteditable

Я имел в виду, получает ли ячейка в которой установлен курсор какой нибудь псевдокласс фокус или актив, чтобы её можно было найти и повесить события blur, focus? Я в этом направлении рыл, тестил - ничего не нашёл. Вот только что пришла идея посмотреть document.activeElement при событиях клавиатуры. А пока нашёл решение - при клике на ячейку устанавливать ей класс select.

contenteditable у меня на всю таблицу в теге table прописан. Я всё в ней редактирую, включая заголовок caption и colgroup. Как лучше для редактора поставить: на всю таблицу или на каждую ячейку - не знаю. В далёкой перспективе есть планы сделать из этого плагин для VSCode.


В общем буду внедрять код в свой и разбираться. Мне теперь на это нужно какое то время.

MC-XOBAHCK 06.11.2018 15:07

Malleys, можно к вам обратиться за помощью?

Я внедрил ваше решение:
https://javascript.ru/forum/xhtml-ht...tml#post497714

В свою таблицу у тега table я прописал атрибут is="editable-table"

Теперь у меня есть две проблемы:
1. Если работает ваш скрипт, то таблица становится нередактируемая. Я и при клике на ячейки добавлял им атрибут contenteditable="true"
el.setAttribute('contenteditable', true);

Атрибут добавляется, но ячейки так и остаются нередактируемыми.
В вашем скрипте случайно нет чего то запрещающего редактирование? Стили CSS я убрал.
В скрипте 97, 102 строка: pointer-events: none;
В блоке style 13 строка: user-select: none;
Можете подсказать как вернуть возможность редактирования?

2. После выделения ячеек при клике правой кнопкой мышки выделение снимается. Я кнопку "Объединить ячейки" убрал из скрипта, а хочу повесить это в кастомное контекстное меню, привязанное к таблице. Но когда вызываю контекстное меню (клик правая кнопка мыши), то выделение ячеек удаляется.
Пробовал 109 строка this.addEventListener("mousedown", this.startSelect.bind(this));
поменять вот так:
this.addEventListener("mousedown", function(e) {
    e.which !== 3 ? this.startSelect.bind(this) : 0
});

но тогда выделение ячеек не запускается. Ошибок в консоле нет.
Может что то посоветуете?

Malleys 06.11.2018 18:22

Цитата:

Сообщение от MC-XOBAHCK
В вашем скрипте случайно нет чего то запрещающего редактирование?

Да, точно есть, это как раз начало выделения ячеек.

Цитата:

Сообщение от MC-XOBAHCK
1. Если работает ваш скрипт, то таблица становится не редактируемая

Добавил при двойном щелчке редактирование. Во время редактирования ячейки отключается выделение ячеек, которое обратно включается, когда теряется фокус у таблицы или нажат Esc.

Цитата:

Сообщение от MC-XOBAHCK
когда вызываю контекстное меню (клик правая кнопка мыши), то выделение ячеек удаляется.

Исправил, т. е. выделение начинается только если основная кнопка мыши нажата.

Двойной щелчёк позволяет начать редактирование, также он является индикатором того, что во время редактирования должен выделятся именно текст, а не начинаться выделение ячеек. Чтобы закончить редактирование, щёлкните вне таблицы или нажмите Esc.

<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width">
	<style type="text/css">
		
		table {
			height: 500px;
			width: 500px;
			border-collapse: collapse;
			position: relative;
			user-select: none;
			cursor: default;
			table-layout: fixed;
			font: 1em Helvetica Neue, Segoe UI, Roboto, Ubuntu, sans-serif;
			text-align: center;
		}

		td {
			border: 1px gray solid;
		}

		td:focus {
			box-shadow: inset 0 0 0 2px rgba(111, 168, 220, 0.66);
			outline: 0;
		}
		
	</style>
	<script>
		
class Box {
	constructor(x = 0, y = 0, width = 0, height = 0) {
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
	}

	contains({ x, y, width, height }) {				
		return (
			x > this.x &&
			y > this.y &&
			(x + width) < (this.x + this.width) &&
			(y + height) < (this.y + this.height)
		);
	}

	overlaps({ x, y, width, height }) {
		return (
			1.001 * Math.max(x, this.x) < 0.998 * Math.min(x + width, this.x + this.width) &&
			1.001 * Math.max(y, this.y) < 0.998 * Math.min(y + height, this.y + this.height)
		);
	}

	equals({ x, y, width, height }) {
		return (
			x === this.x &&
			y === this.y &&
			width === this.width &&
			height === this.height
		);
	}

	add({x, y, width, height}) {
		return new Box(
			Math.min(x, this.x),
			Math.min(y, this.y),
			Math.max(x + width, this.x + this.width) - Math.min(x, this.x),
			Math.max(y + height, this.y + this.height) - Math.min(y, this.y)
		);
	}

	relativeTo({ x, y }) {
		return new Box(
			this.x - x,
			this.y - y,
			this.width,
			this.height
		);
	}

	relativeToElement(element) {
		return this.relativeTo(element.getBoundingClientRect());
	}

	static from({ x = 0, y = 0, width = 0, height = 0 } = {}) {
		return new Box(x, y, width, height);
	}
}

class EditableTable extends HTMLTableElement {
	constructor() {
		super();

		const document = this.ownerDocument;
		this.selectionElement = document.createElement("div");
		this.selectionElement2 = document.createElement("div");
		this.selectionElement.style.cssText = `
			position: absolute;
			background: rgba(111, 168, 220, 0.66);
			pointer-events: none;
		`;
		this.selectionElement2.style.cssText = `
			position: absolute;
			outline: 1px dashed rgba(0, 0, 0, 0.5);
			pointer-events: none;
		`;

		this._startSelect = this.startSelect.bind(this);
		this._moveSelect  = this.moveSelect.bind(this);
		this._endSelect   = this.endSelect.bind(this);

		this.addEventListener("dblclick", this.dblClickHandler.bind(this));
		this.addEventListener("focus", this.unregisterSelectHandlers.bind(this), true);
		this.addEventListener("blur", this.registerSelectHandlers.bind(this), true);
		this.addEventListener("keyup", this.registerSelectHandlers.bind(this), true);

		this.registerSelectHandlers();

		new MutationObserver(mutations => {
			for(const mutation of mutations) {
				if(mutation.type !== "childList") continue;

				for(const node of mutation.addedNodes) {
					if(node.constructor !== HTMLTableCellElement) continue;

					node.contentEditable = true;
					node.tabIndex = 0;
				}
			}
		}).observe(this, { childList: true, subtree: true });
	}

	dblClickHandler({ target }) {
		(target.closest("td") || { focus() {} }).focus();
	}

	unregisterSelectHandlers(event) {
		this.removeEventListener("mousedown", this._startSelect);
		document.removeEventListener("mousemove", this._moveSelect);
		document.removeEventListener("mouseup", this._endSelect);
	}

	registerSelectHandlers(event = {}) {
		if(event.type === "keyup" && event.keyCode !== 27) return;
		if(event.type === "keyup") event.target.blur();

		this.addEventListener("mousedown", this._startSelect);
		document.addEventListener("mousemove", this._moveSelect);
		document.addEventListener("mouseup", this._endSelect);
	}

	startSelect(event) {
		if(event.button !== 0) return;	
		this._x = event.pageX;
		this._y = event.pageY;
		this._pointerIsMoving = true;
		this.style.cursor = "crosshair";
				
		this.cellBoxes = [];

		for(const cell of this.querySelectorAll("td")) {
			const box = Box.from(cell.getBoundingClientRect())
				.relativeToElement(this);
			this.cellBoxes.push(box);
		}

		this.appendChild(this.selectionElement);
		this.appendChild(this.selectionElement2);
		this.moveSelect(event);
	}

	moveSelect(event) {
		if(!this._pointerIsMoving) return;

		event.preventDefault();

		const { pageX: x, pageY: y } = event;
		const selectionBox = new Box(
			Math.min(x, this._x), Math.min(y, this._y),
			Math.abs(x - this._x), Math.abs(y - this._y)
		).relativeToElement(this);

		let actualSelectionBox = this.constructor.getActualSelection(selectionBox, this.cellBoxes);

		this.constructor.transferBoxToElement(actualSelectionBox, this.selectionElement);
		this.constructor.transferBoxToElement(selectionBox, this.selectionElement2);
	}

	endSelect(event) {
		if(!this._pointerIsMoving) return;
		this._pointerIsMoving = false;
		this.style.cursor = "";

		const { pageX: x, pageY: y } = event;
		this._selectionBox = new Box(
			Math.min(x, this._x), Math.min(y, this._y),
			Math.abs(x - this._x), Math.abs(y - this._y)
		).relativeToElement(this);

		this.selectionElement2.remove();
	}

	static transferBoxToElement({x, y, width, height }, { style }) {
		style.left = `${x}px`;
		style.top = `${y}px`;
		style.width = `${width}px`;
		style.height = `${height}px`;
	}

	static getActualSelection(selectionBox, cellBoxes) {
		const box = getActualSelectionHelper(selectionBox, cellBoxes);
		return box.equals(getActualSelectionHelper(box, cellBoxes)) ? box : this.getActualSelection(box, cellBoxes);

		function getActualSelectionHelper(selectionBox, cellBoxes) {
			let actualSelectionBox = Box.from(selectionBox)

			for(const box of cellBoxes) {
				if(selectionBox.overlaps(box)) {
					actualSelectionBox = actualSelectionBox.add(box);
				}
			}

			return actualSelectionBox;
		}
	}
	
	merge() {
		let actualSelectionBox = this.constructor.getActualSelection(this._selectionBox, this.cellBoxes);

		this.constructor.transferBoxToElement(actualSelectionBox, this.selectionElement);
		this.constructor.transferBoxToElement(this._selectionBox, this.selectionElement2);

		const cellsMap = [];
		function addToMap(x, y, r = 0) {
			loop: for(var j = r; true; j++) {
				if(!cellsMap[j]) cellsMap[j] = [];
				
				for(var i = 0; true; i++) {
					if(!cellsMap[j][i]) {
						for(var v = 0; v < y; v++) {
							for(var u = 0; u < x; u++) {
								if(!cellsMap[j + v]) cellsMap[j + v] = [];
								
								cellsMap[j + v][i + u] = 1;
							}
						}

						break loop;
					}
				}
			}
		}
		let selectedCells = [], index;
		let isFirstSelectedRowProcessed = false;

		for(const row of this.rows) {
			for(const cell of row.cells) {
				const cellBox = Box.from(cell.getBoundingClientRect()).relativeToElement(this);

				if(actualSelectionBox.overlaps(cellBox)) {
					if(!isFirstSelectedRowProcessed) {
						isFirstSelectedRowProcessed = true;
						index = 0;
					}

					selectedCells.push(cell);
					addToMap(cell.colSpan, cell.rowSpan, index);
				}
			}

			index++;
		}

		const span = {
			colSpan: "0" in cellsMap ? cellsMap[0].length : 0,
			rowSpan: cellsMap.length
		};
		
		const selectedCell = selectedCells.shift();

		Object.assign(selectedCell || {}, span);
		selectedCells.forEach(cell => {
			selectedCell.append(...cell.childNodes);
			cell.remove()
		});
		this.selectionElement.remove();
	}
	
	split() {
		// TODO если нужно
	}
}

customElements.define("editable-table", EditableTable, { extends: "table" });

</script>
</head>
<body>
	<!-- метод merge у таблицы работает как обычный метод у элемента: при наличии выделенных ячеек они объединяются -->
	<button onclick="document.querySelector('table[is=editable-table]').merge();">Объединить</button>
	<table is="editable-table">
		<tbody>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
				<td></td>
			</tr>
		</tbody>
	</table>
</body>
</html>


Цитата:

Сообщение от MC-XOBAHCK
В скрипте ... pointer-events: none;

Это чтобы по выделению не попадало, а то как по ячейке тогда получится щёлкнуть.

Malleys 06.11.2018 18:23

...Продолжение предыдущего ответа

Цитата:

Сообщение от MC-XOBAHCK
Я и при клике на ячейки добавлял им атрибут contenteditable="true"
el.setAttribute('contenteditable', true);

Теперь таблица это умеет сама (в конструкторе указан наблюдатель изменении, который сам делает ячейки редактируемыми, так что даже если во время жизненного цикла таблицы ей добавить ячеек, то они станут редактируемыми)

Цитата:

Сообщение от MC-XOBAHCK
Я кнопку "Объединить ячейки" убрал из скрипта, а хочу повесить это в кастомное контекстное меню, привязанное к таблице.

Все то время пока в таблице есть выделение ячеек, вы можете выделенные ячейки объединить при помощи метода merge у таблицы. Я не знаю как у вас там сделано это меню, но во всяком случае, как минимум, функция обработчика может выглядеть как-то так...
function(event) {
	event.target.closest("table[is=editable-table]").merge();
}

MC-XOBAHCK 07.11.2018 00:14

Malleys,
Спасибо! Я сначала скопировал и вставил себе, выделение шло, но функционал конфликтовал, так как у меня уже было реализовано часть ваших новых доработок (смена курсора, выделение ячейки, подсветка и т.п.)
Начал я у себя отключать всё это и доотключался до того что у меня теперь ваше приложение не запускается.

Я предполагаю почему, но лучше уточнить.

Дело в том, что изначально я сгенерированный код таблицы вставлял внутрь дива таким способом:
div.innerHTML = `<table is="editable-table">${code}</table>`;

После моих изменений браузер начал ошибку выдавать на такую вставку через innerHTML.
Я переписал объявив таблицу свойством объекта и вставил через DOM. Теперь ваше приложение не запускается так как не видит таблицу.

Вот карскас из моего приложения который генерирует и вставляет таблицу:
document.addEventListener("DOMContentLoaded", function () {

    const table = {
        editor: document.querySelector('#tableEditor'), // Это родительский div в который вставляется таблица
        
        event: function () {
            // Тут слушатели addEventListener
        },

        generator: function () {
            let tableHTMLCode = 'тут код, он сгенерированный';

            this.tablitsa = document.createElement('table'); // Объявление таблицы как свойство объекта
            this.tablitsa.innerHTML = tableHTMLCode;
            this.tablitsa.setAttribute('is', 'editable-table');

            this.editor.appendChild(this.tablitsa);
        },

        init: function () {
            this.generator();
            this.event();
        
            // customElements.define("editable-table", EditableTable, { extends: "table" }); // это закоментировано, отсюда не запускаю.
        },

        // Тут куча методов
    }

    table.init();

    // Тут пошёл код объединения ячеек
});

Теперь таблица у меня получается table.tablitsa
и поэтому не запускается customElements.define("editable-table", EditableTable, { extends: "table" });

Я нашёл доку customElements.define() но не разобрался можно ли для свойства объекта объявить.

Есть вариант - в HTML прописать таблицу, а все внутренности вставлять через метод generator. Тогда должно всё заработать. Но вдруг есть возможность сделать объявление customElements для моего варианта. Поэтому я решил уточнить.


PS. А выделение можно ограничить чтоб оно за пределы таблицы не вылазило или я губешку раскатал и мне стоит написать цикл который будет закатывать её обратно?

Malleys 07.11.2018 10:26

Тот код, который указан в посте №24, структуру его скипта кратко можно представить так:

class Box {}
class EditableTable extends HTMLTableElement {}
customElements.define("editable-table", EditableTable, { extends: "table" });

Эти определения должны идти раньше всего.

Цитата:

Сообщение от MC-XOBAHCK
Дело в том, что изначально я сгенерированный код таблицы вставлял внутрь дива таким способом:
div.innerHTML = `<table is="editable-table">${code}</table>`;

Да, вы можете вставлять код таким образом, но только наблюдатель изменении не узнает ничего о вставленных ячейках, поскольку в div вставляется готовая таблица со всеми ячейками сразу.

Я добавил статичный метод _addedCellAction, в котором описывается что делать с новоявленной ячейкой. Он вызывается наблюдателем изменении (вслучае если добавили динамически к существующей таблице новую ячейку) и в тот момент, когда таблица только что была подключена к DOM (берутся все её ячейки, происходит, например, когда таблица вставляется при помощи innerHTML) Да, теперь вы можете вставлять код таким образом
div.innerHTML = `<table is="editable-table">${code}</table>`;


Цитата:

Сообщение от MC-XOBAHCK
document.createElement('table');

Метод createElement конструирует HTML основываясь на информации, переданной ему в параметрах. На основании этх параметров ('table') делается вывод, что нужен конструктор HTMLTableElement. Т. е. создаётся обычная таблица. Добавление атрибута is не может магическим образом подменить конструктор. По сути оно срабатывало, когда тег table и атрибут is="editable-table" были указаны вместе. Если вы инспектировали через консоль, то наверняка заметили, что у данной таблицы <table is="editable-table"> конструктором является EditableTable, а не HTMLTableElement. Когда вы вызываете document.createElement('table'); вы получаете экземпляр класса HTMLTableElement. Значит нужно сконструировать таблицу так, чтобы она была экземпляром класса EditableTable.

Самый простой вариант: используйте конструктор напрямую new EditableTable();, если вы хотите использовать метод сreateElement, то нужен дополнительный параметр document.createElement('table', { is: "editable-table" });

Цитата:

Сообщение от MC-XOBAHCK
А выделение можно ограничить чтоб оно за пределы таблицы не вылазило или я губешку раскатал и мне стоит написать цикл который будет закатывать её обратно?

Добавил к вспомогательному классу Box метод unionintersection, который вычисляет пересечение областей. Конкретно используется для вычисления пересечения вычисленной области с областью, которую занимает таблица.

Цитата:

Сообщение от MC-XOBAHCK
Вот карскас из моего приложения который генерирует и вставляет таблицу

Я его чуток переделал, как класс MyTableApp, вот что на данный момент
jsfiddle.net/Malleys/3b4r7mqs

SOS это вообще-то правильней intersection назвать, а не union, исправил

MC-XOBAHCK 07.11.2018 18:57

Malleys, СПАСИБО вам ещё раз. Выделение вообще выше всяких похвал.
Полностью удалил свой изначальный код - поставил ваш новый каркас, буду его обвешивать. Встал без багов.

Ошибки свою признаЮ - даже с названием облажался. Урок уяснил, волшебная приставка App теперь навеки со мной (я голову ломал - мне изначально table не нравилось, я его хотел для свойства поставить).

Про то что классы должны быть в начале скрипта знал и изначально правильно поставил, но почему то потом перенёс их в низ скрипта, возможно даже из за этого упало. Спасибо что ткнули носом.

Код чистый, читаемый, логически понятно разбитый. Понятней некуда. Доработать смогу. Я пока не углублялся (не вносил свой функционал), но единственное с чем у меня в дальнейшем могут появиться трудности - это изменение класса ('disabled' / ' ') у пункта кастомного контекстного меню "Объединить ячейки".
Если не ошибаюсь, то в классе EditableTable в методе endSelect(event) удаляю css-класс 'disabled' с пункта меню, а в методе merge() - присваиваю пункту меню css-класс 'disabled'.

В остальном вроде всё понятно.


Часовой пояс GMT +3, время: 15:28.