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)

рони 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, время: 19:30.