Javascript.RU

Создать новую тему Ответ
 
Опции темы Искать в теме
  #21 (permalink)  
Старый 02.11.2018, 16:38
Аватар для рони
Профессор
Отправить личное сообщение для рони Посмотреть профиль Найти все сообщения от рони
 
Регистрация: 27.05.2010
Сообщений: 33,068

Malleys,
ок сейчас нормально, жаль что ничего не понимаю, ваш код выше моего уровня знаний, теоретически всё понятно из описания, но как это работает, для меня загадка.
Ответить с цитированием
  #22 (permalink)  
Старый 02.11.2018, 17:15
Аватар для MC-XOBAHCK
Профессор
Отправить личное сообщение для MC-XOBAHCK Посмотреть профиль Найти все сообщения от MC-XOBAHCK
 
Регистрация: 06.08.2017
Сообщений: 473

Malleys, Спасибо вам!
Да, идея выделения ячеек именно так должно быть.
При зуме баг видел, сейчас уже исправили - работает правильно. Смотрю в хроме.

Сообщение от Malleys
Когда у таблицы стоит атрибут contenteditable, то всё редактируется, что внутри элемента table. Например, вы можете вписать текст в две соседние ячейки, затем обе выделить, скопировать и вставить в третью ячейку. Если нужно редактировать только содержимое ячеек, то нужно прописать к каждой ячейке contenteditable
Я имел в виду, получает ли ячейка в которой установлен курсор какой нибудь псевдокласс фокус или актив, чтобы её можно было найти и повесить события blur, focus? Я в этом направлении рыл, тестил - ничего не нашёл. Вот только что пришла идея посмотреть document.activeElement при событиях клавиатуры. А пока нашёл решение - при клике на ячейку устанавливать ей класс select.

contenteditable у меня на всю таблицу в теге table прописан. Я всё в ней редактирую, включая заголовок caption и colgroup. Как лучше для редактора поставить: на всю таблицу или на каждую ячейку - не знаю. В далёкой перспективе есть планы сделать из этого плагин для VSCode.


В общем буду внедрять код в свой и разбираться. Мне теперь на это нужно какое то время.
Ответить с цитированием
  #23 (permalink)  
Старый 06.11.2018, 15:07
Аватар для MC-XOBAHCK
Профессор
Отправить личное сообщение для MC-XOBAHCK Посмотреть профиль Найти все сообщения от MC-XOBAHCK
 
Регистрация: 06.08.2017
Сообщений: 473

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
});

но тогда выделение ячеек не запускается. Ошибок в консоле нет.
Может что то посоветуете?
Ответить с цитированием
  #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;
Это чтобы по выделению не попадало, а то как по ячейке тогда получится щёлкнуть.
Ответить с цитированием
  #25 (permalink)  
Старый 06.11.2018, 18:23
Аватар для Malleys
Профессор
Отправить личное сообщение для Malleys Посмотреть профиль Найти все сообщения от Malleys
 
Регистрация: 20.12.2009
Сообщений: 1,714

...Продолжение предыдущего ответа

Сообщение от MC-XOBAHCK
Я и при клике на ячейки добавлял им атрибут contenteditable="true"
el.setAttribute('contenteditable', true);
Теперь таблица это умеет сама (в конструкторе указан наблюдатель изменении, который сам делает ячейки редактируемыми, так что даже если во время жизненного цикла таблицы ей добавить ячеек, то они станут редактируемыми)

Сообщение от MC-XOBAHCK
Я кнопку "Объединить ячейки" убрал из скрипта, а хочу повесить это в кастомное контекстное меню, привязанное к таблице.
Все то время пока в таблице есть выделение ячеек, вы можете выделенные ячейки объединить при помощи метода merge у таблицы. Я не знаю как у вас там сделано это меню, но во всяком случае, как минимум, функция обработчика может выглядеть как-то так...
function(event) {
	event.target.closest("table[is=editable-table]").merge();
}

Последний раз редактировалось Malleys, 06.11.2018 в 22:56.
Ответить с цитированием
  #26 (permalink)  
Старый 07.11.2018, 00:14
Аватар для MC-XOBAHCK
Профессор
Отправить личное сообщение для MC-XOBAHCK Посмотреть профиль Найти все сообщения от MC-XOBAHCK
 
Регистрация: 06.08.2017
Сообщений: 473

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. А выделение можно ограничить чтоб оно за пределы таблицы не вылазило или я губешку раскатал и мне стоит написать цикл который будет закатывать её обратно?
Ответить с цитированием
  #27 (permalink)  
Старый 07.11.2018, 10:26
Аватар для Malleys
Профессор
Отправить личное сообщение для Malleys Посмотреть профиль Найти все сообщения от Malleys
 
Регистрация: 20.12.2009
Сообщений: 1,714

Тот код, который указан в посте №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, исправил

Последний раз редактировалось Malleys, 07.11.2018 в 10:43.
Ответить с цитированием
  #28 (permalink)  
Старый 07.11.2018, 18:57
Аватар для MC-XOBAHCK
Профессор
Отправить личное сообщение для MC-XOBAHCK Посмотреть профиль Найти все сообщения от MC-XOBAHCK
 
Регистрация: 06.08.2017
Сообщений: 473

Malleys, СПАСИБО вам ещё раз. Выделение вообще выше всяких похвал.
Полностью удалил свой изначальный код - поставил ваш новый каркас, буду его обвешивать. Встал без багов.

Ошибки свою признаЮ - даже с названием облажался. Урок уяснил, волшебная приставка App теперь навеки со мной (я голову ломал - мне изначально table не нравилось, я его хотел для свойства поставить).

Про то что классы должны быть в начале скрипта знал и изначально правильно поставил, но почему то потом перенёс их в низ скрипта, возможно даже из за этого упало. Спасибо что ткнули носом.

Код чистый, читаемый, логически понятно разбитый. Понятней некуда. Доработать смогу. Я пока не углублялся (не вносил свой функционал), но единственное с чем у меня в дальнейшем могут появиться трудности - это изменение класса ('disabled' / ' ') у пункта кастомного контекстного меню "Объединить ячейки".
Если не ошибаюсь, то в классе EditableTable в методе endSelect(event) удаляю css-класс 'disabled' с пункта меню, а в методе merge() - присваиваю пункту меню css-класс 'disabled'.

В остальном вроде всё понятно.
Ответить с цитированием
Ответ



Опции темы Искать в теме
Искать в теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Динамически обновленная таблица HTML работает неправильно mishapod AJAX и COMET 1 11.04.2018 01:50
Простой онлайн редактор HTML dima_riabets Элементы интерфейса 0 14.02.2015 03:25
Редактор HTML в режиме real-time Demath Сайт Javascript.ru 4 04.07.2012 16:09
редактор html или bb кодов на jquery gaserge Общие вопросы Javascript 15 28.08.2011 01:39
WYSIWYG редактор текста HTML страницы на javascript Дмитри Чижиков Ваши сайты и скрипты 4 14.09.2009 17:05