ScrollSyncHelper.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. let defaultTop = 0;
  2. const padding = 5;
  3. const setDefaultTop = (top: number): void => {
  4. defaultTop = top;
  5. };
  6. const getDefaultTop = (): number => {
  7. return defaultTop + padding;
  8. };
  9. const getDataLine = (element: Element | null): number => {
  10. return element ? +(element.getAttribute('data-line') ?? '0') - 1 : 0;
  11. };
  12. const getEditorElements = (editorRootElement: HTMLElement): Array<Element> => {
  13. return Array.from(editorRootElement.getElementsByClassName('cm-line'))
  14. .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
  15. };
  16. const getPreviewElements = (previewRootElement: HTMLElement): Array<Element> => {
  17. return Array.from(previewRootElement.getElementsByClassName('has-data-line'))
  18. .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
  19. };
  20. // Ref: https://github.com/mikolalysenko/binary-search-bounds/blob/f436a2a8af11bf3208434e18bbac17e18e7a3a30/search-bounds.js
  21. const elementBinarySearch = (list: Array<Element>, fn: (index: number) => boolean): number => {
  22. let ok = 0;
  23. let ng = list.length;
  24. while (ok + 1 < ng) {
  25. const mid = Math.floor((ok + ng) / 2);
  26. if (fn(mid)) {
  27. ok = mid;
  28. }
  29. else {
  30. ng = mid;
  31. }
  32. }
  33. return ok;
  34. };
  35. const findTopElementIndex = (elements: Array<Element>): number => {
  36. const find = (index: number): boolean => {
  37. return elements[index].getBoundingClientRect().top < getDefaultTop();
  38. };
  39. return elementBinarySearch(elements, find);
  40. };
  41. const findElementIndexFromDataLine = (previewElements: Array<Element>, dataline: number): number => {
  42. const find = (index: number): boolean => {
  43. return getDataLine(previewElements[index]) <= dataline;
  44. };
  45. return elementBinarySearch(previewElements, find);
  46. };
  47. type SourceElement = {
  48. start: DOMRect,
  49. top: DOMRect,
  50. next: DOMRect | undefined,
  51. }
  52. type TargetElement = {
  53. start: DOMRect,
  54. next: DOMRect | undefined,
  55. }
  56. const calcScrollElementToTop = (element: Element): number => {
  57. return element.getBoundingClientRect().top - getDefaultTop();
  58. };
  59. const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: TargetElement): number => {
  60. if (sourceElement.start === sourceElement.next || sourceElement.next == null || targetElement.next == null) {
  61. return 0;
  62. }
  63. const sourceAllHeight = sourceElement.next.top - sourceElement.start.top;
  64. const sourceOutHeight = sourceElement.top.top - sourceElement.start.top;
  65. const sourceTopHeight = getDefaultTop() - sourceElement.top.top;
  66. const sourceRaito = (sourceOutHeight + sourceTopHeight) / sourceAllHeight;
  67. const targetAllHeight = targetElement.next.top - targetElement.start.top;
  68. return targetAllHeight * sourceRaito;
  69. };
  70. export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
  71. setDefaultTop(editorRootElement.getBoundingClientRect().top);
  72. const editorElements = getEditorElements(editorRootElement);
  73. const previewElements = getPreviewElements(previewRootElement);
  74. const topEditorElementIndex = findTopElementIndex(editorElements);
  75. const topPreviewElementIndex = findElementIndexFromDataLine(previewElements, getDataLine(editorElements[topEditorElementIndex]));
  76. const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
  77. const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
  78. let newScrollTop = previewRootElement.scrollTop;
  79. newScrollTop += calcScrollElementToTop(previewElements[topPreviewElementIndex]);
  80. newScrollTop += calcScorllElementByRatio(
  81. {
  82. start: editorElements[startEditorElementIndex].getBoundingClientRect(),
  83. top: editorElements[topEditorElementIndex].getBoundingClientRect(),
  84. next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
  85. },
  86. {
  87. start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
  88. next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
  89. },
  90. );
  91. previewRootElement.scrollTop = newScrollTop;
  92. };
  93. export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
  94. setDefaultTop(previewRootElement.getBoundingClientRect().y);
  95. const previewElements = getPreviewElements(previewRootElement);
  96. const editorElements = getEditorElements(editorRootElement);
  97. const topPreviewElementIndex = findTopElementIndex(previewElements);
  98. const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
  99. const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
  100. let newScrollTop = editorRootElement.scrollTop;
  101. newScrollTop += calcScrollElementToTop(editorElements[startEditorElementIndex]);
  102. newScrollTop += calcScorllElementByRatio(
  103. {
  104. start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
  105. top: previewElements[topPreviewElementIndex].getBoundingClientRect(),
  106. next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
  107. },
  108. {
  109. start: editorElements[startEditorElementIndex].getBoundingClientRect(),
  110. next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
  111. },
  112. );
  113. editorRootElement.scrollTop = newScrollTop;
  114. };