diff2html/src/ui/js/diff2html-ui-base.ts

204 lines
6.8 KiB
TypeScript
Raw Normal View History

2021-02-20 13:43:55 +00:00
import { closeTags, nodeStream, mergeStreams } from './highlight.js-helpers';
2019-12-29 22:31:32 +00:00
import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
import { DiffFile } from '../../types';
2021-02-20 13:43:55 +00:00
import { HighlightResult, HLJSApi } from 'highlight.js';
2019-12-29 22:31:32 +00:00
export interface Diff2HtmlUIConfig extends Diff2HtmlConfig {
synchronisedScroll?: boolean;
highlight?: boolean;
fileListToggle?: boolean;
fileListStartVisible?: boolean;
/**
* @deprecated since version 3.1.0
* Smart selection is now enabled by default with vanilla CSS
*/
2019-12-29 22:31:32 +00:00
smartSelection?: boolean;
2021-01-23 15:07:14 +00:00
fileContentToggle?: boolean;
2019-12-29 22:31:32 +00:00
}
export const defaultDiff2HtmlUIConfig = {
...defaultDiff2HtmlConfig,
synchronisedScroll: true,
highlight: true,
fileListToggle: true,
fileListStartVisible: false,
/**
* @deprecated since version 3.1.0
* Smart selection is now enabled by default with vanilla CSS
*/
2019-12-29 22:31:32 +00:00
smartSelection: true,
2021-01-23 15:07:14 +00:00
fileContentToggle: true,
2019-12-29 22:31:32 +00:00
};
export class Diff2HtmlUI {
readonly config: typeof defaultDiff2HtmlUIConfig;
readonly diffHtml: string;
readonly targetElement: HTMLElement;
2021-02-20 13:43:55 +00:00
readonly hljs: HLJSApi | null = null;
2019-12-29 22:31:32 +00:00
currentSelectionColumnId = -1;
2021-02-20 13:43:55 +00:00
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}, hljs?: HLJSApi) {
2019-12-29 22:31:32 +00:00
this.config = { ...defaultDiff2HtmlUIConfig, ...config };
this.diffHtml = diffInput !== undefined ? html(diffInput, this.config) : target.innerHTML;
2019-12-29 22:31:32 +00:00
this.targetElement = target;
if (hljs !== undefined) this.hljs = hljs;
}
draw(): void {
this.targetElement.innerHTML = this.diffHtml;
if (this.config.synchronisedScroll) this.synchronisedScroll();
if (this.config.highlight) this.highlightCode();
if (this.config.fileListToggle) this.fileListToggle(this.config.fileListStartVisible);
2021-01-23 15:07:14 +00:00
if (this.config.fileContentToggle) this.fileContentToggle();
2019-12-29 22:31:32 +00:00
}
synchronisedScroll(): void {
this.targetElement.querySelectorAll('.d2h-file-wrapper').forEach(wrapper => {
const [left, right] = Array<Element>().slice.call(wrapper.querySelectorAll('.d2h-file-side-diff'));
2019-12-29 22:31:32 +00:00
if (left === undefined || right === undefined) return;
2019-12-29 22:31:32 +00:00
const onScroll = (event: Event): void => {
if (event === null || event.target === null) return;
2019-12-29 22:31:32 +00:00
if (event.target === left) {
right.scrollTop = left.scrollTop;
right.scrollLeft = left.scrollLeft;
} else {
left.scrollTop = right.scrollTop;
left.scrollLeft = right.scrollLeft;
}
};
left.addEventListener('scroll', onScroll);
right.addEventListener('scroll', onScroll);
});
}
fileListToggle(startVisible: boolean): void {
const showBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-show');
const hideBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-hide');
const fileList: HTMLElement | null = this.targetElement.querySelector('.d2h-file-list');
2019-12-29 22:31:32 +00:00
if (showBtn === null || hideBtn === null || fileList === null) return;
const show: () => void = () => {
2019-12-29 22:31:32 +00:00
showBtn.style.display = 'none';
hideBtn.style.display = 'inline';
fileList.style.display = 'block';
};
2019-12-29 22:31:32 +00:00
const hide: () => void = () => {
2019-12-29 22:31:32 +00:00
showBtn.style.display = 'inline';
hideBtn.style.display = 'none';
fileList.style.display = 'none';
};
2019-12-29 22:31:32 +00:00
showBtn.addEventListener('click', () => show());
hideBtn.addEventListener('click', () => hide());
const hashTag = this.getHashTag();
2019-12-29 22:31:32 +00:00
if (hashTag === 'files-summary-show') show();
else if (hashTag === 'files-summary-hide') hide();
else if (startVisible) show();
else hide();
}
2021-01-23 15:07:14 +00:00
fileContentToggle(): void {
this.targetElement.querySelectorAll<HTMLElement>('.d2h-file-collapse').forEach(fileContentToggleBtn => {
fileContentToggleBtn.style.display = 'flex';
const toggleFileContents: (selector: string) => void = selector => {
const fileContents: HTMLElement | null | undefined = fileContentToggleBtn
2021-01-23 15:07:14 +00:00
.closest('.d2h-file-wrapper')
?.querySelector(selector);
2021-01-23 15:07:14 +00:00
2021-01-23 22:32:50 +00:00
if (fileContents !== null && fileContents !== undefined) {
fileContentToggleBtn.classList.toggle('d2h-selected');
fileContents.classList.toggle('d2h-d-none');
}
};
const toggleHandler: (e: Event) => void = e => {
if (fileContentToggleBtn === e.target) return;
2021-01-23 15:07:14 +00:00
toggleFileContents('.d2h-file-diff');
toggleFileContents('.d2h-files-diff');
2021-01-23 15:07:14 +00:00
};
fileContentToggleBtn.addEventListener('click', e => toggleHandler(e));
2021-01-23 15:07:14 +00:00
});
}
2019-12-29 22:31:32 +00:00
highlightCode(): void {
if (this.hljs === null) {
throw new Error('Missing a `highlight.js` implementation. Please provide one when instantiating Diff2HtmlUI.');
}
// Collect all the diff files and execute the highlight on their lines
const files = this.targetElement.querySelectorAll('.d2h-file-wrapper');
files.forEach(file => {
2021-02-20 13:43:55 +00:00
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
if (this.hljs === null) return;
const language = file.getAttribute('data-lang');
const hljsLanguage = language ? this.hljs.getLanguage(language) : undefined;
2019-12-29 22:31:32 +00:00
// Collect all the code lines and execute the highlight on them
const codeLines = file.querySelectorAll('.d2h-code-line-ctn');
codeLines.forEach(line => {
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
if (this.hljs === null) return;
const text = line.textContent;
const lineParent = line.parentNode;
2019-12-29 22:31:32 +00:00
if (text === null || lineParent === null || !this.isElement(lineParent)) return;
2019-12-29 22:31:32 +00:00
2021-02-20 13:43:55 +00:00
const result: HighlightResult = closeTags(
this.hljs.highlight(text, {
language: hljsLanguage?.name || 'plaintext',
ignoreIllegals: true,
}),
);
2019-12-29 22:31:32 +00:00
const originalStream = nodeStream(line);
if (originalStream.length) {
const resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
}
line.classList.add('hljs');
2020-08-15 13:40:09 +00:00
if (result.language) {
line.classList.add(result.language);
}
2019-12-29 22:31:32 +00:00
line.innerHTML = result.value;
});
});
}
/**
* @deprecated since version 3.1.0
*/
smartSelection(): void {
console.warn('Smart selection is now enabled by default with CSS. No need to call this method anymore.');
2019-12-29 22:31:32 +00:00
}
private getHashTag(): string | null {
const docUrl = document.URL;
const hashTagIndex = docUrl.indexOf('#');
let hashTag = null;
if (hashTagIndex !== -1) {
hashTag = docUrl.substr(hashTagIndex + 1);
}
return hashTag;
}
private isElement(arg?: unknown): arg is Element {
return arg !== null && (arg as Element)?.classList !== undefined;
}
2019-12-29 22:31:32 +00:00
}