DOMUtils Test
撰写时间:2025-02-11
修订时间:2025-09-16
为便捷生成各种DOMs提供便利工具。
便捷生成表格
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
let ths = ['a', 'b', 'c', 'd'];
let cells = [
[1, 2, 3, 4],
[5, 6, 7, 8]
];
let str = DOMUtils.GenTableStr(ths, cells);
pc.log('%o', str);
使用二维数组来表示多行数据。
将标题数据与主体数据分离至不同的数组中,可解决HTML的表格格式代码与数据耦合度过高的问题,从而让我们专注于表格数据的独立变化。
单元格的跨行及跨列
表格单元格的跨行及跨列耦合度更高,且会引起后续行或列的连锁反应。编写跨行或跨列的表格代码往往成为一个噩梦。
跨列: 影响到当前行当前列的后面的列。colspan的值表示当前单元格共占多少列。
跨行: 影响到当前列的下面的行。rowspan的值表示当前单元格共占多少行。
| A | B | C | D | E | F |
| 1 | 2 | 3 | 4 | 5 | 6 |
| 2 | 2 | 3 | 4 | 5 |
| 3 | 2 | 3 | 4 | 5 | 6 |
| 4 | 2 | 3 | 4 | 5 | 6 |
| 5 | 2 | 3 | 4 | 5 |
| 6 | 2 | 3 | 4 | 5 | 6 |
为解决此问题,可在上节的基础上,将跨行或跨列的需求以特定的格式编写进相应的数组中。
使用[data, row, col]的形式来指定需要跨行或跨列的情况,data表示单元格内容,row表示跨多少行,col表示跨多少列。
因此,['string', 0, 2]表示单元格内容为string
,跨2列;[4, 2]表示单元格内容为4,跨2行。
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
let ths = ['A', 'B', 'C', 'D', 'E'];
let cells = [
[1, ['string', 0, 2], [4, 2], 45],
[2, 6, 8, 9],
[3, [6, 3], 7, 32, 4],
[4, 2, [3, 2], 5],
[5, 2, 3]
];
let str = DOMUtils.GenTableStr(ths, cells);
pc.log('%o', str);
这样可达到非常直观的效果。以后若要修改所跨的行列也更加便捷。
将多行拆分为多列显示
有时,表格的行太多而列太少,为充分利用水平方向上的空间,可考虑分成多列显示,即,将一些过长的行剪下来,放至右边而成为并排的列。
依先排列还是先排行,分为列优先及行优先两种方式。
列优先
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
const { NumUtils } = await import('/js/esm/NumUtils.js');
let ths = ['Row Id', 'A', 'B', 'C'];
let dataColsNum = ths.length - 1;
let rows = 15;
let nums = NumUtils.GetIntsInRange(1, 100, dataColsNum * rows);
let numIndex = 0;
let cells = [];
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
cells.push([rowIndex, nums[numIndex++], nums[numIndex++], nums[numIndex++]]);
}
pc.group('Normal layout');
let str = DOMUtils.GenTableStr(ths, cells);
pc.log('%o', str);
pc.groupEnd();
pc.group('Fold into 4 cols');
let splitIntoColsNum = 4;
str = DOMUtils.GenTableStr(ths, cells, splitIntoColsNum);
pc.log('%o', str);
pc.groupEnd();
将一个15行的表格,转变为每行显示4组数据,从而同时兼顾了美观及空间的需求。
上述效果为列优先 (column majored)的方式,即先在垂直方向上按顺序显示一部分行的数据,再添加新列并按顺序显示另一部分行的数据,直至最后一行数据。
行优先
下面实现行优先 (row majored),即先确定每行共有多少列,再按原数组的顺序逐行往下排。
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
const { NumUtils } = await import('/js/esm/NumUtils.js');
let ths = ['Row Id', 'A', 'B', 'C'];
let dataColsNum = ths.length - 1;
let rows = 7;
let nums = NumUtils.GetIntsInRange(1, 100, dataColsNum * rows);
let numIndex = 0;
let cells = [];
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
cells.push([rowIndex, nums[numIndex++], nums[numIndex++], nums[numIndex++]]);
}
pc.group('Normal layout');
let str = DOMUtils.GenTableStr(ths, cells);
pc.log('%o', str);
pc.groupEnd();
pc.group('Fold into 4 cols');
let splitIntoColsNum = 4;
str = DOMUtils.GenTableStr(ths, cells, splitIntoColsNum, false);
pc.log('%o', str);
pc.groupEnd();
手工指定切割行的范围
如果数据有内在规律,则可手工指定各行的边界。
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
const { getBinStr, getHexStr } = await import('/js/esm/BinUtils.js');
let ths = ['Num', 'Bin', 'Hex'];
let cells = [];
for (let num = 0; num < 16; num++) {
cells.push([num, getBinStr(num), getHexStr(num)]);
}
pc.group('Normal layout');
let str = DOMUtils.GenTableStr(ths, cells);
pc.log('%o', str);
pc.groupEnd();
pc.group('By specifying row delimiters');
str = DOMUtils.GenTableStr(ths, cells, [[0, 1], [2, 3], [4, 7], [8, 15]]);
pc.log('%o', str);
pc.groupEnd();
第一列组有效数位为1位,第二列组有效数位为2位,第三列组有效数位为3位,第四列组有效数位为4位。
这种方式,仍为列优先,但左边的列无须填满即可另起新列。
Node.insertAfter
编写代码
在DOM中,Node只有insertBefore方法,没有insertAfter方法。DomUtils将直接在Node的原型上添加此方法。
Node.prototype.insertAfter = function(insertElement, referenceElement) {
if (!insertElement) {
throw new Error(`Argument 1 ('node') to Node.insertAfter must be an instance of Node`);
}
this.insertBefore(insertElement, referenceElement?.nextSibling);
};
上面最后一行代码也可改写为:
Node.prototype.insertBefore.call(this, insertElement, referenceElement.nextSibling);
但这里由于调用者均为同一对象,因此没必要。
需注意的是,上面代码不能使用lambda的形式:
Node.prototype.insertAfter = (insertElement, referenceElement) => {
// this would not be Node
};
在采用lambda的形式中,this将指向调用此方法时所在环境的对象,而不是Node对象。
测试
下面测试客户端调用。第一种情况,如果insertElement为空值时:
const {} = await import('/js/esm/DOMUtils.js');
// generate DOM tree first
pc.appendHTMLStr(`This is the only child node.
`);
let parentNode = pc.querySelector('#parent');
let refNode = parentNode.querySelector('p');
parentNode.insertAfter(null, refNode);
此时抛出异常,与Node.insertBefore方法的逻辑一致,正确。
第二种情况,如果referenceElement为空值时:
const {} = await import('/js/esm/DOMUtils.js');
// generate DOM tree first
pc.appendHTMLStr(`This is the only child node.
`);
let parentNode = pc.querySelector('#parent');
let refNode = parentNode.querySelector('p');
let newP = document.createElement('p');
newP.textContent = 'This is the new paragraph.';
parentNode.insertAfter(newP, null);
此时将新元素插入为父节点的最后一个节点,与Node.insertBefore方法的逻辑一致,正确。
第三种情况,当两个参数均不为空值时:
const {} = await import('/js/esm/DOMUtils.js');
// generate DOM tree first
pc.appendHTMLStr(``);
let parentNode = pc.querySelector('#parent');
let map = {
1: '1st',
2: '2nd',
3: '3rd'
};
for (let i = 1; i <= 3; i++) {
let newDiv = document.createElement('div');
newDiv.textContent = `This div is expected to be inserted after the ${map[i]} paragraph.`;
let refNode = parentNode.querySelector(`p:nth-of-type(${i})`);
parentNode.insertAfter(newDiv, refNode);
}
3个新节点均正确地插入到相应的节点之后。
AppendCSSFile
AppendCSSFile将一个CSS文件添加到head标签,或ShadowRoot。
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
const { applyColorTheme } = await import('/js/esm/color-scheme.js');
let iframe, doc, htmlElement;
function appendIframe() {
iframe = document.createElement('iframe');
pc.appendHTMLStr(``);
pc.querySelector('#target').appendChild(iframe);
pc.log('%O', iframe);
}
function initIframe() {
doc = iframe.contentDocument;
htmlElement = doc.firstElementChild;
applyColorTheme(htmlElement);
doc.head.innerHTML = `
`;
doc.body.insertAdjacentHTML('afterbegin', `This is a sample paragraph.
`);
}
appendIframe();
initIframe();
DOMUtils.AppendCSSFile(doc, 'demo.css');
pc.listDOMTree(htmlElement, (node) => {
let title = node.tagName.toLowerCase();
if (title === 'link') {
return `${title}: href="${node.href}"`;
}
return title;
});
在DOMTree中应可看到所添加的CSS文件,被插入到script或style标签(若有)之前。
NestedFlatHeaders
Markdown的章节结构是扁平的,我们可以将这种扁平的章节结构转换为嵌套的结构。
const { DOMUtils } = await import('/js/esm/DOMUtils.js');
let str = `
Article Title
aaa
bbb
Section A
A text.
Section A.1
A.1 text.
Section A.2
A.2 text.
Section B
B text.
Section B.1
B.1 text.
Section B.2
B.2 text.
`;
pc.appendHTMLStr(str);
let container = pc.consoleUL.querySelector('article#curr-article');
DOMUtils.NestedFlatHeaders(container);
查看DOM树,扁平结构已转换为嵌套关系,且每个section下面的标题,从h2至h6,均已经标准化为h1。