diff --git a/src/static/js/caretPosition.ts b/src/static/js/caretPosition.ts index 236584a4c..d7f7bf4b6 100644 --- a/src/static/js/caretPosition.ts +++ b/src/static/js/caretPosition.ts @@ -3,8 +3,11 @@ // One rep.line(div) can be broken in more than one line in the browser. // This function is useful to get the caret position of the line as // is represented by the browser +import {Position, RepModel, RepNode} from "./types/RepModel"; + export const getPosition = () => { const range = getSelectionRange(); + // @ts-ignore if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null; // When there's a
or any element that has no height, we can't get the dimension of the // element where the caret is. As we can't get the element height, we create a text node to get @@ -18,7 +21,7 @@ export const getPosition = () => { return line; }; -const createSelectionRange = (range) => { +const createSelectionRange = (range: Range) => { const clonedRange = range.cloneRange(); // we set the selection start and end to avoid error when user selects a text bigger than @@ -30,14 +33,14 @@ const createSelectionRange = (range) => { return clonedRange; }; -const getPositionOfRepLineAtOffset = (node, offset) => { +const getPositionOfRepLineAtOffset = (node: any, offset: number) => { // it is not a text node, so we cannot make a selection if (node.tagName === 'BR' || node.tagName === 'EMPTY') { return getPositionOfElementOrSelection(node); } while (node.length === 0 && node.nextSibling) { - node = node.nextSibling; + node = node.nextSibling as any; } const newRange = new Range(); @@ -48,14 +51,13 @@ const getPositionOfRepLineAtOffset = (node, offset) => { return linePosition; }; -const getPositionOfElementOrSelection = (element) => { +const getPositionOfElementOrSelection = (element: Range):Position => { const rect = element.getBoundingClientRect(); - const linePosition = { + return { bottom: rect.bottom, height: rect.height, top: rect.top, - }; - return linePosition; + } satisfies Position; }; // here we have two possibilities: @@ -64,7 +66,7 @@ const getPositionOfElementOrSelection = (element) => { // where is the top of the previous line // [2] the line before is part of another rep line. It's possible this line has different margins // height. So we have to get the exactly position of the line -export const getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => { +export const getPositionTopOfPreviousBrowserLine = (caretLinePosition: Position, rep: RepModel) => { let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1] const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep); @@ -80,7 +82,7 @@ export const getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => { return previousLineTop; }; -const caretLineIsFirstBrowserLine = (caretLineTop, rep) => { +const caretLineIsFirstBrowserLine = (caretLineTop: number, rep: RepModel) => { const caretRepLine = rep.selStart[0]; const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const firstRootNode = getFirstRootChildNode(lineNode); @@ -91,7 +93,7 @@ const caretLineIsFirstBrowserLine = (caretLineTop, rep) => { }; // find the first root node, usually it is a text node -const getFirstRootChildNode = (node) => { +const getFirstRootChildNode = (node: RepNode) => { if (!node.firstChild) { return node; } else { @@ -99,7 +101,7 @@ const getFirstRootChildNode = (node) => { } }; -const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => { +const getDimensionOfLastBrowserLineOfRepLine = (line: number, rep: RepModel) => { const lineNode = rep.lines.atIndex(line).lineNode; const lastRootChildNode = getLastRootChildNode(lineNode); @@ -109,7 +111,7 @@ const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => { return lastRootChildNodePosition; }; -const getLastRootChildNode = (node) => { +const getLastRootChildNode = (node: RepNode) => { if (!node.lastChild) { return { node, @@ -125,7 +127,7 @@ const getLastRootChildNode = (node) => { // So, we can use the caret line to calculate the bottom of the line. // [2] the next line is part of another rep line. // It's possible this line has different dimensions, so we have to get the exactly dimension of it -export const getBottomOfNextBrowserLine = (caretLinePosition, rep) => { +export const getBottomOfNextBrowserLine = (caretLinePosition: Position, rep: RepModel) => { let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1] const isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep); @@ -142,7 +144,7 @@ export const getBottomOfNextBrowserLine = (caretLinePosition, rep) => { return nextLineBottom; }; -const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => { +const caretLineIsLastBrowserLineOfRepLine = (caretLineTop: number, rep: RepModel) => { const caretRepLine = rep.selStart[0]; const lineNode = rep.lines.atIndex(caretRepLine).lineNode; const lastRootChildNode = getLastRootChildNode(lineNode); @@ -153,7 +155,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => { return lastRootChildNodePosition.top === caretLineTop; }; -export const getPreviousVisibleLine = (line, rep) => { +export const getPreviousVisibleLine = (line: number, rep: RepModel): number => { const firstLineOfPad = 0; if (line <= firstLineOfPad) { return firstLineOfPad; @@ -166,7 +168,7 @@ export const getPreviousVisibleLine = (line, rep) => { -export const getNextVisibleLine = (line, rep) => { +export const getNextVisibleLine = (line: number, rep: RepModel): number => { const lastLineOfThePad = rep.lines.length() - 1; if (line >= lastLineOfThePad) { return lastLineOfThePad; @@ -177,9 +179,9 @@ export const getNextVisibleLine = (line, rep) => { } }; -const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0; +const isLineVisible = (line: number, rep: RepModel) => rep.lines.atIndex(line).lineNode.offsetHeight > 0; -const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => { +const getDimensionOfFirstBrowserLineOfRepLine = (line: number, rep: RepModel) => { const lineNode = rep.lines.atIndex(line).lineNode; const firstRootChildNode = getFirstRootChildNode(lineNode); diff --git a/src/static/js/scroll.ts b/src/static/js/scroll.ts index 0d2dac44c..95075d807 100644 --- a/src/static/js/scroll.ts +++ b/src/static/js/scroll.ts @@ -1,4 +1,5 @@ import {getBottomOfNextBrowserLine, getNextVisibleLine, getPosition, getPositionTopOfPreviousBrowserLine, getPreviousVisibleLine} from './caretPosition'; +import {Position, RepModel, RepNode, WindowElementWithScrolling} from "./types/RepModel"; class Scroll { @@ -8,6 +9,7 @@ class Scroll { private scrollSettings: any; constructor(outerWin: HTMLIFrameElement) { + // @ts-ignore this.scrollSettings = window.clientVars.scrollWhenFocusLineIsOutOfViewport; // DOM reference @@ -16,7 +18,7 @@ class Scroll { this.rootDocument = parent.parent.document; } - scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight) { + scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) { // are we placing the caret on the line at the bottom of viewport? // And if so, do we need to scroll the editor, as defined on the settings.json? const shouldScrollWhenCaretIsAtBottomOfViewport = @@ -36,7 +38,7 @@ class Scroll { } } - scrollWhenPressArrowKeys(arrowUp, rep, innerHeight) { + scrollWhenPressArrowKeys(arrowUp: boolean, rep: RepModel, innerHeight: number) { // if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous // rep line on the top of the viewport if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) { @@ -50,7 +52,7 @@ class Scroll { } } - _isCaretAtTheBottomOfViewport(rep) { + _isCaretAtTheBottomOfViewport(rep: RepModel) { // computing a line position using getBoundingClientRect() is expensive. // (obs: getBoundingClientRect() is called on caretPosition.getPosition()) // To avoid that, we only call this function when it is possible that the @@ -64,16 +66,15 @@ class Scroll { this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep); if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) { // check if the caret is in the bottom of the viewport - const caretLinePosition = getPosition(); + const caretLinePosition = getPosition()!; const viewportBottom = this._getViewPortTopBottom().bottom; const nextLineBottom = getBottomOfNextBrowserLine(caretLinePosition, rep); - const nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom; - return nextLineIsBelowViewportBottom; + return nextLineBottom > viewportBottom; } return false; }; - _isLinePartiallyVisibleOnViewport(lineNumber, rep){ + _isLinePartiallyVisibleOnViewport(lineNumber: number, rep: RepModel){ const lineNode = rep.lines.atIndex(lineNumber); const linePosition = this._getLineEntryTopBottom(lineNode); const lineTop = linePosition.top; @@ -123,7 +124,7 @@ class Scroll { }; _getScrollXY() { - const win = this.outerWin; + const win = this.outerWin as WindowElementWithScrolling; const odoc = this.doc; if (typeof (win.pageYOffset) === 'number') { return { @@ -141,26 +142,26 @@ class Scroll { }; getScrollX() { - return this._getScrollXY().x; + return this._getScrollXY()!.x; }; getScrollY () { - return this._getScrollXY().y; + return this._getScrollXY()!.y; }; - setScrollX(x) { + setScrollX(x: number) { this.outerWin.scrollTo(x, this.getScrollY()); }; - setScrollY(y) { + setScrollY(y: number) { this.outerWin.scrollTo(this.getScrollX(), y); }; - setScrollXY(x, y) { + setScrollXY(x: number, y: number) { this.outerWin.scrollTo(x, y); }; - _isCaretAtTheTopOfViewport(rep) { + _isCaretAtTheTopOfViewport(rep: RepModel) { const caretLine = rep.selStart[0]; const linePrevCaretLine = caretLine - 1; const firstLineVisibleBeforeCaretLine = @@ -174,12 +175,12 @@ class Scroll { const viewportPosition = this._getViewPortTopBottom(); const viewportTop = viewportPosition.top; const viewportBottom = viewportPosition.bottom; - const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop; - const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom; + const caretLineIsBelowViewportTop = caretLinePosition!.bottom >= viewportTop; + const caretLineIsAboveViewportBottom = caretLinePosition!.top < viewportBottom; const caretLineIsInsideOfViewport = caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom; if (caretLineIsInsideOfViewport) { - const prevLineTop = getPositionTopOfPreviousBrowserLine(caretLinePosition, rep); + const prevLineTop = getPositionTopOfPreviousBrowserLine(caretLinePosition!, rep); const previousLineIsAboveViewportTop = prevLineTop < viewportTop; return previousLineIsAboveViewportTop; } @@ -190,18 +191,18 @@ class Scroll { // By default, when user makes an edition in a line out of viewport, this line goes // to the edge of viewport. This function gets the extra pixels necessary to get the // caret line in a position X relative to Y% viewport. - _getPixelsRelativeToPercentageOfViewport(innerHeight, aboveOfViewport) { + _getPixelsRelativeToPercentageOfViewport(innerHeight: number, aboveOfViewport?: boolean) { let pixels = 0; const scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport); if (scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1) { - pixels = parseInt(innerHeight * scrollPercentageRelativeToViewport); + pixels = parseInt(String(innerHeight * scrollPercentageRelativeToViewport)); } return pixels; }; // we use different percentages when change selection. It depends on if it is // either above the top or below the bottom of the page - _getPercentageToScroll(aboveOfViewport: boolean) { + _getPercentageToScroll(aboveOfViewport: boolean|undefined) { let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport; if (aboveOfViewport) { percentageToScroll = this.scrollSettings.percentage.editionAboveViewport; @@ -209,16 +210,16 @@ class Scroll { return percentageToScroll; }; - _getPixelsToScrollWhenUserPressesArrowUp(innerHeight) { + _getPixelsToScrollWhenUserPressesArrowUp(innerHeight: number) { let pixels = 0; const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp; if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) { - pixels = parseInt(innerHeight * percentageToScrollUp); + pixels = parseInt(String(innerHeight * percentageToScrollUp)); } return pixels; }; - _scrollYPage(pixelsToScroll) { + _scrollYPage(pixelsToScroll: number) { const durationOfAnimationToShowFocusline = this.scrollSettings.duration; if (durationOfAnimationToShowFocusline) { this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline); @@ -227,15 +228,15 @@ class Scroll { } }; - _scrollYPageWithoutAnimation(pixelsToScroll) { + _scrollYPageWithoutAnimation(pixelsToScroll: number) { this.outerWin.scrollBy(0, pixelsToScroll); }; - _scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline) { + _scrollYPageWithAnimation(pixelsToScroll: number, durationOfAnimationToShowFocusline: number) { const outerDocBody = this.doc.getElementById('outerdocbody'); // it works on later versions of Chrome - const $outerDocBody = $(outerDocBody); + const $outerDocBody = $(outerDocBody!); this._triggerScrollWithAnimation( $outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline); @@ -245,7 +246,7 @@ class Scroll { $outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline); }; - _triggerScrollWithAnimation($elem, pixelsToScroll, durationOfAnimationToShowFocusline) { + _triggerScrollWithAnimation($elem:any, pixelsToScroll: number, durationOfAnimationToShowFocusline: number) { // clear the queue of animation $elem.stop('scrollanimation'); $elem.animate({ @@ -258,7 +259,7 @@ class Scroll { - scrollNodeVerticallyIntoView(rep, innerHeight) { + scrollNodeVerticallyIntoView(rep: RepModel, innerHeight: number) { const viewport = this._getViewPortTopBottom(); // when the selection changes outside of the viewport the browser automatically scrolls the line @@ -288,7 +289,7 @@ class Scroll { } }; - _partOfRepLineIsOutOfViewport(viewportPosition, rep) { + _partOfRepLineIsOutOfViewport(viewportPosition: Position, rep: RepModel) { const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]); const line = rep.lines.atIndex(focusLine); const linePosition = this._getLineEntryTopBottom(line); @@ -298,25 +299,25 @@ class Scroll { return lineIsBelowOfViewport || lineIsAboveOfViewport; }; - _getLineEntryTopBottom(entry, destObj) { + _getLineEntryTopBottom(entry: RepNode, destObj?: Position) { const dom = entry.lineNode; const top = dom.offsetTop; const height = dom.offsetHeight; - const obj = (destObj || {}); + const obj = (destObj || {}) as Position; obj.top = top; obj.bottom = (top + height); return obj; }; - _arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep) { + _arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp: boolean, rep: RepModel) { const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp; return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep); }; - getVisibleLineRange(rep) { + getVisibleLineRange(rep: RepModel) { const viewport = this._getViewPortTopBottom(); // console.log("viewport top/bottom: %o", viewport); - const obj = {}; + const obj = {} as Position; const self = this; const start = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).bottom > viewport.top); // return the first line that the top position is greater or equal than @@ -328,7 +329,7 @@ class Scroll { return [start, end - 1]; }; - getVisibleCharRange(rep) { + getVisibleCharRange(rep: RepModel) { const lineRange = this.getVisibleLineRange(rep); return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])]; }; diff --git a/src/static/js/types/RepModel.ts b/src/static/js/types/RepModel.ts new file mode 100644 index 000000000..821549e1d --- /dev/null +++ b/src/static/js/types/RepModel.ts @@ -0,0 +1,31 @@ +export type RepModel = { + lines: { + atIndex: (num: number)=>RepNode, + offsetOfIndex: (range: number)=>number, + search: (filter: (e: RepNode)=>boolean)=>number, + length: ()=>number + } + selStart: number[], + selEnd: number[], + selFocusAtStart: boolean +} + +export type Position = { + bottom: number, + height: number, + top: number +} + +export type RepNode = { + firstChild: RepNode, + lineNode: RepNode + length: number, + lastChild: RepNode, + offsetHeight: number, + offsetTop: number +} + +export type WindowElementWithScrolling = HTMLIFrameElement & { + pageYOffset: number|string, + pageXOffset: number +}