/** * This class is copied from Microsoft/vscode repository * @see https://github.com/Microsoft/vscode/blob/0532a3429a18688a0c086a4212e7e5b4888b2a48/extensions/markdown/media/main.js */ class ScrollSyncHelper { /** * @typedef {{ element: Element, line: number }} CodeLineElement */ constructor() { } getCodeLineElements(parentElement) { /** @type {CodeLineElement[]} */ let elements; if (!elements) { elements = Array.prototype.map.call( parentElement.getElementsByClassName('code-line'), element => { const line = +element.getAttribute('data-line'); return { element, line }; }) .filter(x => !isNaN(x.line)); } return elements; } /** * Find the html elements that map to a specific target line in the editor. * * If an exact match, returns a single element. If the line is between elements, * returns the element prior to and the element after the given line. * * @param {Element} element * @param {number} targetLine * * @returns {{ previous: CodeLineElement, next?: CodeLineElement }} */ getElementsForSourceLine(element, targetLine) { const lines = this.getCodeLineElements(element); let previous = lines[0] || null; for (const entry of lines) { if (entry.line === targetLine) { return { previous: entry, next: null }; } else if (entry.line > targetLine) { return { previous, next: entry }; } previous = entry; } return { previous }; } /** * Find the html elements that are at a specific pixel offset on the page. * * @param {Element} parentElement * @param {number} offset * * @returns {{ previous: CodeLineElement, next?: CodeLineElement }} */ getLineElementsAtPageOffset(parentElement, offset) { const lines = this.getCodeLineElements(parentElement); const position = offset - parentElement.scrollTop + this.getParentElementOffset(parentElement); let lo = -1; let hi = lines.length - 1; while (lo + 1 < hi) { const mid = Math.floor((lo + hi) / 2); const bounds = lines[mid].element.getBoundingClientRect(); if (bounds.top + bounds.height >= position) { hi = mid; } else { lo = mid; } } const hiElement = lines[hi]; if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) { const loElement = lines[lo]; const bounds = loElement.element.getBoundingClientRect(); let previous = { element: loElement.element, line: loElement.line }; if (bounds.height > 0) { previous.line += (position - bounds.top) / (bounds.height); } const next = { element: hiElement.element, line: hiElement.line, fractional: 0 }; return { previous, next }; } const bounds = hiElement.element.getBoundingClientRect(); const previous = { element: hiElement.element, line: hiElement.line + (position - bounds.top) / (bounds.height) }; return { previous }; } getEditorLineNumberForPageOffset(parentElement, offset) { const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset); if (previous) { if (next) { const betweenProgress = (offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top) / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top); return previous.line + betweenProgress * (next.line - previous.line); } else { return previous.line; } } return null; } /** * return the sum of the offset position of parent element and paddingTop * @param {Element} parentElement */ getParentElementOffset(parentElement) { const offsetY = parentElement.getBoundingClientRect().top; // get paddingTop const style = window.getComputedStyle(parentElement, null); const paddingTop = +(style.paddingTop.replace('px', '')); return offsetY + paddingTop; } /** * Attempt to scroll preview element for a source line in the editor. * * @param {Element} previewElement * @param {number} line */ scrollPreview(previewElement, line) { const { previous, next } = this.getElementsForSourceLine(previewElement, line); if (previous) { let scrollTo = 0; if (next) { // Between two elements. Go to percentage offset between them. const betweenProgress = (line - previous.line) / (next.line - previous.line); const elementOffset = next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top; scrollTo = previous.element.getBoundingClientRect().top + betweenProgress * elementOffset; } else { scrollTo = previous.element.getBoundingClientRect().top; } scrollTo -= this.getParentElementOffset(previewElement); previewElement.scroll(0, previewElement.scrollTop + scrollTo); } } /** * Attempt to reveal the element that is overflowing from previewElement. * * @param {Element} previewElement * @param {number} line */ scrollPreviewToRevealOverflowing(previewElement, line) { const { previous, next } = this.getElementsForSourceLine(previewElement, line); if (previous) { const parentElementOffset = this.getParentElementOffset(previewElement); const prevElmTop = previous.element.getBoundingClientRect().top - parentElementOffset; const prevElmBottom = previous.element.getBoundingClientRect().bottom - parentElementOffset; let scrollTo = null; if (prevElmTop < 0) { // set the top of 'previous.element' to the top of 'previewElement' scrollTo = previewElement.scrollTop + prevElmTop; } else if (prevElmBottom > previewElement.clientHeight) { // set the bottom of 'previous.element' to the bottom of 'previewElement' scrollTo = previewElement.scrollTop + prevElmBottom - previewElement.clientHeight + 20; } if (scrollTo == null) { return; } previewElement.scroll(0, scrollTo); } } /** * Attempt to scroll Editor component for the offset of the element in the Preview component. * * @param {Editor} editor * @param {Element} previewElement * @param {number} offset */ scrollEditor(editor, previewElement, offset) { let line = this.getEditorLineNumberForPageOffset(previewElement, offset); line = Math.floor(line); editor.setScrollTopByLine(line); } } // singleton pattern const instance = new ScrollSyncHelper(); Object.freeze(instance); export default instance;