mirror of
				https://github.com/isledecomp/isle.git
				synced 2025-10-28 10:54:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			868 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			868 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // reccmp.js
 | |
| /* global data */
 | |
| 
 | |
| // Unwrap array of functions into a dictionary with address as the key.
 | |
| const dataDict = Object.fromEntries(data.map(row => [row.address, row]));
 | |
| 
 | |
| function getDataByAddr(addr) {
 | |
|   return dataDict[addr];
 | |
| }
 | |
| 
 | |
| //
 | |
| // Pure functions
 | |
| //
 | |
| 
 | |
| function formatAsm(entries, addrOption) {
 | |
|   const output = [];
 | |
| 
 | |
|   const createTh = (text) => {
 | |
|     const th = document.createElement('th');
 | |
|     th.innerText = text;
 | |
|     return th;
 | |
|   };
 | |
| 
 | |
|   const createTd = (text, className = '') => {
 | |
|     const td = document.createElement('td');
 | |
|     td.innerText = text;
 | |
|     td.className = className;
 | |
|     return td;
 | |
|   };
 | |
| 
 | |
|   entries.forEach(obj => {
 | |
|     // These won't all be present. You get "both" for an equal node
 | |
|     // and orig/recomp for a diff.
 | |
|     const { both = [], orig = [], recomp = [] } = obj;
 | |
| 
 | |
|     output.push(...both.map(([addr, line, recompAddr]) => {
 | |
|       const tr = document.createElement('tr');
 | |
|       tr.appendChild(createTh(addr));
 | |
|       tr.appendChild(createTh(recompAddr));
 | |
|       tr.appendChild(createTd(line));
 | |
|       return tr;
 | |
|     }));
 | |
| 
 | |
|     output.push(...orig.map(([addr, line]) => {
 | |
|       const tr = document.createElement('tr');
 | |
|       tr.appendChild(createTh(addr));
 | |
|       tr.appendChild(createTh(''));
 | |
|       tr.appendChild(createTd(`-${line}`, 'diffneg'));
 | |
|       return tr;
 | |
|     }));
 | |
| 
 | |
|     output.push(...recomp.map(([addr, line]) => {
 | |
|       const tr = document.createElement('tr');
 | |
|       tr.appendChild(createTh(''));
 | |
|       tr.appendChild(createTh(addr));
 | |
|       tr.appendChild(createTd(`+${line}`, 'diffpos'));
 | |
|       return tr;
 | |
|     }));
 | |
|   });
 | |
| 
 | |
|   return output;
 | |
| }
 | |
| 
 | |
| // Special internal values to ensure this sort order for matching column:
 | |
| // 1. Stub
 | |
| // 2. Any match percentage [0.0, 1.0)
 | |
| // 3. Effective match
 | |
| // 4. Actual 100% match
 | |
| function matchingColAdjustment(row) {
 | |
|   if ('stub' in row) {
 | |
|     return -1;
 | |
|   }
 | |
| 
 | |
|   if ('effective' in row) {
 | |
|     return 1.0;
 | |
|   }
 | |
| 
 | |
|   if (row.matching === 1.0) {
 | |
|     return 1000;
 | |
|   }
 | |
| 
 | |
|   return row.matching;
 | |
| }
 | |
| 
 | |
| function getCppClass(str) {
 | |
|   const idx = str.indexOf('::');
 | |
|   if (idx !== -1) {
 | |
|     return str.slice(0, idx);
 | |
|   }
 | |
| 
 | |
|   return str;
 | |
| }
 | |
| 
 | |
| // Clamp string length to specified length and pad with ellipsis
 | |
| function stringTruncate(str, maxlen = 20) {
 | |
|   str = getCppClass(str);
 | |
|   if (str.length > maxlen) {
 | |
|     return `${str.slice(0, maxlen)}...`;
 | |
|   }
 | |
| 
 | |
|   return str;
 | |
| }
 | |
| 
 | |
| function getMatchPercentText(row) {
 | |
|   if ('stub' in row) {
 | |
|     return 'stub';
 | |
|   }
 | |
| 
 | |
|   if ('effective' in row) {
 | |
|     return '100.00%*';
 | |
|   }
 | |
| 
 | |
|   return (row.matching * 100).toFixed(2) + '%';
 | |
| }
 | |
| 
 | |
| function countDiffs(row) {
 | |
|   const { diff = '' } = row;
 | |
|   if (diff === '') {
 | |
|     return '';
 | |
|   }
 | |
| 
 | |
|   const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
 | |
|   const diffLength = diffs.filter(d => !('both' in d)).length;
 | |
|   const diffWord = diffLength === 1 ? 'diff' : 'diffs';
 | |
|   return diffLength === 0 ? '' : `${diffLength} ${diffWord}`;
 | |
| }
 | |
| 
 | |
| // Helper for this set/remove attribute block
 | |
| function setBooleanAttribute(element, attribute, value) {
 | |
|   if (value) {
 | |
|     element.setAttribute(attribute, '');
 | |
|   } else {
 | |
|     element.removeAttribute(attribute);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function copyToClipboard(value) {
 | |
|   navigator.clipboard.writeText(value);
 | |
| }
 | |
| 
 | |
| const PAGE_SIZE = 200;
 | |
| 
 | |
| //
 | |
| // Global state
 | |
| //
 | |
| 
 | |
| class ListingState {
 | |
|   constructor() {
 | |
|     this._query = '';
 | |
|     this._sortCol = 'address';
 | |
|     this._filterType = 1;
 | |
|     this._sortDesc = false;
 | |
|     this._hidePerfect = false;
 | |
|     this._hideStub = false;
 | |
|     this._showRecomp = false;
 | |
|     this._expanded = {};
 | |
|     this._page = 0;
 | |
| 
 | |
|     this._listeners = [];
 | |
| 
 | |
|     this._results = [];
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   addListener(fn) {
 | |
|     this._listeners.push(fn);
 | |
|   }
 | |
| 
 | |
|   callListeners() {
 | |
|     for (const fn of this._listeners) {
 | |
|       fn();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   isExpanded(addr) {
 | |
|     return addr in this._expanded;
 | |
|   }
 | |
| 
 | |
|   toggleExpanded(addr) {
 | |
|     this.setExpanded(addr, !this.isExpanded(addr));
 | |
|   }
 | |
| 
 | |
|   setExpanded(addr, value) {
 | |
|     if (value) {
 | |
|       this._expanded[addr] = true;
 | |
|     } else {
 | |
|       delete this._expanded[addr];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   updateResults() {
 | |
|     const filterFn = this.rowFilterFn.bind(this);
 | |
|     const sortFn = this.rowSortFn.bind(this);
 | |
| 
 | |
|     this._results = data.filter(filterFn).sort(sortFn);
 | |
| 
 | |
|     // Set _page directly to avoid double call to listeners.
 | |
|     this._page = this.pageClamp(this.page);
 | |
|     this.callListeners();
 | |
|   }
 | |
| 
 | |
|   pageSlice() {
 | |
|     return this._results.slice(this.page * PAGE_SIZE, (this.page + 1) * PAGE_SIZE);
 | |
|   }
 | |
| 
 | |
|   resultsCount() {
 | |
|     return this._results.length;
 | |
|   }
 | |
| 
 | |
|   pageCount() {
 | |
|     return Math.ceil(this._results.length / PAGE_SIZE);
 | |
|   }
 | |
| 
 | |
|   maxPage() {
 | |
|     return Math.max(0, this.pageCount() - 1);
 | |
|   }
 | |
| 
 | |
|   // A list showing the range of each page based on the sort column and direction.
 | |
|   pageHeadings() {
 | |
|     if (this._results.length === 0) {
 | |
|       return [];
 | |
|     }
 | |
| 
 | |
|     const headings = [];
 | |
| 
 | |
|     for (let i = 0; i < this.pageCount(); i++) {
 | |
|       const startIdx = i * PAGE_SIZE;
 | |
|       const endIdx = Math.min(this._results.length, ((i + 1) * PAGE_SIZE)) - 1;
 | |
| 
 | |
|       let start = this._results[startIdx][this.sortCol];
 | |
|       let end = this._results[endIdx][this.sortCol];
 | |
| 
 | |
|       if (this.sortCol === 'matching') {
 | |
|         start = getMatchPercentText(this._results[startIdx]);
 | |
|         end = getMatchPercentText(this._results[endIdx]);
 | |
|       }
 | |
| 
 | |
|       headings.push([i, stringTruncate(start), stringTruncate(end)]);
 | |
|     }
 | |
| 
 | |
|     return headings;
 | |
|   }
 | |
| 
 | |
|   rowFilterFn(row) {
 | |
|     // Destructuring sets defaults for optional values from this object.
 | |
|     const {
 | |
|       effective = false,
 | |
|       stub = false,
 | |
|       diff = '',
 | |
|       name,
 | |
|       address,
 | |
|       matching
 | |
|     } = row;
 | |
| 
 | |
|     if (this.hidePerfect && (effective || matching >= 1)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (this.hideStub && stub) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (this.query === '') {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     // Name/addr search
 | |
|     if (this.filterType === 1) {
 | |
|       return (
 | |
|         address.includes(this.query) ||
 | |
|         name.toLowerCase().includes(this.query)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // no diff for review.
 | |
|     if (diff === '') {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // special matcher for combined diff
 | |
|     const anyLineMatch = ([addr, line]) => line.toLowerCase().trim().includes(this.query);
 | |
| 
 | |
|     // Flatten all diff groups for the search
 | |
|     const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
 | |
|     for (const subgroup of diffs) {
 | |
|       const { both = [], orig = [], recomp = [] } = subgroup;
 | |
| 
 | |
|       // If search includes context
 | |
|       if (this.filterType === 2 && both.some(anyLineMatch)) {
 | |
|         return true;
 | |
|       }
 | |
| 
 | |
|       if (orig.some(anyLineMatch) || recomp.some(anyLineMatch)) {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   rowSortFn(rowA, rowB) {
 | |
|     const valA = this.sortCol === 'matching'
 | |
|       ? matchingColAdjustment(rowA)
 | |
|       : rowA[this.sortCol];
 | |
| 
 | |
|     const valB = this.sortCol === 'matching'
 | |
|       ? matchingColAdjustment(rowB)
 | |
|       : rowB[this.sortCol];
 | |
| 
 | |
|     if (valA > valB) {
 | |
|       return this.sortDesc ? -1 : 1;
 | |
|     } else if (valA < valB) {
 | |
|       return this.sortDesc ? 1 : -1;
 | |
|     }
 | |
| 
 | |
|     return 0;
 | |
|   }
 | |
| 
 | |
|   pageClamp(page) {
 | |
|     return Math.max(0, Math.min(page, this.maxPage()));
 | |
|   }
 | |
| 
 | |
|   get page() {
 | |
|     return this._page;
 | |
|   }
 | |
| 
 | |
|   set page(page) {
 | |
|     this._page = this.pageClamp(page);
 | |
|     this.callListeners();
 | |
|   }
 | |
| 
 | |
|   get filterType() {
 | |
|     return parseInt(this._filterType);
 | |
|   }
 | |
| 
 | |
|   set filterType(value) {
 | |
|     value = parseInt(value);
 | |
|     if (value >= 1 && value <= 3) {
 | |
|       this._filterType = value;
 | |
|     }
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   get query() {
 | |
|     return this._query;
 | |
|   }
 | |
| 
 | |
|   set query(value) {
 | |
|     // Normalize search string
 | |
|     this._query = value.toLowerCase().trim();
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   get showRecomp() {
 | |
|     return this._showRecomp;
 | |
|   }
 | |
| 
 | |
|   set showRecomp(value) {
 | |
|     // Don't sort by the recomp column we are about to hide
 | |
|     if (!value && this.sortCol === 'recomp') {
 | |
|       this._sortCol = 'address';
 | |
|     }
 | |
| 
 | |
|     this._showRecomp = value;
 | |
|     this.callListeners();
 | |
|   }
 | |
| 
 | |
|   get sortCol() {
 | |
|     return this._sortCol;
 | |
|   }
 | |
| 
 | |
|   set sortCol(column) {
 | |
|     if (column === this._sortCol) {
 | |
|       this._sortDesc = !this._sortDesc;
 | |
|     } else {
 | |
|       this._sortCol = column;
 | |
|     }
 | |
| 
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   get sortDesc() {
 | |
|     return this._sortDesc;
 | |
|   }
 | |
| 
 | |
|   set sortDesc(value) {
 | |
|     this._sortDesc = value;
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   get hidePerfect() {
 | |
|     return this._hidePerfect;
 | |
|   }
 | |
| 
 | |
|   set hidePerfect(value) {
 | |
|     this._hidePerfect = value;
 | |
|     this.updateResults();
 | |
|   }
 | |
| 
 | |
|   get hideStub() {
 | |
|     return this._hideStub;
 | |
|   }
 | |
| 
 | |
|   set hideStub(value) {
 | |
|     this._hideStub = value;
 | |
|     this.updateResults();
 | |
|   }
 | |
| }
 | |
| 
 | |
| const appState = new ListingState();
 | |
| 
 | |
| //
 | |
| // Custom elements
 | |
| //
 | |
| 
 | |
| // Sets sort indicator arrow based on element attributes.
 | |
| class SortIndicator extends window.HTMLElement {
 | |
|   static observedAttributes = ['data-sort'];
 | |
| 
 | |
|   attributeChangedCallback(name, oldValue, newValue) {
 | |
|     if (newValue === null) {
 | |
|       // Reserve space for blank indicator so column width stays the same
 | |
|       this.innerHTML = ' ';
 | |
|     } else {
 | |
|       this.innerHTML = newValue === 'asc' ? '▲' : '▼';
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class FuncRow extends window.HTMLElement {
 | |
|   connectedCallback() {
 | |
|     if (this.shadowRoot !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const template = document.querySelector('template#funcrow-template').content;
 | |
|     const shadow = this.attachShadow({ mode: 'open' });
 | |
|     shadow.appendChild(template.cloneNode(true));
 | |
|     shadow.querySelector(':host > div[data-col="name"]').addEventListener('click', evt => {
 | |
|       this.dispatchEvent(new Event('name-click'));
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   get address() {
 | |
|     return this.getAttribute('data-address');
 | |
|   }
 | |
| }
 | |
| 
 | |
| class NoDiffMessage extends window.HTMLElement {
 | |
|   connectedCallback() {
 | |
|     if (this.shadowRoot !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const template = document.querySelector('template#nodiff-template').content;
 | |
|     const shadow = this.attachShadow({ mode: 'open' });
 | |
|     shadow.appendChild(template.cloneNode(true));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CanCopy extends window.HTMLElement {
 | |
|   connectedCallback() {
 | |
|     if (this.shadowRoot !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const template = document.querySelector('template#can-copy-template').content;
 | |
|     const shadow = this.attachShadow({ mode: 'open' });
 | |
|     shadow.appendChild(template.cloneNode(true));
 | |
| 
 | |
|     const el = shadow.querySelector('slot').assignedNodes()[0];
 | |
|     el.addEventListener('mouseout', evt => { this.copied = false; });
 | |
|     el.addEventListener('click', evt => {
 | |
|       copyToClipboard(evt.target.textContent);
 | |
|       this.copied = true;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   get copied() {
 | |
|     return this.getAttribute('copied');
 | |
|   }
 | |
| 
 | |
|   set copied(value) {
 | |
|     if (value) {
 | |
|       setTimeout(() => { this.copied = false; }, 2000);
 | |
|     }
 | |
|     setBooleanAttribute(this, 'copied', value);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Displays asm diff for the given @data-address value.
 | |
| class DiffRow extends window.HTMLElement {
 | |
|   connectedCallback() {
 | |
|     if (this.shadowRoot !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const template = document.querySelector('template#diffrow-template').content;
 | |
|     const shadow = this.attachShadow({ mode: 'open' });
 | |
|     shadow.appendChild(template.cloneNode(true));
 | |
|   }
 | |
| 
 | |
|   get address() {
 | |
|     return this.getAttribute('data-address');
 | |
|   }
 | |
| 
 | |
|   set address(value) {
 | |
|     this.setAttribute('data-address', value);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DiffDisplayOptions extends window.HTMLElement {
 | |
|   static observedAttributes = ['data-option'];
 | |
| 
 | |
|   connectedCallback() {
 | |
|     if (this.shadowRoot !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const shadow = this.attachShadow({ mode: 'open' });
 | |
|     shadow.innerHTML = `
 | |
|       <style>
 | |
|         fieldset {
 | |
|           align-items: center;
 | |
|           display: flex;
 | |
|           margin-bottom: 20px;
 | |
|         }
 | |
| 
 | |
|         label {
 | |
|           margin-right: 10px;
 | |
|           user-select: none;
 | |
|         }
 | |
| 
 | |
|         label, input {
 | |
|           cursor: pointer;
 | |
|         }
 | |
|       </style>
 | |
|       <fieldset>
 | |
|         <legend>Address display:</legend>
 | |
|         <input type="radio" id="showNone" name="addrDisplay" value=0>
 | |
|         <label for="showNone">None</label>
 | |
|         <input type="radio" id="showOrig" name="addrDisplay" value=1>
 | |
|         <label for="showOrig">Original</label>
 | |
|         <input type="radio" id="showBoth" name="addrDisplay" value=2>
 | |
|         <label for="showBoth">Both</label>
 | |
|       </fieldset>`;
 | |
| 
 | |
|     shadow.querySelectorAll('input[type=radio]').forEach(radio => {
 | |
|       const checked = this.option === radio.getAttribute('value');
 | |
|       setBooleanAttribute(radio, 'checked', checked);
 | |
| 
 | |
|       radio.addEventListener('change', evt => (this.option = evt.target.value));
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   set option(value) {
 | |
|     this.setAttribute('data-option', parseInt(value));
 | |
|   }
 | |
| 
 | |
|   get option() {
 | |
|     return this.getAttribute('data-option') ?? 1;
 | |
|   }
 | |
| 
 | |
|   attributeChangedCallback(name, oldValue, newValue) {
 | |
|     if (name !== 'data-option') {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.dispatchEvent(new Event('change'));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DiffDisplay extends window.HTMLElement {
 | |
|   static observedAttributes = ['data-option'];
 | |
| 
 | |
|   connectedCallback() {
 | |
|     if (this.querySelector('diff-display-options') !== null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const optControl = new DiffDisplayOptions();
 | |
|     optControl.option = this.option;
 | |
|     optControl.addEventListener('change', evt => (this.option = evt.target.option));
 | |
|     this.appendChild(optControl);
 | |
| 
 | |
|     const div = document.createElement('div');
 | |
|     const obj = getDataByAddr(this.address);
 | |
| 
 | |
|     const createHeaderLine = (text, className) => {
 | |
|       const div = document.createElement('div');
 | |
|       div.textContent = text;
 | |
|       div.className = className;
 | |
|       return div;
 | |
|     };
 | |
| 
 | |
|     const groups = obj.diff;
 | |
|     groups.forEach(([slug, subgroups]) => {
 | |
|       const secondTable = document.createElement('table');
 | |
|       secondTable.classList.add('diffTable');
 | |
| 
 | |
|       const hdr = document.createElement('div');
 | |
|       hdr.appendChild(createHeaderLine('---', 'diffneg'));
 | |
|       hdr.appendChild(createHeaderLine('+++', 'diffpos'));
 | |
|       hdr.appendChild(createHeaderLine(slug, 'diffslug'));
 | |
|       div.appendChild(hdr);
 | |
| 
 | |
|       const tbody = document.createElement('tbody');
 | |
|       secondTable.appendChild(tbody);
 | |
| 
 | |
|       const diffs = formatAsm(subgroups, this.option);
 | |
|       for (const el of diffs) {
 | |
|         tbody.appendChild(el);
 | |
|       }
 | |
| 
 | |
|       div.appendChild(secondTable);
 | |
|     });
 | |
| 
 | |
|     this.appendChild(div);
 | |
|   }
 | |
| 
 | |
|   get address() {
 | |
|     return this.getAttribute('data-address');
 | |
|   }
 | |
| 
 | |
|   set address(value) {
 | |
|     this.setAttribute('data-address', value);
 | |
|   }
 | |
| 
 | |
|   get option() {
 | |
|     return this.getAttribute('data-option') ?? 1;
 | |
|   }
 | |
| 
 | |
|   set option(value) {
 | |
|     this.setAttribute('data-option', value);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ListingOptions extends window.HTMLElement {
 | |
|   constructor() {
 | |
|     super();
 | |
| 
 | |
|     // Register to receive updates
 | |
|     appState.addListener(() => this.onUpdate());
 | |
| 
 | |
|     const input = this.querySelector('input[type=search]');
 | |
|     input.oninput = evt => (appState.query = evt.target.value);
 | |
| 
 | |
|     const hidePerf = this.querySelector('input#cbHidePerfect');
 | |
|     hidePerf.onchange = evt => (appState.hidePerfect = evt.target.checked);
 | |
|     hidePerf.checked = appState.hidePerfect;
 | |
| 
 | |
|     const hideStub = this.querySelector('input#cbHideStub');
 | |
|     hideStub.onchange = evt => (appState.hideStub = evt.target.checked);
 | |
|     hideStub.checked = appState.hideStub;
 | |
| 
 | |
|     const showRecomp = this.querySelector('input#cbShowRecomp');
 | |
|     showRecomp.onchange = evt => (appState.showRecomp = evt.target.checked);
 | |
|     showRecomp.checked = appState.showRecomp;
 | |
| 
 | |
|     this.querySelector('button#pagePrev').addEventListener('click', evt => {
 | |
|       appState.page = appState.page - 1;
 | |
|     });
 | |
| 
 | |
|     this.querySelector('button#pageNext').addEventListener('click', evt => {
 | |
|       appState.page = appState.page + 1;
 | |
|     });
 | |
| 
 | |
|     this.querySelector('select#pageSelect').addEventListener('change', evt => {
 | |
|       appState.page = evt.target.value;
 | |
|     });
 | |
| 
 | |
|     this.querySelectorAll('input[name=filterType]').forEach(radio => {
 | |
|       const checked = appState.filterType === parseInt(radio.getAttribute('value'));
 | |
|       setBooleanAttribute(radio, 'checked', checked);
 | |
| 
 | |
|       radio.onchange = evt => (appState.filterType = radio.getAttribute('value'));
 | |
|     });
 | |
| 
 | |
|     this.onUpdate();
 | |
|   }
 | |
| 
 | |
|   onUpdate() {
 | |
|     // Update input placeholder based on search type
 | |
|     this.querySelector('input[type=search]').placeholder = appState.filterType === 1
 | |
|       ? 'Search for offset or function name...'
 | |
|       : 'Search for instruction...';
 | |
| 
 | |
|     // Update page number and max page
 | |
|     this.querySelector('fieldset#pageDisplay > legend').textContent = `Page ${appState.page + 1} of ${Math.max(1, appState.pageCount())}`;
 | |
| 
 | |
|     // Disable prev/next buttons on first/last page
 | |
|     setBooleanAttribute(this.querySelector('button#pagePrev'), 'disabled', appState.page === 0);
 | |
|     setBooleanAttribute(this.querySelector('button#pageNext'), 'disabled', appState.page === appState.maxPage());
 | |
| 
 | |
|     // Update page select dropdown
 | |
|     const pageSelect = this.querySelector('select#pageSelect');
 | |
|     setBooleanAttribute(pageSelect, 'disabled', appState.resultsCount() === 0);
 | |
|     pageSelect.innerHTML = '';
 | |
| 
 | |
|     if (appState.resultsCount() === 0) {
 | |
|       const opt = document.createElement('option');
 | |
|       opt.textContent = '- no results -';
 | |
|       pageSelect.appendChild(opt);
 | |
|     } else {
 | |
|       for (const row of appState.pageHeadings()) {
 | |
|         const opt = document.createElement('option');
 | |
|         opt.value = row[0];
 | |
|         if (appState.page === row[0]) {
 | |
|           opt.setAttribute('selected', '');
 | |
|         }
 | |
| 
 | |
|         const [start, end] = [row[1], row[2]];
 | |
| 
 | |
|         opt.textContent = `${appState.sortCol}: ${start} to ${end}`;
 | |
|         pageSelect.appendChild(opt);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Update row count
 | |
|     this.querySelector('#rowcount').textContent = `${appState.resultsCount()}`;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Main application.
 | |
| class ListingTable extends window.HTMLElement {
 | |
|   constructor() {
 | |
|     super();
 | |
| 
 | |
|     // Register to receive updates
 | |
|     appState.addListener(() => this.somethingChanged());
 | |
|   }
 | |
| 
 | |
|   setDiffRow(address, shouldExpand) {
 | |
|     const tbody = this.querySelector('tbody');
 | |
|     const funcrow = tbody.querySelector(`func-row[data-address="${address}"]`);
 | |
|     if (funcrow === null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const existing = tbody.querySelector(`diff-row[data-address="${address}"]`);
 | |
|     if (existing !== null) {
 | |
|       if (!shouldExpand) {
 | |
|         tbody.removeChild(existing);
 | |
|       }
 | |
| 
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const diffrow = document.createElement('diff-row');
 | |
|     diffrow.address = address;
 | |
| 
 | |
|     // Decide what goes inside the diff row.
 | |
|     const obj = getDataByAddr(address);
 | |
| 
 | |
|     if ('stub' in obj) {
 | |
|       const msg = document.createElement('no-diff');
 | |
|       const p = document.createElement('div');
 | |
|       p.innerText = 'Stub. No diff.';
 | |
|       msg.appendChild(p);
 | |
|       diffrow.appendChild(msg);
 | |
|     } else if (obj.diff.length === 0) {
 | |
|       const msg = document.createElement('no-diff');
 | |
|       const p = document.createElement('div');
 | |
|       p.innerText = 'Identical function - no diff';
 | |
|       msg.appendChild(p);
 | |
|       diffrow.appendChild(msg);
 | |
|     } else {
 | |
|       const dd = new DiffDisplay();
 | |
|       dd.option = '1';
 | |
|       dd.address = address;
 | |
|       diffrow.appendChild(dd);
 | |
|     }
 | |
| 
 | |
|     // Insert the diff row after the parent func row.
 | |
|     tbody.insertBefore(diffrow, funcrow.nextSibling);
 | |
|   }
 | |
| 
 | |
|   connectedCallback() {
 | |
|     const thead = this.querySelector('thead');
 | |
|     const headers = thead.querySelectorAll('th:not([data-no-sort])'); // TODO
 | |
|     headers.forEach(th => {
 | |
|       const col = th.getAttribute('data-col');
 | |
|       if (col) {
 | |
|         const span = th.querySelector('span');
 | |
|         if (span) {
 | |
|           span.addEventListener('click', evt => { appState.sortCol = col; });
 | |
|         }
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     this.somethingChanged();
 | |
|   }
 | |
| 
 | |
|   somethingChanged() {
 | |
|     // Toggle recomp/diffs column
 | |
|     setBooleanAttribute(this.querySelector('table'), 'show-recomp', appState.showRecomp);
 | |
|     this.querySelectorAll('func-row[data-address]').forEach(row => {
 | |
|       setBooleanAttribute(row, 'show-recomp', appState.showRecomp);
 | |
|     });
 | |
| 
 | |
|     const thead = this.querySelector('thead');
 | |
|     const headers = thead.querySelectorAll('th');
 | |
| 
 | |
|     // Update sort indicator
 | |
|     headers.forEach(th => {
 | |
|       const col = th.getAttribute('data-col');
 | |
|       const indicator = th.querySelector('sort-indicator');
 | |
|       if (indicator === null) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (appState.sortCol === col) {
 | |
|         indicator.setAttribute('data-sort', appState.sortDesc ? 'desc' : 'asc');
 | |
|       } else {
 | |
|         indicator.removeAttribute('data-sort');
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // Add the rows
 | |
|     const tbody = this.querySelector('tbody');
 | |
|     tbody.innerHTML = ''; // ?
 | |
| 
 | |
|     for (const obj of appState.pageSlice()) {
 | |
|       const row = document.createElement('func-row');
 | |
|       row.setAttribute('data-address', obj.address); // ?
 | |
|       row.addEventListener('name-click', evt => {
 | |
|         appState.toggleExpanded(obj.address);
 | |
|         this.setDiffRow(obj.address, appState.isExpanded(obj.address));
 | |
|       });
 | |
|       setBooleanAttribute(row, 'show-recomp', appState.showRecomp);
 | |
|       setBooleanAttribute(row, 'expanded', appState.isExpanded(row));
 | |
| 
 | |
|       const items = [
 | |
|         ['address', obj.address],
 | |
|         ['recomp', obj.recomp],
 | |
|         ['name', obj.name],
 | |
|         ['diffs', countDiffs(obj)],
 | |
|         ['matching', getMatchPercentText(obj)]
 | |
|       ];
 | |
| 
 | |
|       items.forEach(([slotName, content]) => {
 | |
|         const div = document.createElement('span');
 | |
|         div.setAttribute('slot', slotName);
 | |
|         div.innerText = content;
 | |
|         row.appendChild(div);
 | |
|       });
 | |
| 
 | |
|       tbody.appendChild(row);
 | |
| 
 | |
|       if (appState.isExpanded(obj.address)) {
 | |
|         this.setDiffRow(obj.address, true);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| window.onload = () => {
 | |
|   window.customElements.define('listing-table', ListingTable);
 | |
|   window.customElements.define('listing-options', ListingOptions);
 | |
|   window.customElements.define('diff-display', DiffDisplay);
 | |
|   window.customElements.define('diff-display-options', DiffDisplayOptions);
 | |
|   window.customElements.define('sort-indicator', SortIndicator);
 | |
|   window.customElements.define('func-row', FuncRow);
 | |
|   window.customElements.define('diff-row', DiffRow);
 | |
|   window.customElements.define('no-diff', NoDiffMessage);
 | |
|   window.customElements.define('can-copy', CanCopy);
 | |
| };
 | 
