| Index: devtools-panel.js | 
| =================================================================== | 
| --- a/devtools-panel.js | 
| +++ b/devtools-panel.js | 
| @@ -23,17 +23,12 @@ | 
| (filterTypes) => | 
| { | 
| let filterTypesElem = document.getElementById("filter-type"); | 
| -    let filterStyleElem = document.createElement("style"); | 
| for (let type of filterTypes) | 
| { | 
| -      filterStyleElem.innerHTML += | 
| -        `#items[data-filter-type=${type}] tr:not([data-type=${type}])` + | 
| -        "{display: none;}"; | 
| let optionNode = document.createElement("option"); | 
| optionNode.appendChild(document.createTextNode(type)); | 
| filterTypesElem.appendChild(optionNode); | 
| } | 
| -    document.body.appendChild(filterStyleElem); | 
| }); | 
|  | 
| function generateFilter(request, domainSpecific) | 
| @@ -190,11 +185,81 @@ | 
| row.classList.remove("filtered-by-search"); | 
| } | 
|  | 
| +/** | 
| + * Return a generic <tr> placeholder | 
| + * double linked with the <tr> itself to simplify | 
| + * tbody swaps / filtering of undesired rows. | 
| + * @param {HTMLTableRowElement} tr | 
| + * @returns {Comment} | 
| + */ | 
| +function getPlaceholder(tr) | 
| +{ | 
| +  let placeholder = tr.placeholder; | 
| +  if (!placeholder) | 
| +  { | 
| +    placeholder = document.createComment("placeholder"); | 
| +    tr.placeholder = placeholder; | 
| +    placeholder.placeholder = tr; | 
| +  } | 
| +  return placeholder; | 
| +} | 
| + | 
| +/** | 
| + * Put back all hidden table rows and eventually | 
| + * filters those that do not match the state or type. | 
| + * @param {HTMLTableSectionElement} tbody | 
| + * @param {DOMStringMap} dataset | 
| + */ | 
| +function updateRows(tbody, dataset) | 
| +{ | 
| +  const {filterState, filterType} = dataset; | 
| + | 
| +  // create a list of possible CSS filter | 
| +  // excluding #background-items from the list of rows to consider | 
| +  const query = []; | 
| +  if (filterState) | 
| +  { | 
| +    query.push(`tr:not(#background-items):not([data-state="${filterState}"])`); | 
| +  } | 
| +  if (filterType) | 
| +  { | 
| +    query.push(`tr:not(#background-items):not([data-type="${filterType}"])`); | 
| +  } | 
| + | 
| +  // use filters to match nodes or retrieve them later on | 
| +  const selector = query.join(","); | 
| +  for (const node of tbody.childNodes) | 
| +  { | 
| +    // comments linked to table rows | 
| +    // that won't match selector will be used | 
| +    // to place their linked row back in the tbody | 
| +    if ( | 
| +      node.nodeType === Node.COMMENT_NODE && | 
| +      (!selector || !node.placeholder.matches(selector)) | 
| +    ) | 
| +    { | 
| +      tbody.replaceChild(node.placeholder, node); | 
| +    } | 
| +  } | 
| + | 
| +  // if there is a list of nodes to filter | 
| +  if (selector) | 
| +  { | 
| +    // per each of them put a placeholder inside the tbody | 
| +    for (const tr of tbody.querySelectorAll(selector)) | 
| +    { | 
| +      tbody.replaceChild(getPlaceholder(tr), tr); | 
| +    } | 
| +  } | 
| +} | 
| + | 
| document.addEventListener("DOMContentLoaded", () => | 
| { | 
| let container = document.getElementById("items"); | 
| let table = container.querySelector("tbody"); | 
| +  let bgItems = container.querySelector("#background-items"); | 
| let template = document.querySelector("template").content.firstElementChild; | 
| +  const records = []; | 
|  | 
| document.getElementById("reload").addEventListener("click", () => | 
| { | 
| @@ -204,11 +269,13 @@ | 
| document.getElementById("filter-state").addEventListener("change", (event) => | 
| { | 
| container.dataset.filterState = event.target.value; | 
| +    updateRows(table, container.dataset); | 
| }, false); | 
|  | 
| document.getElementById("filter-type").addEventListener("change", (event) => | 
| { | 
| container.dataset.filterType = event.target.value; | 
| +    updateRows(table, container.dataset); | 
| }, false); | 
|  | 
| ext.onMessage.addListener((message) => | 
| @@ -216,25 +283,37 @@ | 
| switch (message.type) | 
| { | 
| case "add-record": | 
| -        table.appendChild(createRecord(message.request, message.filter, | 
| -                                       template)); | 
| +        let record = createRecord(message.request, message.filter, template); | 
| +        records.push(record); | 
| +        if (message.request.docDomain == null) | 
| +        { | 
| +          table.appendChild(record); | 
| +        } | 
| +        else | 
| +        { | 
| +          table.insertBefore(record, bgItems); | 
| +        } | 
| break; | 
|  | 
| case "update-record": | 
| -        let oldRow = table.getElementsByTagName("tr")[message.index]; | 
| +        let oldRow = records[message.index]; | 
| let newRow = createRecord(message.request, message.filter, template); | 
| -        oldRow.parentNode.replaceChild(newRow, oldRow); | 
| +        records[message.index] = newRow; | 
| +        let liveRow = oldRow.parentNode ? oldRow : oldRow.placeholder; | 
| +        liveRow.parentNode.replaceChild(newRow, liveRow); | 
| newRow.classList.add("changed"); | 
| container.classList.add("has-changes"); | 
| break; | 
|  | 
| case "remove-record": | 
| -        let row = table.getElementsByTagName("tr")[message.index]; | 
| -        row.parentNode.removeChild(row); | 
| +        let row = records.splice(message.index, 1)[0]; | 
| +        let live = row.parentNode ? row : row.placeholder; | 
| +        live.parentNode.removeChild(live); | 
| container.classList.add("has-changes"); | 
| break; | 
|  | 
| case "reset": | 
| +        records.splice(0); | 
| table.innerHTML = ""; | 
| container.classList.remove("has-changes"); | 
| break; | 
|  |