Редактор и 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> Может кто накинет идею, как мне выделить ячейки. Тестово поставил - мышкой поводить добавляет класс и по классу объединяет. Но это неправильно. |
Цитата:
|
Цитата:
Проблема в том, что у меня нет идеи как выделять ячейки, при этом нужно выделять прямоугольной областью не вылазя за пределы tbody, thead, tfooter. При объединении по горизонтали моё решение подходит, а вот в высоту пока только мысли проставить атрибуты с номерами строк или может по родителю ячеек как то можно. Но я не могу выделить ячейки, поэтому в высоту пока не развиваю идею. |
Цитата:
Цитата:
Откройте любое ПО, которое может создавать таблицы, объединять ячейки, тот же Word. Сохраните код как html и посмотрите структуру таблицы с объединенными ячейками, в особенности если в строке есть объединение в столбце. Это даст понимание того, что при объединении ячеек столбца не все так просто будет как с объединением в строке. |
выделятор блоков и обьединение ячеек
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> |
рони, Спасибо вам!
Вижу выделение ячеек происходит так как нужно, и вижу баг, что вы указали как требует доработки. Я с утра приболел, вот начинаю приходить в себя и разбирать код. Я код понимаю, но не весь. Если можно - есть вопросы. В строке 33 создаётся элемент - createElement. Это прямоугольник области выделения мышью? 37 строка if (e.which > 1) return; - я правильно понял - это условие останавливает выполнение функции если код клавиши > 1 ? То есть вместе с событием mousedown это и есть нажатая левая кнопка мыши. |
MC-XOBAHCK,
да и да :) |
MC-XOBAHCK,
Цитата:
|
Цитата:
Получается, что выделение ячеек вы реализовали, а нужно доработать последнюю функцию - вот эту: 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> вот же блин... |
MC-XOBAHCK,
да 1.можно убрать 2.свойство и атрибут, в данном случае изменение свойства равно назначению атрибута, такое не всегда, например свойство value можно изменить, атрибут останется прежним. подробнее тут https://learn.javascript.ru/attribut...nachenie-value 3. удачи!!! 4. не знаю, выбор за вами, смотря какой нужен результат. |
Я понял.
Цитата:
Не думаю что у меня получится с этим разобраться. Буду что то думать. Спасибо что помогли. |
Доработал расчёт размеров объединённой ячейки:
<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", то есть можно ячейки редактировать. Можно ли как то определить в какой ячейке установлен курсор? Может добавляется к ячейке фокус или ещё что? |
Вложений: 2
Цитата:
1. Пусть дана таблица 5 на 5 ячеек. Некоторые ячейки уже объединены: (Это не сложно) Вложение 4022 Если теперь объединить ячейки A и B, то таблица должна по прежнему остаться прямоугольной таблицей! 2. Дана таблица: Вложение 4023 Объединив любую ячейку с соседней, у вас по прежнему должна остаться прямоугольная таблица! Цитата:
Цитата:
Вот мой вариант. Я ввёл вспомогательный класс 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> Цитата:
for(const cell of document.querySelectorAll("td")) { cell.contentEditable = true; } |
Malleys,
выделение странно работает, захват ячеек больше чем нужно, особенно если начать с 2 или 3 колонки. |
Вложений: 1
Цитата:
UPD Пусть исходная таблица выглядит так. Вы хотите объединить ячейки A и B. Вы выделяете мышью область, которая затрагивает эти ячейки(отмечено красным). Вы могли решить, что объединённая ячейка должна занимать область, отмеченную жёлтым цветом, но это не так. Если предположить, что область, отмеченная жёлтым цветом и есть наименьшая область, которая может быть заменена одной ячейкой, то ячейка С удаляется, что приводит к смещению ячейки следующей за ней в той же колонке. Но это противоречит утверждению «ячейка таблицы прямоугольная, ведь выделенные ячейки (из которых можно получить объединённую ячейку) представляют из себя прямоугольную область», а значит выбранные (A, B и C) ячейки на самом деле не составляли прямоугольную область. Синяя область, равным образом, как и область всей таблицы, соответствует этому утверждению, но только она является наименьшей областью включающей в себя исходное выделение и граница которой проходить по рёбрам ячеек. |
Malleys,
ещё нет обьединённых ячеек, таблица изначальна, я не могу выделить одну ячейку!!! или 2!!! выделяется вместо одной две, вместо двух четыре. |
|
рони, а какой у вас браузер?
У меня, к слову, в Chrome@70.0.3538.77 подобного поведения нет, все корректно работает. Upd. баг есть, если zoom страницы в браузере отличен от 100%. |
Цитата:
и ещё в Firefox таблица странно выглядит. |
Цитата:
Цитата:
// помасштабируй и запускай alert(1 - 1 / devicePixelRatio) Или как исправить этот костыль? (99.8% от реального размера) Цитата:
|
Malleys,
ок сейчас нормально, жаль что ничего не понимаю, ваш код выше моего уровня знаний, теоретически всё понятно из описания, но как это работает, для меня загадка. :) :thanks: |
Malleys, Спасибо вам!
Да, идея выделения ячеек именно так должно быть. При зуме баг видел, сейчас уже исправили - работает правильно. Смотрю в хроме. Цитата:
contenteditable у меня на всю таблицу в теге table прописан. Я всё в ней редактирую, включая заголовок caption и colgroup. Как лучше для редактора поставить: на всю таблицу или на каждую ячейку - не знаю. В далёкой перспективе есть планы сделать из этого плагин для VSCode. В общем буду внедрять код в свой и разбираться. Мне теперь на это нужно какое то время. |
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 }); но тогда выделение ячеек не запускается. Ошибок в консоле нет. Может что то посоветуете? |
Цитата:
Цитата:
Цитата:
Двойной щелчёк позволяет начать редактирование, также он является индикатором того, что во время редактирования должен выделятся именно текст, а не начинаться выделение ячеек. Чтобы закончить редактирование, щёлкните вне таблицы или нажмите 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> Цитата:
|
...Продолжение предыдущего ответа
Цитата:
Цитата:
function(event) { event.target.closest("table[is=editable-table]").merge(); } |
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. А выделение можно ограничить чтоб оно за пределы таблицы не вылазило или я губешку раскатал и мне стоит написать цикл который будет закатывать её обратно? |
Тот код, который указан в посте №24, структуру его скипта кратко можно представить так:
class Box {} class EditableTable extends HTMLTableElement {} customElements.define("editable-table", EditableTable, { extends: "table" }); Эти определения должны идти раньше всего. Цитата:
Я добавил статичный метод _addedCellAction, в котором описывается что делать с новоявленной ячейкой. Он вызывается наблюдателем изменении (вслучае если добавили динамически к существующей таблице новую ячейку) и в тот момент, когда таблица только что была подключена к DOM (берутся все её ячейки, происходит, например, когда таблица вставляется при помощи innerHTML) Да, теперь вы можете вставлять код таким образом div.innerHTML = `<table is="editable-table">${code}</table>`; Цитата:
Самый простой вариант: используйте конструктор напрямую new EditableTable();, если вы хотите использовать метод сreateElement, то нужен дополнительный параметр document.createElement('table', { is: "editable-table" }); Цитата:
Цитата:
jsfiddle.net/Malleys/3b4r7mqs SOS это вообще-то правильней intersection назвать, а не union, исправил |
Malleys, СПАСИБО вам ещё раз. Выделение вообще выше всяких похвал.
Полностью удалил свой изначальный код - поставил ваш новый каркас, буду его обвешивать. Встал без багов. Ошибки свою признаЮ - даже с названием облажался. Урок уяснил, волшебная приставка App теперь навеки со мной (я голову ломал - мне изначально table не нравилось, я его хотел для свойства поставить). Про то что классы должны быть в начале скрипта знал и изначально правильно поставил, но почему то потом перенёс их в низ скрипта, возможно даже из за этого упало. Спасибо что ткнули носом. Код чистый, читаемый, логически понятно разбитый. Понятней некуда. Доработать смогу. Я пока не углублялся (не вносил свой функционал), но единственное с чем у меня в дальнейшем могут появиться трудности - это изменение класса ('disabled' / ' ') у пункта кастомного контекстного меню "Объединить ячейки". Если не ошибаюсь, то в классе EditableTable в методе endSelect(event) удаляю css-класс 'disabled' с пункта меню, а в методе merge() - присваиваю пункту меню css-класс 'disabled'. В остальном вроде всё понятно. |
Часовой пояс GMT +3, время: 15:28. |