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

222 lines
7.4 KiB
TypeScript
Raw Normal View History

2020-05-09 11:05:18 +00:00
import * as HighlightJS from 'highlight.js/lib/core';
2020-08-15 13:40:09 +00:00
// import { CompiledMode, HighlightResult, AutoHighlightResult } from 'highlight.js/lib/core.js';
2019-12-29 22:31:32 +00:00
import { nodeStream, mergeStreams } from './highlight.js-helpers';
import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
import { DiffFile } from '../../types';
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;
readonly hljs: typeof HighlightJS | null = null;
2019-12-29 22:31:32 +00:00
currentSelectionColumnId = -1;
constructor(
target: HTMLElement,
diffInput?: string | DiffFile[],
config: Diff2HtmlUIConfig = {},
hljs?: typeof HighlightJS,
) {
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('.d2h-file-collapse').forEach(fileContentToggleBtn => {
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 => {
2020-08-15 13:40:09 +00:00
let oldLinesState: CompiledMode | Language | undefined;
let newLinesState: CompiledMode | 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
const lineState = lineParent.classList.contains('d2h-del') ? oldLinesState : newLinesState;
2019-12-29 22:31:32 +00:00
const language = file.getAttribute('data-lang');
2020-08-15 13:40:09 +00:00
const result: HighlightResult =
2019-12-29 22:31:32 +00:00
language && this.hljs.getLanguage(language)
? this.hljs.highlight(language, text, true, lineState)
: this.hljs.highlightAuto(text);
2020-08-15 13:40:09 +00:00
if (this.instanceOfHighlightResult(result)) {
if (lineParent.classList.contains('d2h-del')) {
2019-12-29 22:31:32 +00:00
oldLinesState = result.top;
} else if (lineParent.classList.contains('d2h-ins')) {
2019-12-29 22:31:32 +00:00
newLinesState = result.top;
} else {
oldLinesState = result.top;
newLinesState = result.top;
}
}
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
}
2020-08-15 13:40:09 +00:00
private instanceOfHighlightResult(object: HighlightResult | AutoHighlightResult): object is HighlightResult {
return 'top' in object;
}
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
}