|
| 1 | +import { html, TemplateResult } from 'lit-element'; |
| 2 | +import { repeat } from 'lit-html/directives/repeat'; |
| 3 | +import { get, translate } from 'lit-translate'; |
| 4 | + |
| 5 | +import '@material/mwc-list'; |
| 6 | +import '@material/mwc-list/mwc-list-item'; |
| 7 | +import '@material/mwc-icon'; |
| 8 | + |
| 9 | +import { identity } from '../foundation.js'; |
| 10 | +import { nothing } from 'lit-html'; |
| 11 | + |
| 12 | +export type Diff<T> = |
| 13 | + | { oldValue: T; newValue: null } |
| 14 | + | { oldValue: null; newValue: T } |
| 15 | + | { oldValue: T; newValue: T }; |
| 16 | + |
| 17 | +/** |
| 18 | + * Returns the description of the Element that differs. |
| 19 | + * |
| 20 | + * @param element - The Element to retrieve the identifier from. |
| 21 | + */ |
| 22 | +function describe(element: Element): string { |
| 23 | + const id = identity(element); |
| 24 | + return typeof id === 'string' ? id : get('unidentifiable'); |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Check if there are any attribute values changed between the two elements. |
| 29 | + * |
| 30 | + * @param elementToBeCompared - The element to check for differences. |
| 31 | + * @param elementToCompareAgainst - The element used to check against. |
| 32 | + */ |
| 33 | +export function diffSclAttributes( |
| 34 | + elementToBeCompared: Element, |
| 35 | + elementToCompareAgainst: Element |
| 36 | +): [string, Diff<string>][] { |
| 37 | + const attrDiffs: [string, Diff<string>][] = []; |
| 38 | + |
| 39 | + // First check if there is any text inside the element and there should be no child elements. |
| 40 | + const newText = elementToBeCompared.textContent?.trim() ?? ''; |
| 41 | + const oldText = elementToCompareAgainst.textContent?.trim() ?? ''; |
| 42 | + if ( |
| 43 | + elementToBeCompared.childElementCount === 0 && |
| 44 | + elementToCompareAgainst.childElementCount === 0 && |
| 45 | + newText !== oldText |
| 46 | + ) { |
| 47 | + attrDiffs.push(['value', { newValue: newText, oldValue: oldText }]); |
| 48 | + } |
| 49 | + |
| 50 | + // Next check if there are any difference between attributes. |
| 51 | + const attributeNames = new Set( |
| 52 | + elementToCompareAgainst |
| 53 | + .getAttributeNames() |
| 54 | + .concat(elementToBeCompared.getAttributeNames()) |
| 55 | + ); |
| 56 | + for (const name of attributeNames) { |
| 57 | + if ( |
| 58 | + elementToCompareAgainst.getAttribute(name) !== |
| 59 | + elementToBeCompared.getAttribute(name) |
| 60 | + ) { |
| 61 | + attrDiffs.push([ |
| 62 | + name, |
| 63 | + <Diff<string>>{ |
| 64 | + newValue: elementToBeCompared.getAttribute(name), |
| 65 | + oldValue: elementToCompareAgainst.getAttribute(name), |
| 66 | + }, |
| 67 | + ]); |
| 68 | + } |
| 69 | + } |
| 70 | + return attrDiffs; |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Function to retrieve the identity to compare 2 children on the same level. |
| 75 | + * This means we only need to last part of the Identity string to compare the children. |
| 76 | + * |
| 77 | + * @param element - The element to retrieve the identity from. |
| 78 | + */ |
| 79 | +export function identityForCompare(element: Element): string | number { |
| 80 | + let identityOfElement = identity(element); |
| 81 | + if (typeof identityOfElement === 'string') { |
| 82 | + identityOfElement = identityOfElement.split('>').pop() ?? ''; |
| 83 | + } |
| 84 | + return identityOfElement; |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * Custom method for comparing to check if 2 elements are the same. Because they are on the same level |
| 89 | + * we don't need to compare the full identity, we just compare the part of the Element itself. |
| 90 | + * |
| 91 | + * <b>Remark</b>Private elements are already filtered out, so we don't need to bother them. |
| 92 | + * |
| 93 | + * @param newValue - The new element to compare with the old element. |
| 94 | + * @param oldValue - The old element to which the new element is compared. |
| 95 | + */ |
| 96 | +export function isSame(newValue: Element, oldValue: Element): boolean { |
| 97 | + return ( |
| 98 | + newValue.tagName === oldValue.tagName && |
| 99 | + identityForCompare(newValue) === identityForCompare(oldValue) |
| 100 | + ); |
| 101 | +} |
| 102 | + |
| 103 | +/** |
| 104 | + * List of all differences between children elements that both old and new element have. |
| 105 | + * The list contains children both elements have and children that were added or removed |
| 106 | + * from the new element. |
| 107 | + * <b>Remark</b>: Private elements are ignored. |
| 108 | + * |
| 109 | + * @param elementToBeCompared - The element to check for differences. |
| 110 | + * @param elementToCompareAgainst - The element used to check against. |
| 111 | + */ |
| 112 | +export function diffSclChilds( |
| 113 | + elementToBeCompared: Element, |
| 114 | + elementToCompareAgainst: Element |
| 115 | +): Diff<Element>[] { |
| 116 | + const childDiffs: Diff<Element>[] = []; |
| 117 | + const childrenToBeCompared = Array.from(elementToBeCompared.children); |
| 118 | + const childrenToCompareTo = Array.from(elementToCompareAgainst.children); |
| 119 | + |
| 120 | + childrenToBeCompared.forEach(newElement => { |
| 121 | + if (!newElement.closest('Private')) { |
| 122 | + const twinIndex = childrenToCompareTo.findIndex(ourChild => |
| 123 | + isSame(newElement, ourChild) |
| 124 | + ); |
| 125 | + const oldElement = twinIndex > -1 ? childrenToCompareTo[twinIndex] : null; |
| 126 | + |
| 127 | + if (oldElement) { |
| 128 | + childrenToCompareTo.splice(twinIndex, 1); |
| 129 | + childDiffs.push({ newValue: newElement, oldValue: oldElement }); |
| 130 | + } else { |
| 131 | + childDiffs.push({ newValue: newElement, oldValue: null }); |
| 132 | + } |
| 133 | + } |
| 134 | + }); |
| 135 | + childrenToCompareTo.forEach(oldElement => { |
| 136 | + if (!oldElement.closest('Private')) { |
| 137 | + childDiffs.push({ newValue: null, oldValue: oldElement }); |
| 138 | + } |
| 139 | + }); |
| 140 | + return childDiffs; |
| 141 | +} |
| 142 | + |
| 143 | +/** |
| 144 | + * Generate HTML (TemplateResult) containing all the differences between the two elements passed. |
| 145 | + * If null is returned there are no differences between the two elements. |
| 146 | + * |
| 147 | + * @param elementToBeCompared - The element to check for differences. |
| 148 | + * @param elementToCompareAgainst - The element used to check against. |
| 149 | + */ |
| 150 | +export function renderDiff( |
| 151 | + elementToBeCompared: Element, |
| 152 | + elementToCompareAgainst: Element |
| 153 | +): TemplateResult | null { |
| 154 | + // Determine the ID from the current tag. These can be numbers or strings. |
| 155 | + let idTitle: string | undefined = identity(elementToBeCompared).toString(); |
| 156 | + if (idTitle === 'NaN') { |
| 157 | + idTitle = undefined; |
| 158 | + } |
| 159 | + |
| 160 | + // First get all differences in attributes and text for the current 2 elements. |
| 161 | + const attrDiffs: [string, Diff<string>][] = diffSclAttributes( |
| 162 | + elementToBeCompared, |
| 163 | + elementToCompareAgainst |
| 164 | + ); |
| 165 | + // Next check which elements are added, deleted or in both elements. |
| 166 | + const childDiffs: Diff<Element>[] = diffSclChilds( |
| 167 | + elementToBeCompared, |
| 168 | + elementToCompareAgainst |
| 169 | + ); |
| 170 | + |
| 171 | + const childAddedOrDeleted: Diff<Element>[] = []; |
| 172 | + const childToCompare: Diff<Element>[] = []; |
| 173 | + childDiffs.forEach(diff => { |
| 174 | + if (!diff.oldValue || !diff.newValue) { |
| 175 | + childAddedOrDeleted.push(diff); |
| 176 | + } else { |
| 177 | + childToCompare.push(diff); |
| 178 | + } |
| 179 | + }); |
| 180 | + |
| 181 | + // These children exist in both old and new element, let's check if there are any difference in the children. |
| 182 | + const childToCompareTemplates = childToCompare |
| 183 | + .map(diff => renderDiff(diff.newValue!, diff.oldValue!)) |
| 184 | + .filter(result => result !== null); |
| 185 | + |
| 186 | + // If there are difference generate the HTML otherwise just return null. |
| 187 | + if ( |
| 188 | + childToCompareTemplates.length > 0 || |
| 189 | + attrDiffs.length > 0 || |
| 190 | + childAddedOrDeleted.length > 0 |
| 191 | + ) { |
| 192 | + return html` ${attrDiffs.length > 0 || childAddedOrDeleted.length > 0 |
| 193 | + ? html` <mwc-list multi> |
| 194 | + ${attrDiffs.length > 0 |
| 195 | + ? html` <mwc-list-item noninteractive ?twoline=${!!idTitle}> |
| 196 | + <span class="resultTitle"> |
| 197 | + ${translate('compare.attributes', { |
| 198 | + elementName: elementToBeCompared.tagName, |
| 199 | + })} |
| 200 | + </span> |
| 201 | + ${idTitle |
| 202 | + ? html`<span slot="secondary">${idTitle}</span>` |
| 203 | + : nothing} |
| 204 | + </mwc-list-item> |
| 205 | + <li padded divider role="separator"></li>` |
| 206 | + : ''} |
| 207 | + ${repeat( |
| 208 | + attrDiffs, |
| 209 | + e => e, |
| 210 | + ([name, diff]) => |
| 211 | + html` <mwc-list-item twoline left hasMeta> |
| 212 | + <span>${name}</span> |
| 213 | + <span slot="secondary"> |
| 214 | + ${diff.oldValue ?? ''} |
| 215 | + ${diff.oldValue && diff.newValue ? html`↷` : ' '} |
| 216 | + ${diff.newValue ?? ''} |
| 217 | + </span> |
| 218 | + <mwc-icon slot="meta"> |
| 219 | + ${diff.oldValue ? (diff.newValue ? 'edit' : 'delete') : 'add'} |
| 220 | + </mwc-icon> |
| 221 | + </mwc-list-item>` |
| 222 | + )} |
| 223 | + ${childAddedOrDeleted.length > 0 |
| 224 | + ? html` <mwc-list-item noninteractive ?twoline=${!!idTitle}> |
| 225 | + <span class="resultTitle"> |
| 226 | + ${translate('compare.children', { |
| 227 | + elementName: elementToBeCompared.tagName, |
| 228 | + })} |
| 229 | + </span> |
| 230 | + ${idTitle |
| 231 | + ? html`<span slot="secondary">${idTitle}</span>` |
| 232 | + : nothing} |
| 233 | + </mwc-list-item> |
| 234 | + <li padded divider role="separator"></li>` |
| 235 | + : ''} |
| 236 | + ${repeat( |
| 237 | + childAddedOrDeleted, |
| 238 | + e => e, |
| 239 | + diff => |
| 240 | + html` <mwc-list-item twoline left hasMeta> |
| 241 | + <span>${diff.oldValue?.tagName ?? diff.newValue?.tagName}</span> |
| 242 | + <span slot="secondary"> |
| 243 | + ${diff.oldValue |
| 244 | + ? describe(diff.oldValue) |
| 245 | + : describe(diff.newValue)} |
| 246 | + </span> |
| 247 | + <mwc-icon slot="meta"> |
| 248 | + ${diff.oldValue ? 'delete' : 'add'} |
| 249 | + </mwc-icon> |
| 250 | + </mwc-list-item>` |
| 251 | + )} |
| 252 | + </mwc-list>` |
| 253 | + : ''} |
| 254 | + ${childToCompareTemplates}`; |
| 255 | + } |
| 256 | + return null; |
| 257 | +} |
0 commit comments