重新排列UL列表,而不对所有LI元素使用appendChild



我可以使用一些正确的方法来处理这个问题。实际上,我正在尝试定期更新各种UL元素,而不会在整个列表上使用appendChild,因为这会导致浏览器失去焦点(例如,如果用户突出显示要复制和粘贴的内容)。我觉得我的方法过于复杂了,但它一直困扰着我,我不确定我能找到更好的方法。我有一个来自数据源的有序数组,并且我的UL可能需要新项或删除旧项以匹配有序数组。通过删除所有LI元素,然后按顺序添加有效列表,我实现了这一点。请注意,下面的代码是非常小的,以表示问题的性质,因为我的原始代码有更多的膨胀。

<BODY>
<UL id="list">
<LI class="listItem" data-name="john">red</LI>
<LI class="listItem" data-name="mark">green</LI>
<LI class="listItem" data-name="jane">yellow</LI>
<LI class="listItem" data-name="suzie">orange</LI>
</UL>
</BODY>

我第一次使用的javascript类似于:

let updatedArray = [
{name: "john", colour: "red"},
{name: "phil", colour: "blue"},
{name: "jane", colour: "orange"},
{name: "suzie", colour: "orange"},
{name: "casey", colour: "purple"},
];
let list = document.getElementById("list");
let currentItems = Array.from(list.querySelectorAll(".listItem"));
let orderedItems = [];
updatedArray.forEach( (updatedItem) => {
let currentItem = currentItems.find( item => item.dataset.name == updatedItem.name);
if(currentItem != null) {
if(currentItem.textContent != updatedItem.colour) currentItem.textContent = updatedItem.colour;
} else {
currentItem = document.createElement("LI");
currentItem.dataset.name = updatedItem.name;
currentItem.textContent = updatedItem.colour;
}
orderedItems.push(currentItem);
});
for(let i=list.children.length; i > 0; i--) {
list.removeChild(list.lastChild);
}
for(let i=0, n=orderedItems.length; i < n; i++) {
list.appendChild(orderedItems[i]);
}

我认为有不同的方法来改善上述情况,但我的问题的症结是使用appendChild以正确的顺序重新列出所有内容,因为这也消除了焦点。我一直在尝试做的是比较这两个数组,但它需要许多步骤,似乎是一种糟糕的方法(尚未经过彻底测试):

let updatedArray = [
{name: "john", colour: "red"},
{name: "phil", colour: "blue"},
{name: "jane", colour: "orange"},
{name: "suzie", colour: "orange"},
{name: "casey", colour: "purple"},
];
let list = document.getElementById("list");
let currentItems = Array.from(list.querySelectorAll(".listItem"));
let orderedItems = [];
updatedArray.forEach( (updatedItem) => {
let index = currentItems.findIndex( item => item.dataset.name == updatedItem.name);
if(index > -1) {
if(currentItems[index].textContent != updatedItem.colour) currentItems[index].textContent = updatedItem.colour;
// Remove found items to identify unmatched elements.
currentItems.splice(index);
} else {
currentItem = document.createElement("LI");
currentItem.dataset.name = updatedItem.name;
currentItem.textContent = updatedItem.colour;
}
orderedItems.push(currentItem);
});
// currentItems array now represents LI elements to be delisted. These are removed.
currentItems.forEach( (delistedItem) => {
delistedItem.parentNode.removeChild(delistedItem);
})
for(let i=0, n=orderedItems.length; i < n; i++) {
if(orderedItems[i] != list.children[i]) {
// Check if at the end of the current list already, or need to insert into list.
if(i >= list.children.length-1) {
list.appendChild(orderedItems[i]);
} else {
list.insertBefore(orderedItems[i], list.children[i+1]);
}
}
}

我没有得到太多(或任何)对这个问题的回应,所以现在我已经提出了一个通用的使用函数来满足我的上述需求。我不确定是否值得分享,但如果它能帮助到其他人,我还是会分享的。

这个函数基本上分为三个步骤:

  1. 创建一个列表,包含已更新的列表元素和要删除的列表元素(未在源中找到的项)。
  2. 遍历已删除的数组并将其从列表容器中移除。
  3. 遍历更新后的数组,并根据该数组在容器中错误位置追加新项或重新追加错误位置的项。

由于源数据是任意的,因此由用户通过提供用于检查标识和创建具有该标识的新LI元素的函数来确定源如何惟一地与列表元素相关联。例如,funcs.funcNewItem的函数可以设置id属性或一个数据id属性,和funcs.funcCheckIdentity可以检查他们匹配。

欢迎建议、更正或批评。这个功能解决了我目前的困境,但我可以欣赏任何改进的方法。

问题示例的完整代码:

/**
* @template T
* @template {HTMLLIElement} LISTELEM
* @template {HTMLUListElement|HTMLMenuElement} LISTCONTAINER
* @param {Array<T>} sourceArray
* @param {LISTCONTAINER} listContainer
* @param {string | null} containerQuerySelector
* @param {Object} funcs
* @param {function(T, LISTELEM): Boolean} funcs.funcCheckIdentity
* @param {function(T): LISTELEM} funcs.funcNewItem
* @param {function(T, LISTELEM): Void | null} funcs.funcModifyItem
* @param {function(LISTELEM): Void | null} funcs.funcDeleteItem
* @return {Array<LISTELEM>}
*/
function SyncHTMLList(sourceArray, listContainer, containerQuerySelector, funcs) {
if(!Array.isArray(sourceArray)) {
throw new TypeError("SyncHTMLList : Bad argument for sourceArray");
} else if (listContainer == null || !(listContainer instanceof Element)) {
throw new TypeError("SyncHTMLList : Bad argument for listContainer");
} else if ( funcs == null
|| typeof funcs.funcCheckIdentity !== "function" 
|| typeof funcs.funcNewItem !== "function" 
) {
throw new TypeError("SyncHTMLList : Bad argument for funcs");
}
//  Apply correct method for retrieving current live list of list elements.
let GetListElementsArray = (containerQuerySelector && typeof containerQuerySelector === "string")
? () => Array.from(listContainer.querySelectorAll(containerQuerySelector))
: () => Array.from(listContainer.children)
;
/**@type {Array<LISTELEM>} Copy of existing list as potential delist array, and splice as items are found in source. */
let delistedItems = GetListElementsArray();

/**@type {Array<LISTELEM>} New list, updated from source array. */
let orderedItems = [];
for(let i=0, n=sourceArray.length; i<n; i++) {
// Look for associated LI element using comparison function "funcCheckIdentity"
let itemIndex = delistedItems.findIndex( listElement => funcs.funcCheckIdentity(sourceArray[i], listElement) );
/**@type {LISTITEM} Current LI Element (whether found or created) */
let listElement = null;
//  If associated LI element found in container, associate to variable and remove from delist array.
//  Otherwise create element.
if (itemIndex < 0) {
listElement = funcs.funcNewItem(sourceArray[i]);
} else {
listElement = delistedItems[itemIndex];
delistedItems.splice(itemIndex,1);
}
//  Make any checks/modifcations specified by user on item.
if (funcs.funcModifyItem) funcs.funcModifyItem(sourceArray[i], listElement);
//  Add list element to final ordered list.
orderedItems.push(listElement);
}
//  Remove unfound LI elements from container, with user specified function if available.
if (funcs.funcDeleteItem && typeof funcs.funcDeleteItem === "function") {
delistedItems.forEach( listElement => funcs.funcDeleteItem(listElement) );
} else {
delistedItems.forEach( listElement => listElement.parentNode.removeChild(listElement) );
}
//  Reorganize list in DOM according to sourceArray/orderedItems.
for (let i=0, n=orderedItems.length; i < n; i++) {
let liveList = GetListElementsArray();
if(orderedItems[i] != liveList[i]) {
if(i >= liveList.length-1) {
listContainer.appendChild(orderedItems[i]);
} else {
listContainer.insertBefore(orderedItems[i], liveList[i+1]);
}
}
}
//  Return an array of the list elements.
return orderedItems;
}
var updateButton = document.getElementById("btnUpdate");
updateButton.addEventListener( 'click'
,   (event) => {
let updatedArray = [
{name: "john", colour: "red"},
{name: "phil", colour: "blue"},
{name: "jane", colour: "orange"},
{name: "suzie", colour: "orange"},
{name: "casey", colour: "purple"},
];

let list = document.getElementById("list");
let funcs = {};
funcs.funcNewItem = (data) => {
let el = document.createElement("LI");
el.dataset.name = data.name;
el.className = "listitem";
el.textContent = data.colour;
return el;
}
funcs.funcCheckIdentity = (data, element) => {
return data.name == element.dataset.name;
}
funcs.funcModifyItem = (data, element) => {
if(data.colour != element.textContent) {
element.textContent = data.colour;
}
}
SyncHTMLList(updatedArray, list, null, funcs);
}
,   {once: true}
);
<BODY>
<UL id="list">
<LI class="listItem" data-name="john">red</LI>
<LI class="listItem" data-name="mark">green</LI>
<LI class="listItem" data-name="jane">yellow</LI>
<LI class="listItem" data-name="suzie">orange</LI>
</UL>
<BUTTON id="btnUpdate">Update</BUTTON
</BODY>

最新更新