| /** | |
| * @license | |
| * Copyright 2016 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import {ImageInfo} from '../../../types/common'; | |
| import {Side} from '../../../api/diff'; | |
| import '../gr-diff-image-viewer/gr-image-viewer'; | |
| import {html, LitElement, nothing} from 'lit'; | |
| import {property, query, state} from 'lit/decorators.js'; | |
| import {isNewDiff} from '../gr-diff/gr-diff-utils'; | |
| // MIME types for images we allow showing. Do not include SVG, it can contain | |
| // arbitrary JavaScript. | |
| const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/; | |
| class GrDiffImageNew extends LitElement { | |
| @property() baseImage?: ImageInfo; | |
| @property() revisionImage?: ImageInfo; | |
| @property() automaticBlink = false; | |
| /** | |
| * The browser API for handling selection does not (yet) work for selection | |
| * across multiple shadow DOM elements. So we are rendering gr-diff components | |
| * into the light DOM instead of the shadow DOM by overriding this method, | |
| * which was the recommended workaround by the lit team. | |
| * See also https://github.com/WICG/webcomponents/issues/79. | |
| */ | |
| override createRenderRoot() { | |
| return this; | |
| } | |
| override render() { | |
| return html` | |
| <tbody class="gr-diff image-diff"> | |
| <tr class="gr-diff"> | |
| <td class="gr-diff" colspan="4"> | |
| <gr-image-viewer | |
| class="gr-diff" | |
| .baseUrl=${imageSrc(this.baseImage)} | |
| .revisionUrl=${imageSrc(this.revisionImage)} | |
| .automaticBlink=${this.automaticBlink} | |
| > | |
| </gr-image-viewer> | |
| </td> | |
| </tr> | |
| </tbody> | |
| `; | |
| } | |
| } | |
| class GrDiffImageOld extends LitElement { | |
| @property() baseImage?: ImageInfo; | |
| @property() revisionImage?: ImageInfo; | |
| @query('img.left') baseImageEl?: HTMLImageElement; | |
| @query('img.right') revisionImageEl?: HTMLImageElement; | |
| @state() baseError?: string; | |
| @state() revisionError?: string; | |
| /** | |
| * The browser API for handling selection does not (yet) work for selection | |
| * across multiple shadow DOM elements. So we are rendering gr-diff components | |
| * into the light DOM instead of the shadow DOM by overriding this method, | |
| * which was the recommended workaround by the lit team. | |
| * See also https://github.com/WICG/webcomponents/issues/79. | |
| */ | |
| override createRenderRoot() { | |
| return this; | |
| } | |
| override render() { | |
| return html` | |
| <tbody class="gr-diff image-diff"> | |
| ${this.renderImagePairRow()} ${this.renderImageLabelRow()} | |
| </tbody> | |
| ${this.renderEndpoint()} | |
| `; | |
| } | |
| private renderEndpoint() { | |
| return html` | |
| <tbody class="gr-diff endpoint"> | |
| <tr class="gr-diff"> | |
| <td class="gr-diff" colspan="4"> | |
| <gr-endpoint-decorator class="gr-diff" name="image-diff"> | |
| ${this.renderEndpointParam('baseImage', this.baseImage)} | |
| ${this.renderEndpointParam('revisionImage', this.revisionImage)} | |
| </gr-endpoint-decorator> | |
| </td> | |
| </tr> | |
| </tbody> | |
| `; | |
| } | |
| private renderEndpointParam(name: string, value: unknown) { | |
| if (!value) return nothing; | |
| return html` | |
| <gr-endpoint-param class="gr-diff" name=${name} .value=${value}> | |
| </gr-endpoint-param> | |
| `; | |
| } | |
| private renderImagePairRow() { | |
| return html` | |
| <tr class="gr-diff"> | |
| <td class="gr-diff left lineNum blank"></td> | |
| <td class="gr-diff left">${this.renderImage(Side.LEFT)}</td> | |
| <td class="gr-diff right lineNum blank"></td> | |
| <td class="gr-diff right">${this.renderImage(Side.RIGHT)}</td> | |
| </tr> | |
| `; | |
| } | |
| private renderImage(side: Side) { | |
| const image = side === Side.LEFT ? this.baseImage : this.revisionImage; | |
| if (!image) return nothing; | |
| const error = side === Side.LEFT ? this.baseError : this.revisionError; | |
| if (error) return error; | |
| const src = imageSrc(image); | |
| if (!src) return nothing; | |
| return html` | |
| <img | |
| class="gr-diff ${side}" | |
| src=${src} | |
| @load=${this.handleLoad} | |
| @error=${(e: Event) => this.handleError(e, side)} | |
| > | |
| </img> | |
| `; | |
| } | |
| private handleLoad() { | |
| this.requestUpdate(); | |
| } | |
| private handleError(e: Event, side: Side) { | |
| const msg = `[Image failed to load] ${e.type}`; | |
| if (side === Side.LEFT) this.baseError = msg; | |
| if (side === Side.RIGHT) this.revisionError = msg; | |
| } | |
| private renderImageLabelRow() { | |
| return html` | |
| <tr class="gr-diff"> | |
| <td class="gr-diff left lineNum blank"></td> | |
| <td class="gr-diff left"> | |
| <label class="gr-diff"> | |
| ${this.renderName(this.baseImage?._name ?? '')} | |
| <span class="gr-diff label">${this.imageLabel(Side.LEFT)}</span> | |
| </label> | |
| </td> | |
| <td class="gr-diff right lineNum blank"></td> | |
| <td class="gr-diff right"> | |
| <label class="gr-diff"> | |
| ${this.renderName(this.revisionImage?._name ?? '')} | |
| <span class="gr-diff label"> ${this.imageLabel(Side.RIGHT)} </span> | |
| </label> | |
| </td> | |
| </tr> | |
| `; | |
| } | |
| private renderName(name?: string) { | |
| const addNamesInLabel = | |
| this.baseImage && | |
| this.revisionImage && | |
| this.baseImage._name !== this.revisionImage._name; | |
| if (!addNamesInLabel) return nothing; | |
| return html` | |
| <span class="gr-diff name">${name}</span><br class="gr-diff" /> | |
| `; | |
| } | |
| private imageLabel(side: Side) { | |
| const image = side === Side.LEFT ? this.baseImage : this.revisionImage; | |
| const imageEl = | |
| side === Side.LEFT ? this.baseImageEl : this.revisionImageEl; | |
| if (image) { | |
| const type = image.type ?? image._expectedType; | |
| if (imageEl?.naturalWidth && imageEl.naturalHeight) { | |
| return `${imageEl?.naturalWidth}×${imageEl.naturalHeight} ${type}`; | |
| } else { | |
| return type; | |
| } | |
| } | |
| return 'No image'; | |
| } | |
| } | |
| function imageSrc(image?: ImageInfo): string { | |
| return image && IMAGE_MIME_PATTERN.test(image.type) | |
| ? `data:${image.type};base64,${image.body}` | |
| : ''; | |
| } | |
| // TODO(newdiff-cleanup): Remove once newdiff migration is completed. | |
| if (isNewDiff()) { | |
| customElements.define('gr-diff-image-new', GrDiffImageNew); | |
| customElements.define('gr-diff-image-old', GrDiffImageOld); | |
| } | |
| declare global { | |
| interface HTMLElementTagNameMap { | |
| // TODO(newdiff-cleanup): Replace once newdiff migration is completed. | |
| 'gr-diff-image-new': LitElement; | |
| 'gr-diff-image-old': LitElement; | |
| } | |
| } |