UNPKG

@limetech/lime-elements

Version:
776 lines (775 loc) 33.8 kB
import { h, Host } from "@stencil/core"; import translate from "../../global/translations"; import { buildSplitLines, computeDiff, normalizeForDiff } from "./diff-engine"; import { tokenize } from "./syntax-highlighter"; import { buildSearchRegex, navigateMatchIndex } from "./search-utils"; import { extractRemovedContent, extractRemovedContentFromSplit, } from "./content-utils"; /** * Displays a visual diff between two text values, modeled on * GitHub's code difference view. * * Supports unified and split (side-by-side) views with line numbers, * color-coded additions and removals, word-level inline highlighting, * and collapsible unchanged context sections. * * @beta * @exampleComponent limel-example-code-diff-basic * @exampleComponent limel-example-code-diff-headings * @exampleComponent limel-example-code-diff-json * @exampleComponent limel-example-code-diff-split * @exampleComponent limel-example-code-diff-line-wrap * @exampleComponent limel-example-code-diff-expand */ export class CodeDiff { constructor() { /** * The "before" value to compare. * Can be a string or an object (which will be serialized to JSON). */ this.oldValue = ''; /** * The "after" value to compare. * Can be a string or an object (which will be serialized to JSON). */ this.newValue = ''; /** * The layout of the diff view. * - `unified` — single column with interleaved additions and removals * - `split` — side-by-side comparison with old on left, new on right */ this.layout = 'unified'; /** * Number of unchanged context lines to display around each change. */ this.contextLines = 3; /** * When `true`, long lines are wrapped instead of causing * horizontal scrolling. Useful when comparing prose or * config files with long values. */ this.lineWrapping = true; /** * When `true`, JSON values are parsed, keys are sorted, * and indentation is normalized before diffing. * This eliminates noise from formatting or key-order differences. */ this.reformatJson = false; /** * Defines the language for translations. * Will translate all visible labels and announcements. */ this.translationLanguage = 'en'; this.diffResult = { hunks: [], additions: 0, deletions: 0, allLines: [], }; this.liveAnnouncement = ''; this.copyState = 'idle'; this.searchVisible = false; this.searchTerm = ''; this.currentMatchIndex = 0; this.focusedRowIndex = -1; this.normalizedOldText = ''; /** * Render-time counter that increments for each search match * found while rendering removed lines. Used to determine which * match is the "current" one for navigation highlighting. */ this.searchMatchCounter = 0; /** * Total search matches found during the last render pass. */ this.totalSearchMatches = 0; /** * Whether the current render is inside a removed line, * so search highlighting knows when to activate. */ this.isRenderingRemovedLine = false; /** * Cached search regex for the current render pass. * Built once in render() and reused across all renderSearchableText calls. */ this.activeSearchRegex = null; this.prevSearchVisible = false; } componentWillLoad() { this.recomputeDiff(); } componentDidRender() { var _a, _b; if (this.searchVisible && !this.prevSearchVisible) { (_a = this.searchInputEl) === null || _a === void 0 ? void 0 : _a.focus(); } this.prevSearchVisible = this.searchVisible; if (this.searchTerm && this.totalSearchMatches > 0) { const current = (_b = this.host.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.search-match--current'); current === null || current === void 0 ? void 0 : current.scrollIntoView({ block: 'center', behavior: 'smooth' }); } } render() { this.searchMatchCounter = 0; this.activeSearchRegex = buildSearchRegex(this.searchTerm); const diffContent = this.renderDiff(); // Capture total matches after rendering completes this.totalSearchMatches = this.searchMatchCounter; const lineNumberWidth = this.computeLineNumberWidth(); return (h(Host, { key: '8122ede0d323b9021dae44cd5f65703d03083617', style: { '--limel-line-number-min-width': lineNumberWidth } }, this.renderHeader(), this.renderScreenReaderSummary(), this.searchVisible && this.renderSearchBar(), h("div", { key: 'fefe6e4898cbc82b6eb87f7dff7af71a14c7c91e', class: "diff-body", role: "table", "aria-label": this.getTranslation('code-diff.table-label'), tabindex: "0", onKeyDown: (event) => this.handleKeyDown(event) }, diffContent), h("div", { key: '2af1c7b29f2060a58af13d67fb174cb4de085efb', class: "screen-reader-only", role: "status", "aria-live": "polite", "aria-atomic": "true" }, this.liveAnnouncement))); } watchInputs() { this.recomputeDiff(); } recomputeDiff() { const oldText = normalizeForDiff(this.oldValue, this.reformatJson); const newText = normalizeForDiff(this.newValue, this.reformatJson); this.normalizedOldText = oldText; this.diffResult = computeDiff(oldText, newText, this.contextLines); this.focusedRowIndex = -1; } formatSrSummary() { const { additions, deletions } = this.diffResult; if (additions === 0 && deletions === 0) { return null; } const parts = []; if (additions > 0) { const key = additions === 1 ? 'code-diff.diff-addition' : 'code-diff.diff-additions'; parts.push(this.getTranslation(key, { count: additions })); } if (deletions > 0) { const key = deletions === 1 ? 'code-diff.diff-deletion' : 'code-diff.diff-deletions'; parts.push(this.getTranslation(key, { count: deletions })); } return this.getTranslation('code-diff.diff-summary', { parts: parts.join(', '), }); } renderScreenReaderSummary() { const summary = this.formatSrSummary(); return (h("div", { class: "screen-reader-only", role: "status", "aria-live": "polite" }, summary !== null && summary !== void 0 ? summary : this.getTranslation('code-diff.no-differences-found'))); } handleKeyDown(event) { if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return; } event.preventDefault(); const rows = this.getDiffRows(); if (rows.length === 0) { return; } if (event.key === 'ArrowDown') { this.focusedRowIndex = Math.min(this.focusedRowIndex + 1, rows.length - 1); } else { this.focusedRowIndex = Math.max(this.focusedRowIndex - 1, 0); } this.updateRowFocus(rows); } getDiffRows() { var _a; const body = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.diff-body'); if (!body) { return []; } return [ ...body.querySelectorAll('.diff-line:not(.diff-line--collapsed)'), ]; } updateRowFocus(rows) { for (const row of rows) { row.removeAttribute('tabindex'); row.classList.remove('diff-line--focused'); } const target = rows[this.focusedRowIndex]; if (target) { target.setAttribute('tabindex', '-1'); target.classList.add('diff-line--focused'); target.focus(); this.announceLine(target); } } announceLine(row) { var _a, _b; let lineType = this.getTranslation('code-diff.line-context'); if (row.classList.contains('diff-line--added')) { lineType = this.getTranslation('code-diff.line-added'); } else if (row.classList.contains('diff-line--removed')) { lineType = this.getTranslation('code-diff.line-removed'); } const content = (_b = (_a = row.querySelector('.line-content, .split-content')) === null || _a === void 0 ? void 0 : _a.textContent) !== null && _b !== void 0 ? _b : ''; const trimmed = content.length > 80 ? content.slice(0, 80) + '…' : content; this.liveAnnouncement = `${lineType}: ${trimmed}`; } renderHeader() { var _a, _b; const oldHeading = (_a = this.oldHeading) !== null && _a !== void 0 ? _a : this.getTranslation('code-diff.old-heading'); const newHeading = (_b = this.newHeading) !== null && _b !== void 0 ? _b : this.getTranslation('code-diff.new-heading'); const { additions, deletions } = this.diffResult; const hasDiff = additions > 0 || deletions > 0; return (h("div", { class: "diff-header" }, h("div", { class: "diff-header__labels" }, h("span", { class: "diff-header__old" }, oldHeading), h("span", { class: "diff-header__new" }, newHeading)), h("div", { class: "diff-header__actions" }, h("div", { class: "diff-header__stats" }, additions > 0 && (h("span", { class: "stat stat--added" }, "+", additions)), deletions > 0 && (h("span", { class: "stat stat--removed" }, "-", deletions))), hasDiff && this.renderCopyButton(), deletions > 0 && this.renderSearchToggle()))); } renderCopyButton() { const label = this.copyState === 'copied' ? this.getTranslation('code-diff.copied') : this.getTranslation('code-diff.copy-old-version'); const icon = this.copyState === 'copied' ? 'checkmark' : 'copy'; return (h("limel-icon-button", { label: label, icon: icon, onClick: () => this.copyToClipboard(this.normalizedOldText) })); } async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.copyState = 'copied'; this.liveAnnouncement = this.getTranslation('code-diff.copied-to-clipboard'); setTimeout(() => { this.copyState = 'idle'; }, 2000); } catch (_a) { // Clipboard API may fail in insecure contexts } } renderSearchToggle() { return (h("limel-icon-button", { class: { 'search-toggle--active': this.searchVisible }, label: this.getTranslation('code-diff.search'), icon: "search", onClick: () => this.toggleSearch() })); } renderSearchBar() { const matchInfo = this.totalSearchMatches === 0 ? this.getTranslation('code-diff.no-matches') : this.getTranslation('code-diff.match-count', { current: this.currentMatchIndex + 1, total: this.totalSearchMatches, }); return (h("div", { class: "search-bar" }, h("limel-input-field", { class: "search-bar__input", type: "search", placeholder: this.getTranslation('code-diff.search') + '…', value: this.searchTerm, onChange: (e) => this.onSearchInput(e), onKeyDown: (e) => this.onSearchKeyDown(e), ref: (el) => (this.searchInputEl = el) }), h("span", { class: "search-bar__count" }, matchInfo), h("limel-action-bar", { actions: this.getSearchActions(), onItemSelected: (e) => this.onSearchAction(e) }))); } toggleSearch() { this.searchVisible = !this.searchVisible; if (!this.searchVisible) { this.searchTerm = ''; this.currentMatchIndex = 0; } } onSearchInput(event) { this.searchTerm = event.detail; this.currentMatchIndex = 0; } onSearchKeyDown(event) { if (event.key === 'Enter') { event.preventDefault(); if (event.shiftKey) { this.navigateSearch(-1); } else { this.navigateSearch(1); } } else if (event.key === 'Escape') { this.toggleSearch(); } } computeLineNumberWidth() { const maxLineNumber = this.diffResult.allLines.length; const digits = String(maxLineNumber).length; return `calc(${digits}ch + 2 * var(--limel-code-diff-line-number-padding))`; } getSearchActions() { const noMatches = this.totalSearchMatches === 0; return [ { text: this.getTranslation('code-diff.previous-match'), icon: '-lime-caret-top', iconOnly: true, disabled: noMatches, value: 'prev', }, { text: this.getTranslation('code-diff.next-match'), icon: '-lime-caret-bottom', iconOnly: true, disabled: noMatches, value: 'next', }, { text: this.getTranslation('code-diff.close-search'), icon: 'cancel', iconOnly: true, value: 'close', }, ]; } onSearchAction(event) { const { value } = event.detail; if (value === 'prev') { this.navigateSearch(-1); } else if (value === 'next') { this.navigateSearch(1); } else if (value === 'close') { this.toggleSearch(); } } navigateSearch(direction) { this.currentMatchIndex = navigateMatchIndex(this.currentMatchIndex, direction, this.totalSearchMatches); } renderDiff() { const { hunks, collapsedAfter } = this.diffResult; if (hunks.length === 0) { return (h("div", { class: "diff-empty" }, this.getTranslation('code-diff.no-differences'))); } const lineRenderer = this.layout === 'split' ? (hunk) => this.renderSplitHunkRows(hunk) : (hunk) => this.renderHunkLines(hunk); return this.renderHunks(hunks, collapsedAfter, lineRenderer); } renderHunks(hunks, collapsedAfter, lineRenderer) { const elements = []; for (const [i, hunk] of hunks.entries()) { if (hunk.collapsedBefore) { elements.push(this.renderCollapsedRow(hunk.collapsedBefore, i)); } elements.push(...lineRenderer(hunk)); } if (collapsedAfter) { elements.push(this.renderCollapsedAfterRow(collapsedAfter)); } return elements; } renderHunkLines(hunk) { const elements = []; let i = 0; while (i < hunk.lines.length) { const line = hunk.lines[i]; if (line.type === 'context') { elements.push(this.renderLine(line)); i++; continue; } // Collect consecutive changed lines as a change block const blockLines = []; while (i < hunk.lines.length && hunk.lines[i].type !== 'context') { blockLines.push(hunk.lines[i]); i++; } elements.push(this.renderChangeBlock(blockLines)); } return elements; } renderChangeBlock(lines) { const removedContent = extractRemovedContent(lines); return (h("div", { class: "change-group" }, lines.map((line) => this.renderLine(line)), removedContent && this.renderBlockCopyButton(removedContent))); } renderLine(line) { var _a, _b; const lineClass = { 'diff-line': true, [`diff-line--${line.type}`]: true, }; const indicatorMap = { added: '+', removed: '-', context: ' ', }; const indicator = indicatorMap[line.type]; return (h("div", { class: lineClass, role: "row" }, h("span", { class: "line-number line-number--old", role: "cell", "aria-label": line.oldLineNumber ? this.getTranslation('code-diff.old-line', { number: line.oldLineNumber, }) : undefined }, (_a = line.oldLineNumber) !== null && _a !== void 0 ? _a : ''), h("span", { class: "line-number line-number--new", role: "cell", "aria-label": line.newLineNumber ? this.getTranslation('code-diff.new-line', { number: line.newLineNumber, }) : undefined }, (_b = line.newLineNumber) !== null && _b !== void 0 ? _b : ''), h("span", { class: "line-indicator", role: "cell" }, indicator), h("span", { class: "line-content", role: "cell" }, this.renderContent(line)))); } renderSplitHunkRows(hunk) { var _a, _b, _c, _d; const splitRows = buildSplitLines(hunk.lines); const elements = []; let i = 0; while (i < splitRows.length) { const row = splitRows[i]; const isContext = ((_a = row.left) === null || _a === void 0 ? void 0 : _a.type) === 'context' && ((_b = row.right) === null || _b === void 0 ? void 0 : _b.type) === 'context'; if (isContext) { elements.push(this.renderSplitRow(row)); i++; continue; } // Collect consecutive changed rows const blockRows = []; while (i < splitRows.length) { const r = splitRows[i]; const rIsContext = ((_c = r.left) === null || _c === void 0 ? void 0 : _c.type) === 'context' && ((_d = r.right) === null || _d === void 0 ? void 0 : _d.type) === 'context'; if (rIsContext) { break; } blockRows.push(r); i++; } elements.push(this.renderSplitChangeBlock(blockRows)); } return elements; } renderSplitChangeBlock(rows) { const removedContent = extractRemovedContentFromSplit(rows); return (h("div", { class: "change-group" }, rows.map((row) => this.renderSplitRow(row)), removedContent && this.renderBlockCopyButton(removedContent))); } renderSplitRow(row) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const leftType = (_b = (_a = row.left) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'empty'; const rightType = (_d = (_c = row.right) === null || _c === void 0 ? void 0 : _c.type) !== null && _d !== void 0 ? _d : 'empty'; const oldLineLabel = ((_e = row.left) === null || _e === void 0 ? void 0 : _e.oldLineNumber) ? this.getTranslation('code-diff.old-line', { number: row.left.oldLineNumber, }) : undefined; const newLineLabel = ((_f = row.right) === null || _f === void 0 ? void 0 : _f.newLineNumber) ? this.getTranslation('code-diff.new-line', { number: row.right.newLineNumber, }) : undefined; return (h("div", { class: "diff-line diff-line--split", role: "row" }, h("span", { class: "line-number line-number--old", role: "cell", "aria-label": oldLineLabel }, (_h = (_g = row.left) === null || _g === void 0 ? void 0 : _g.oldLineNumber) !== null && _h !== void 0 ? _h : ''), h("span", { class: `split-content split-content--left split-content--${leftType}`, role: "cell" }, row.left ? this.renderContent(row.left) : ''), h("span", { class: "line-number line-number--new", role: "cell", "aria-label": newLineLabel }, (_k = (_j = row.right) === null || _j === void 0 ? void 0 : _j.newLineNumber) !== null && _k !== void 0 ? _k : ''), h("span", { class: `split-content split-content--right split-content--${rightType}`, role: "cell" }, row.right ? this.renderContent(row.right) : ''))); } renderBlockCopyButton(removedContent) { return (h("limel-icon-button", { class: "change-group__copy", elevated: true, label: this.getTranslation('code-diff.copy-change'), icon: "copy", onClick: () => this.copyToClipboard(removedContent) })); } renderContent(line) { this.isRenderingRemovedLine = line.type === 'removed' && this.searchTerm.length > 0; if (!line.segments || line.segments.length === 0) { return this.renderSyntaxTokens(line.content); } return line.segments.map((segment) => this.renderSegment(segment, line.type)); } renderSegment(segment, lineType) { const content = this.renderSyntaxTokens(segment.value); if (segment.type === 'equal') { return content; } const segmentClass = lineType === 'removed' ? 'segment--removed' : 'segment--added'; return h("mark", { class: segmentClass }, content); } renderSyntaxTokens(text) { const tokens = tokenize(text, this.language); if (tokens.length === 1 && tokens[0].type === 'plain') { return this.renderSearchableText(text); } return tokens.map((token) => this.renderSyntaxToken(token)); } renderSyntaxToken(token) { const text = this.renderSearchableText(token.value); if (token.type === 'plain') { return text; } return h("span", { class: `syntax--${token.type}` }, text); } renderSearchableText(text) { if (!this.isRenderingRemovedLine || !this.activeSearchRegex) { return text; } const parts = text.split(this.activeSearchRegex); if (parts.length === 1) { return text; } return parts.map((part, i) => { // Odd indices are the captured matches from split if (i % 2 === 0) { return part; } const matchIndex = this.searchMatchCounter++; const isCurrent = matchIndex === this.currentMatchIndex; const cls = { 'search-match': true, 'search-match--current': isCurrent, }; return (h("mark", { key: `match-${matchIndex}`, class: cls }, part)); }); } renderCollapsedRow(count, hunkIndex) { return (h("div", { class: "diff-line diff-line--collapsed", role: "row" }, h("button", { class: "expand-button", type: "button", onClick: () => this.expandHunk(hunkIndex), "aria-label": this.getTranslation('code-diff.show-hidden-lines', { count, }) }, this.getTranslation('code-diff.hidden-lines', { count })))); } renderCollapsedAfterRow(count) { return (h("div", { class: "diff-line diff-line--collapsed", role: "row" }, h("button", { class: "expand-button", type: "button", onClick: () => this.expandAfter(), "aria-label": this.getTranslation('code-diff.show-hidden-lines', { count, }) }, this.getTranslation('code-diff.hidden-lines', { count })))); } expandHunk(hunkIndex) { const hunks = [...this.diffResult.hunks]; const hunk = hunks[hunkIndex]; const prevHunkEnd = hunkIndex > 0 ? hunks[hunkIndex - 1].startIndex + hunks[hunkIndex - 1].lines.length : 0; const hiddenLines = this.diffResult.allLines.slice(prevHunkEnd, hunk.startIndex); hunks[hunkIndex] = Object.assign(Object.assign({}, hunk), { lines: [...hiddenLines, ...hunk.lines], collapsedBefore: undefined, startIndex: prevHunkEnd }); this.diffResult = Object.assign(Object.assign({}, this.diffResult), { hunks }); this.liveAnnouncement = this.getTranslation('code-diff.expanded-lines'); } expandAfter() { const hunks = [...this.diffResult.hunks]; const lastIndex = hunks.length - 1; const lastHunk = hunks[lastIndex]; const lastHunkEnd = lastHunk.startIndex + lastHunk.lines.length; const hiddenLines = this.diffResult.allLines.slice(lastHunkEnd); hunks[lastIndex] = Object.assign(Object.assign({}, lastHunk), { lines: [...lastHunk.lines, ...hiddenLines] }); this.diffResult = Object.assign(Object.assign({}, this.diffResult), { hunks, collapsedAfter: undefined }); this.liveAnnouncement = this.getTranslation('code-diff.expanded-lines-end'); } getTranslation(key, params) { return translate.get(key, this.translationLanguage, params); } static get is() { return "limel-code-diff"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["code-diff.scss"] }; } static get styleUrls() { return { "$": ["code-diff.css"] }; } static get properties() { return { "oldValue": { "type": "string", "mutable": false, "complexType": { "original": "string | object", "resolved": "object | string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The \"before\" value to compare.\nCan be a string or an object (which will be serialized to JSON)." }, "getter": false, "setter": false, "reflect": false, "attribute": "old-value", "defaultValue": "''" }, "newValue": { "type": "string", "mutable": false, "complexType": { "original": "string | object", "resolved": "object | string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The \"after\" value to compare.\nCan be a string or an object (which will be serialized to JSON)." }, "getter": false, "setter": false, "reflect": false, "attribute": "new-value", "defaultValue": "''" }, "oldHeading": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Heading for the original (before) version, displayed in the diff header.\nDefaults to `\"Original\"`, localized via `translationLanguage`." }, "getter": false, "setter": false, "reflect": true, "attribute": "old-heading" }, "newHeading": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Heading for the modified (after) version, displayed in the diff header.\nDefaults to `\"Modified\"`, localized via `translationLanguage`." }, "getter": false, "setter": false, "reflect": true, "attribute": "new-heading" }, "layout": { "type": "string", "mutable": false, "complexType": { "original": "'unified' | 'split'", "resolved": "\"split\" | \"unified\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The layout of the diff view.\n- `unified` \u2014 single column with interleaved additions and removals\n- `split` \u2014 side-by-side comparison with old on left, new on right" }, "getter": false, "setter": false, "reflect": true, "attribute": "layout", "defaultValue": "'unified'" }, "contextLines": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Number of unchanged context lines to display around each change." }, "getter": false, "setter": false, "reflect": true, "attribute": "context-lines", "defaultValue": "3" }, "lineWrapping": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, long lines are wrapped instead of causing\nhorizontal scrolling. Useful when comparing prose or\nconfig files with long values." }, "getter": false, "setter": false, "reflect": true, "attribute": "line-wrapping", "defaultValue": "true" }, "language": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Language for syntax highlighting.\nCurrently supports `\"json\"`. When set, code tokens are\ncolorized (strings, numbers, keys, etc.) alongside the\ndiff highlighting." }, "getter": false, "setter": false, "reflect": true, "attribute": "language" }, "reformatJson": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, JSON values are parsed, keys are sorted,\nand indentation is normalized before diffing.\nThis eliminates noise from formatting or key-order differences." }, "getter": false, "setter": false, "reflect": true, "attribute": "reformat-json", "defaultValue": "false" }, "translationLanguage": { "type": "string", "mutable": false, "complexType": { "original": "Languages", "resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"", "references": { "Languages": { "location": "import", "path": "../date-picker/date.types", "id": "src/components/date-picker/date.types.ts::Languages", "referenceLocation": "Languages" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines the language for translations.\nWill translate all visible labels and announcements." }, "getter": false, "setter": false, "reflect": true, "attribute": "translation-language", "defaultValue": "'en'" } }; } static get states() { return { "diffResult": {}, "liveAnnouncement": {}, "copyState": {}, "searchVisible": {}, "searchTerm": {}, "currentMatchIndex": {} }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "oldValue", "methodName": "watchInputs" }, { "propName": "newValue", "methodName": "watchInputs" }, { "propName": "contextLines", "methodName": "watchInputs" }, { "propName": "reformatJson", "methodName": "watchInputs" }, { "propName": "layout", "methodName": "watchInputs" }]; } }