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, время: 08:51. |