| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- import { type RefObject, useCallback, useRef } from 'react';
- import type { GlobalCodeMirrorEditorKey } from '@growi/editor';
- import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
- let defaultTop = 0;
- const padding = 5;
- const setDefaultTop = (top: number): void => {
- defaultTop = top;
- };
- const getDefaultTop = (): number => {
- return defaultTop + padding;
- };
- const getDataLine = (element: Element | null): number => {
- return element ? +(element.getAttribute('data-line') ?? '0') - 1 : 0;
- };
- const getEditorElements = (editorRootElement: HTMLElement): Array<Element> => {
- return Array.from(editorRootElement.getElementsByClassName('cm-line')).filter(
- (element) => {
- return !Number.isNaN(element.getAttribute('data-line') ?? Number.NaN);
- },
- );
- };
- const getPreviewElements = (
- previewRootElement: HTMLElement,
- ): Array<Element> => {
- return Array.from(
- previewRootElement.getElementsByClassName('has-data-line'),
- ).filter((element) => {
- return !Number.isNaN(element.getAttribute('data-line') ?? Number.NaN);
- });
- };
- // Ref: https://github.com/mikolalysenko/binary-search-bounds/blob/f436a2a8af11bf3208434e18bbac17e18e7a3a30/search-bounds.js
- const elementBinarySearch = (
- list: Array<Element>,
- fn: (index: number) => boolean,
- ): number => {
- let ok = 0;
- let ng = list.length;
- while (ok + 1 < ng) {
- const mid = Math.floor((ok + ng) / 2);
- if (fn(mid)) {
- ok = mid;
- } else {
- ng = mid;
- }
- }
- return ok;
- };
- const findTopElementIndex = (elements: Array<Element>): number => {
- const find = (index: number): boolean => {
- return elements[index].getBoundingClientRect().top < getDefaultTop();
- };
- return elementBinarySearch(elements, find);
- };
- const findElementIndexFromDataLine = (
- previewElements: Array<Element>,
- dataline: number,
- ): number => {
- const find = (index: number): boolean => {
- return getDataLine(previewElements[index]) <= dataline;
- };
- return elementBinarySearch(previewElements, find);
- };
- type SourceElement = {
- start?: DOMRect;
- top?: DOMRect;
- next?: DOMRect;
- };
- type TargetElement = {
- start?: DOMRect;
- next?: DOMRect;
- };
- const calcScrollElementToTop = (element: Element): number => {
- return element.getBoundingClientRect().top - getDefaultTop();
- };
- const calcScorllElementByRatio = (
- sourceElement: SourceElement,
- targetElement: TargetElement,
- ): number => {
- if (sourceElement.start === sourceElement.next) {
- return 0;
- }
- if (
- sourceElement.start == null ||
- sourceElement.top == null ||
- sourceElement.next == null
- ) {
- return 0;
- }
- if (targetElement.start == null || targetElement.next == null) {
- return 0;
- }
- const sourceAllHeight = sourceElement.next.top - sourceElement.start.top;
- const sourceOutHeight = sourceElement.top.top - sourceElement.start.top;
- const sourceTopHeight = getDefaultTop() - sourceElement.top.top;
- const sourceRaito = (sourceOutHeight + sourceTopHeight) / sourceAllHeight;
- const targetAllHeight = targetElement.next.top - targetElement.start.top;
- return targetAllHeight * sourceRaito;
- };
- const scrollEditor = (
- editorRootElement: HTMLElement,
- previewRootElement: HTMLElement,
- ): void => {
- setDefaultTop(editorRootElement.getBoundingClientRect().top);
- const editorElements = getEditorElements(editorRootElement);
- const previewElements = getPreviewElements(previewRootElement);
- const topEditorElementIndex = findTopElementIndex(editorElements);
- const topPreviewElementIndex = findElementIndexFromDataLine(
- previewElements,
- getDataLine(editorElements[topEditorElementIndex]),
- );
- const startEditorElementIndex = findElementIndexFromDataLine(
- editorElements,
- getDataLine(previewElements[topPreviewElementIndex]),
- );
- const nextEditorElementIndex = findElementIndexFromDataLine(
- editorElements,
- getDataLine(previewElements[topPreviewElementIndex + 1]),
- );
- let newScrollTop = previewRootElement.scrollTop;
- if (previewElements[topPreviewElementIndex] == null) {
- return;
- }
- newScrollTop += calcScrollElementToTop(
- previewElements[topPreviewElementIndex],
- );
- newScrollTop += calcScorllElementByRatio(
- {
- start: editorElements[startEditorElementIndex]?.getBoundingClientRect(),
- top: editorElements[topEditorElementIndex]?.getBoundingClientRect(),
- next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
- },
- {
- start: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
- next: previewElements[
- topPreviewElementIndex + 1
- ]?.getBoundingClientRect(),
- },
- );
- previewRootElement.scrollTop = newScrollTop;
- };
- const scrollPreview = (
- editorRootElement: HTMLElement,
- previewRootElement: HTMLElement,
- ): void => {
- setDefaultTop(previewRootElement.getBoundingClientRect().y);
- const previewElements = getPreviewElements(previewRootElement);
- const editorElements = getEditorElements(editorRootElement);
- const topPreviewElementIndex = findTopElementIndex(previewElements);
- const startEditorElementIndex = findElementIndexFromDataLine(
- editorElements,
- getDataLine(previewElements[topPreviewElementIndex]),
- );
- const nextEditorElementIndex = findElementIndexFromDataLine(
- editorElements,
- getDataLine(previewElements[topPreviewElementIndex + 1]),
- );
- if (editorElements[startEditorElementIndex] == null) {
- return;
- }
- let newScrollTop = editorRootElement.scrollTop;
- newScrollTop += calcScrollElementToTop(
- editorElements[startEditorElementIndex],
- );
- newScrollTop += calcScorllElementByRatio(
- {
- start: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
- top: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
- next: previewElements[
- topPreviewElementIndex + 1
- ]?.getBoundingClientRect(),
- },
- {
- start: editorElements[startEditorElementIndex]?.getBoundingClientRect(),
- next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
- },
- );
- editorRootElement.scrollTop = newScrollTop;
- };
- // eslint-disable-next-line max-len
- export const useScrollSync = (
- codeMirrorKey: GlobalCodeMirrorEditorKey,
- previewRef: RefObject<HTMLDivElement | null>,
- ): { scrollEditorHandler: () => void; scrollPreviewHandler: () => void } => {
- const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(codeMirrorKey);
- const isOriginOfScrollSyncEditor = useRef(false);
- const isOriginOfScrollSyncPreview = useRef(false);
- const scrollEditorHandler = useCallback(() => {
- if (
- codeMirrorEditor?.view?.scrollDOM == null ||
- previewRef.current == null
- ) {
- return;
- }
- if (isOriginOfScrollSyncPreview.current) {
- isOriginOfScrollSyncPreview.current = false;
- return;
- }
- isOriginOfScrollSyncEditor.current = true;
- scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
- }, [codeMirrorEditor, previewRef]);
- const scrollPreviewHandler = useCallback(() => {
- if (
- codeMirrorEditor?.view?.scrollDOM == null ||
- previewRef.current == null
- ) {
- return;
- }
- if (isOriginOfScrollSyncEditor.current) {
- isOriginOfScrollSyncEditor.current = false;
- return;
- }
- isOriginOfScrollSyncPreview.current = true;
- scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
- }, [codeMirrorEditor, previewRef]);
- return { scrollEditorHandler, scrollPreviewHandler };
- };
|