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