Показать сообщение отдельно
  #24 (permalink)  
Старый 06.11.2018, 18:22
Аватар для Malleys
Профессор
Отправить личное сообщение для Malleys Посмотреть профиль Найти все сообщения от Malleys
 
Регистрация: 20.12.2009
Сообщений: 1,714

Сообщение от 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;
Это чтобы по выделению не попадало, а то как по ячейке тогда получится щёлкнуть.
Ответить с цитированием