手写js表格表头宽度拖动

514 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

github地址

如图

在这里插入图片描述

html代码

使用方法: new tableResizable(id)
表格需要用一个div包裹住,然后传入divid
关键技术点: 在表格内生成<col>标签

<col> 标签为表格中一个或多个列定义属性值。
<col>标签添加 class 属性。这样就可以使用 CSS 来负责对齐方式、宽度和颜色等等。

在这里插入图片描述

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title></title>
		<script src="tableResizable.js"></script>
	</head>
	<body>
		<html>
			<body>
				<div id="table">
					<table border="1">
						<thead>
							<tr>
								<th>Month</th>
								<th>Savings</th>
								<th>Month</th>
								<th>Savings</th>
							</tr>
						</thead>
						<tbody>
							<tr>
								<td>January</td>
								<td>$100</td>
								<td>January</td>
								<td>$100</td>
							</tr>
							<tr>
								<td>January</td>
								<td>$100</td>
								<td>January</td>
								<td>$100</td>
							</tr>
							<tr>
								<td>January</td>
								<td>$100</td>
								<td>January</td>
								<td>$100</td>
							</tr>
						</tbody>
					</table>
				</div>
			</body>
		</html>
		<script>
			new tableResizable('table')
		</script>
	</body>
</html>

js代码

//表格宽度自由调整
class tableResizable {
	constructor(id, options) {
		this._el = document.querySelector('#' + id);
		// 实际使用中需要对dom结构进行判断,这里就不做了
		this._tables = Array.from(this._el.querySelectorAll('table'));
		setTimeout(() => this._resolveDom());

		this.store = {
			dragging: false, //是否拖动
			draggingColumn: null, //拖动的对象
			miniWidth: 30, //拖动的最小宽度
			startMouseLeft: undefined, //鼠标点击时的clientX
			startLeft: undefined, //th右离table的距离
			startColumnLeft: undefined, //th左离table的距离
			tableLeft: undefined, //table离页面左边的距离,
			HColumns: [],
			BColumns: [],
		};
	};

	_saveCols(header, body) {
		// cols
		this.store.HColumns = Array.from(header.querySelectorAll('col')).map(v => ({
			el: v,
			isChange: false,
		}));
		this.store.BColumns = Array.from(body.querySelectorAll('col')).map(v => ({
			el: v,
			isChange: false,
		}));
	};

	_resolveDom() {
		const [THeader] = this._tables;

		let TBody;
		let Tr = [];
		let cols = [];
		if (THeader.tHead.rows.length > 1) {
			$(THeader.tHead.rows).find('th').each(function(index, item) {
				if (parseInt($(item).attr('colspan')) > 1) {

				} else {
					Tr.push($(item)[0])
				}
			})
			Tr.forEach((item, index) => {
				const col = document.createElement('col');
				item.dataset.index = index;
				col.width = +item.offsetWidth;
				cols.push(col);
			});
		} else {
			Tr = THeader.tHead.rows[0];
			const columns = Tr ? Array.from(Tr.cells) : [];
			cols = columns.map((item, index) => {
				const col = document.createElement('col');
				item.dataset.index = index;
				col.width = +item.offsetWidth;
				return col;

			});
		}

		const Bcolgroup = document.createElement('colgroup');

		cols.reduce((newDom, item) => {
			newDom.appendChild(item);
			return newDom;
		}, Bcolgroup);
		const HColgroup = Bcolgroup.cloneNode(true);

		//不管是一个table还是两个,都把header合body提出来
		if (this._tables.length === 1) {
			const [, tbody] = Array.from(THeader.children);
			tbody.remove();
			var HFirstChild = THeader.firstChild;
			THeader.insertBefore(HColgroup, HFirstChild);

			TBody = THeader.cloneNode();
			TBody.appendChild(Bcolgroup);
			TBody.appendChild(tbody);
			this._el.appendChild(TBody);
		} else {
			var HFirstChild = THeader.firstChild;
			THeader.insertBefore(HColgroup, HFirstChild);
			[, TBody] = this._tables;
			var BFirstChild = TBody.firstChild;
			TBody.insertBefore(Bcolgroup, BFirstChild);
		}

		//拖动时的占位线
		const hold = document.createElement('div');
		hold.classList.add('resizable-hold');
		this._el.appendChild(hold);

		// 把cols缓存起来
		this._saveCols(THeader, TBody);

		//处理事件
		for (var i = 0; i < THeader.tHead.rows.length; i++) {
			THeader.tHead.rows[i].addEventListener('mousemove', this.handleMouseMove.bind(this));
			THeader.tHead.rows[i].addEventListener('mouseout', this.handleMouseOut.bind(this));
		}


		//处理拖动
		const handleMouseDown = (evt) => {
			if (this.store.draggingColumn) {
				this.store.dragging = true;

				let {
					target
				} = evt;
				while (target && target.tagName !== 'TH') {
					target = target.parentNode;
				}

				if (!target) return;

				const tableEle = THeader;
				const tableLeft = tableEle.getBoundingClientRect().left;
				const columnRect = target.getBoundingClientRect();
				const minLeft = columnRect.left - tableLeft + this.store.miniWidth;
				target.classList.add('noclick');

				this.store.startMouseLeft = evt.clientX;
				this.store.startLeft = columnRect.right - tableLeft;
				this.store.startColumnLeft = columnRect.left - tableLeft;
				this.store.tableLeft = tableLeft;

				document.onselectstart = () => false;
				document.ondragstart = () => false;

				hold.style.display = 'block';
				hold.style.left = this.store.startLeft + 'px';

				const handleOnMouseMove = (event) => {
					const deltaLeft = event.clientX - this.store.startMouseLeft;
					const proxyLeft = this.store.startLeft + deltaLeft;

					hold.style.left = Math.max(minLeft, proxyLeft) + 'px';
				};

				// 宽度是这样分配的,举个🌰,如果a,b,c,d,他们每个都有个changed状态,默认false,拖过a,a.changed改为true,改变的宽度就由剩下的b,c,d平摊,如果都改变了,就让最后一个元素d背锅
				const handleOnMouseUp = (event) => {
					if (this.store.dragging) {
						const {
							startColumnLeft
						} = this.store;
						const finalLeft = parseInt(hold.style.left, 10);
						const columnWidth = finalLeft - startColumnLeft;
						const index = +target.dataset.index;
						HColgroup.children[index].width = columnWidth;
						if (index !== this.store.HColumns.length - 1) {
							this.store.HColumns[index].isChange = true;
						}
						const deltaLeft = event.clientX - this.store.startMouseLeft;
						const changeColumns = this.store.HColumns.filter((v, i) => i > index && !v.isChange && +v.el.width > 30);
						changeColumns.forEach(item => {
							item.el.width = +item.el.width - deltaLeft / changeColumns.length;
						});

						this.store.BColumns.forEach((item, i) => {
							item.el.width = this.store.HColumns[i].el.width;
						});

						hold.style.display = 'none';

						document.body.style.cursor = '';
						this.store.dragging = false;
						this.store.draggingColumn = null;
						this.store.startMouseLeft = undefined;
						this.store.startLeft = undefined;
						this.store.startColumnLeft = undefined;
						this.store.tableLeft = undefined;
					}

					document.removeEventListener('mousemove', handleOnMouseMove);
					document.removeEventListener('mouseup', handleOnMouseUp);
					document.onselectstart = null;
					document.ondragstart = null;

					setTimeout(() => {
						target.classList.remove('noclick');
					}, 0);
				};

				document.addEventListener('mouseup', handleOnMouseUp);
				document.addEventListener('mousemove', handleOnMouseMove);
			}
		};

		for (var i = 0; i < THeader.tHead.rows.length; i++) {
			THeader.tHead.rows[i].addEventListener('mousedown', handleMouseDown);
		}
	};

	handleMouseMove(evt) {
		let {
			target
		} = evt;
		while (target && target.tagName !== 'TH') {
			target = target.parentNode;
		}

		if (!target) return;

		if (!this.store.dragging) {
			const rect = target.getBoundingClientRect();
			const bodyStyle = document.body.style;
			if (rect.width > 12 && rect.right - evt.pageX < 8) {
				bodyStyle.cursor = 'col-resize';
				target.style.cursor = 'col-resize';
				this.store.draggingColumn = target;
			} else {
				bodyStyle.cursor = '';
				target.style.cursor = 'pointer';
				this.store.draggingColumn = null;
			}
		}
	};

	handleMouseOut() {
		document.body.style.cursor = '';
	}
}