blob: 869d600eb30361b5d6ec30d4e6b06b145ae3c03a [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {FILE, RenderPreferences, Side} from '../../../api/diff';
import '../../diff/gr-diff-image-viewer/gr-image-viewer';
import {html, LitElement, nothing} from 'lit';
import {property, query, state} from 'lit/decorators.js';
import {GrDiffBuilder} from './gr-diff-builder';
import {GrDiffGroup} from '../gr-diff/gr-diff-group';
import {isNewDiff, createElementDiff} from '../../diff/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)$/;
export class GrDiffBuilderImage extends GrDiffBuilder {
constructor(
diff: DiffInfo,
prefs: DiffPreferencesInfo,
outputEl: HTMLElement,
private readonly baseImage: ImageInfo | null,
private readonly revisionImage: ImageInfo | null,
renderPrefs?: RenderPreferences,
private readonly useNewImageDiffUi: boolean = false
) {
super(diff, prefs, outputEl, [], renderPrefs);
}
override buildSectionElement(group: GrDiffGroup): HTMLElement {
const section = createElementDiff('tbody');
// Do not create a diff row for LOST.
if (group.lines[0].beforeNumber !== FILE) return section;
return super.buildSectionElement(group);
}
public renderImageDiff() {
const imageDiff = this.useNewImageDiffUi
? this.createImageDiffNew()
: this.createImageDiffOld();
this.outputEl.appendChild(imageDiff);
}
private createImageDiffNew() {
// TODO(newdiff-cleanup): Remove cast when newdiff migration is complete.
const imageDiff = document.createElement(
'gr-diff-image-new'
) as GrDiffImageNew;
imageDiff.automaticBlink = this.autoBlink();
imageDiff.baseImage = this.baseImage ?? undefined;
imageDiff.revisionImage = this.revisionImage ?? undefined;
return imageDiff;
}
private createImageDiffOld() {
// TODO(newdiff-cleanup): Remove cast when newdiff migration is complete.
const imageDiff = document.createElement(
'gr-diff-image-old'
) as GrDiffImageOld;
imageDiff.baseImage = this.baseImage ?? undefined;
imageDiff.revisionImage = this.revisionImage ?? undefined;
return imageDiff;
}
private autoBlink(): boolean {
return !!this.renderPrefs?.image_diff_prefs?.automatic_blink;
}
override updateRenderPrefs(renderPrefs: RenderPreferences) {
this.renderPrefs = renderPrefs;
// We have to update `imageDiff.automaticBlink` manually, because `this` is
// not a LitElement.
const imageDiff = this.outputEl.querySelector(
'gr-diff-image-new'
) as GrDiffImageNew;
if (imageDiff) imageDiff.automaticBlink = this.autoBlink();
}
}
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;
}
}