Convert files to typescript

The change converts the following files to typescript:

* elements/diff/gr-diff-cursor/gr-diff-cursor.ts

Change-Id: Iedd1f6f02028dbed7afa4f6e7035c2d55ab410ee
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index 99c8cc1..ebd410e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -15,106 +15,106 @@
  * limitations under the License.
  */
 
-import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-diff-cursor_html.js';
-import {ScrollMode} from '../../../constants/constants.js';
-
-const DiffSides = {
-  LEFT: 'left',
-  RIGHT: 'right',
-};
+import '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-diff-cursor_html';
+import {ScrollMode} from '../../../constants/constants';
+import {customElement, property, observe} from '@polymer/decorators';
+import {DiffSide} from '../gr-diff/gr-diff-utils';
+import {GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
+import {PolymerDomWrapper} from '../../../types/types';
+import {GrDiffGroupType} from '../gr-diff/gr-diff-group';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
   UNIFIED: 'UNIFIED_DIFF',
 };
 
+type GrDiffRowType = GrDiffLineType | GrDiffGroupType;
+
 const LEFT_SIDE_CLASS = 'target-side-left';
 const RIGHT_SIDE_CLASS = 'target-side-right';
 
-/** @extends PolymerElement */
-class GrDiffCursor extends GestureEventListeners(
-    LegacyElementMixin(PolymerElement)) {
-  static get template() { return htmlTemplate; }
+// TODO(TS): Use proper GrDiff type once that file is converted to TS.
+interface GrDiff extends HTMLElement {
+  path: string;
+  addDraftAtLine: (element: HTMLElement) => void;
+  createRangeComment: () => void;
+  getCursorStops: () => HTMLElement[];
+  isRangeSelected: () => boolean;
+}
 
-  static get is() { return 'gr-diff-cursor'; }
+export interface GrDiffCursor {
+  $: {
+    cursorManager: GrCursorManager;
+  };
+}
 
-  static get properties() {
-    return {
-    /**
-     * Either DiffSides.LEFT or DiffSides.RIGHT.
-     */
-      side: {
-        type: String,
-        value: DiffSides.RIGHT,
-      },
-      /** @type {!HTMLElement|undefined} */
-      diffRow: {
-        type: Object,
-        notify: true,
-        observer: '_rowChanged',
-      },
-
-      /**
-       * The diff views to cursor through and listen to.
-       */
-      diffs: {
-        type: Array,
-        value() { return []; },
-      },
-
-      /**
-       * If set, the cursor will attempt to move to the line number (instead of
-       * the first chunk) the next time the diff renders. It is set back to null
-       * when used. It should be only used if you want the line to be focused
-       * after initialization of the component and page should scroll
-       * to that position. This parameter should be set at most for one gr-diff
-       * element in the page.
-       *
-       * @type {?number}
-       */
-      initialLineNumber: {
-        type: Number,
-        value: null,
-      },
-
-      /**
-       * The scroll behavior for the cursor. Values are 'never' and
-       * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
-       * the viewport.
-       */
-      _scrollMode: {
-        type: String,
-        value: ScrollMode.KEEP_VISIBLE,
-      },
-
-      _focusOnMove: {
-        type: Boolean,
-        value: true,
-      },
-
-      _listeningForScroll: Boolean,
-    };
+@customElement('gr-diff-cursor')
+export class GrDiffCursor extends GestureEventListeners(
+  LegacyElementMixin(PolymerElement)
+) {
+  static get template() {
+    return htmlTemplate;
   }
 
-  static get observers() {
-    return [
-      '_updateSideClass(side)',
-      '_diffsChanged(diffs.splices)',
-    ];
-  }
+  private _boundHandleWindowScroll: () => void;
+
+  private _boundHandleDiffRenderStart: () => void;
+
+  private _boundHandleDiffRenderContent: () => void;
+
+  private _boundHandleDiffLineSelected: (e: Event) => void;
+
+  private _preventAutoScrollOnManualScroll = false;
+
+  @property({type: String})
+  side = DiffSide.RIGHT;
+
+  @property({type: Object, notify: true, observer: '_rowChanged'})
+  diffRow?: HTMLElement;
+
+  @property({type: Object})
+  diffs: GrDiff[] = [];
+
+  /**
+   * If set, the cursor will attempt to move to the line number (instead of
+   * the first chunk) the next time the diff renders. It is set back to null
+   * when used. It should be only used if you want the line to be focused
+   * after initialization of the component and page should scroll
+   * to that position. This parameter should be set at most for one gr-diff
+   * element in the page.
+   */
+  @property({type: Number})
+  initialLineNumber: number | null = null;
+
+  /**
+   * The scroll behavior for the cursor. Values are 'never' and
+   * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+   * the viewport.
+   */
+  @property({type: String})
+  _scrollMode = ScrollMode.KEEP_VISIBLE;
+
+  @property({type: Boolean})
+  _focusOnMove = true;
+
+  @property({type: Boolean})
+  _listeningForScroll = false;
 
   constructor() {
     super();
     this._boundHandleWindowScroll = () => this._handleWindowScroll();
     this._boundHandleDiffRenderStart = () => this._handleDiffRenderStart();
     this._boundHandleDiffRenderContent = () => this._handleDiffRenderContent();
-    this._boundHandleDiffLineSelected = e => this._handleDiffLineSelected(e);
+    this._boundHandleDiffLineSelected = (e: Event) =>
+      this._handleDiffLineSelected(e);
   }
 
   /** @override */
@@ -131,9 +131,12 @@
       element with an actual lifecycle. This will be triggered only once
       per element.
       */
-      this.dispatchEvent(new CustomEvent('ready', {
-        composed: true, bubbles: false,
-      }));
+      this.dispatchEvent(
+        new CustomEvent('ready', {
+          composed: true,
+          bubbles: false,
+        })
+      );
     });
   }
 
@@ -151,14 +154,14 @@
   }
 
   moveLeft() {
-    this.side = DiffSides.LEFT;
+    this.side = DiffSide.LEFT;
     if (this._isTargetBlank()) {
       this.moveUp();
     }
   }
 
   moveRight() {
-    this.side = DiffSides.RIGHT;
+    this.side = DiffSide.RIGHT;
     if (this._isTargetBlank()) {
       this.moveUp();
     }
@@ -182,17 +185,19 @@
 
   moveToVisibleArea() {
     if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
-      this.$.cursorManager.moveToVisibleArea(
-          this._rowHasSide.bind(this));
+      this.$.cursorManager.moveToVisibleArea(this._rowHasSide.bind(this));
     } else {
       this.$.cursorManager.moveToVisibleArea();
     }
   }
 
-  moveToNextChunk(opt_clipToTop, opt_navigateToNextFile) {
-    this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
-        target => target.parentNode.scrollHeight, opt_clipToTop,
-        opt_navigateToNextFile);
+  moveToNextChunk(clipToTop?: boolean, navigateToNextFile?: boolean) {
+    this.$.cursorManager.next(
+      this._isFirstRowOfChunk.bind(this),
+      target => (target?.parentNode as HTMLElement)?.scrollHeight || 0,
+      clipToTop,
+      navigateToNextFile
+    );
     this._fixSide();
   }
 
@@ -211,13 +216,8 @@
     this._fixSide();
   }
 
-  /**
-   * @param {number} number
-   * @param {string} side
-   * @param {string=} opt_path
-   */
-  moveToLineNumber(number, side, opt_path) {
-    const row = this._findRowByNumberAndFile(number, side, opt_path);
+  moveToLineNumber(number: number, side: DiffSide, path?: string) {
+    const row = this._findRowByNumberAndFile(number, side, path);
     if (row) {
       this.side = side;
       this.$.cursorManager.setCursor(row);
@@ -226,31 +226,27 @@
 
   /**
    * Get the line number element targeted by the cursor row and side.
-   *
-   * @return {?Element|undefined}
    */
-  getTargetLineElement() {
+  getTargetLineElement(): HTMLElement | null {
     let lineElSelector = '.lineNum';
 
     if (!this.diffRow) {
-      return;
+      return null;
     }
 
     if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
-      lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
+      lineElSelector += this.side === DiffSide.LEFT ? '.left' : '.right';
     }
 
     return this.diffRow.querySelector(lineElSelector);
   }
 
-  getTargetDiffElement() {
+  getTargetDiffElement(): GrDiff | null {
     if (!this.diffRow) return null;
 
-    const hostOwner = dom( (this.diffRow))
-        .getOwnerRoot();
-    if (hostOwner && hostOwner.host &&
-        hostOwner.host.tagName === 'GR-DIFF') {
-      return hostOwner.host;
+    const hostOwner = (dom(this.diffRow) as PolymerDomWrapper).getOwnerRoot();
+    if (hostOwner?.host?.tagName === 'GR-DIFF') {
+      return hostOwner.host as GrDiff;
     }
     return null;
   }
@@ -277,9 +273,9 @@
   reInitCursor() {
     if (!this.diffRow) {
       // does not scroll during init unless requested
-      this._scrollMode = this.initialLineNumber ?
-        ScrollMode.KEEP_VISIBLE :
-        ScrollMode.NEVER;
+      this._scrollMode = this.initialLineNumber
+        ? ScrollMode.KEEP_VISIBLE
+        : ScrollMode.NEVER;
       if (this.initialLineNumber) {
         this.moveToLineNumber(this.initialLineNumber, this.side);
         this.initialLineNumber = null;
@@ -323,20 +319,26 @@
     this._preventAutoScrollOnManualScroll = false;
   }
 
-  _handleDiffLineSelected(event) {
+  _handleDiffLineSelected(event: Event) {
+    const customEvent = event as CustomEvent;
     this.moveToLineNumber(
-        event.detail.number, event.detail.side, event.detail.path);
+      customEvent.detail.number,
+      customEvent.detail.side,
+      customEvent.detail.path
+    );
   }
 
   createCommentInPlace() {
-    const diffWithRangeSelected = this.diffs
-        .find(diff => diff.isRangeSelected());
+    const diffWithRangeSelected = this.diffs.find(diff =>
+      diff.isRangeSelected()
+    );
     if (diffWithRangeSelected) {
       diffWithRangeSelected.createRangeComment();
     } else {
       const line = this.getTargetLineElement();
-      if (line) {
-        this.getTargetDiffElement().addDraftAtLine(line);
+      const diff = this.getTargetDiffElement();
+      if (diff && line) {
+        diff.addDraftAtLine(line);
       }
     }
   }
@@ -347,10 +349,12 @@
    * {leftSide: true, number: 321} for line 321 of the base patch.
    * Returns null if an address is not available.
    *
-   * @return {?Object}
+   * @return
    */
   getAddress() {
-    if (!this.diffRow) { return null; }
+    if (!this.diffRow) {
+      return null;
+    }
 
     // Get the line-number cell targeted by the cursor. If the mode is unified
     // then prefer the revision cell if available.
@@ -363,10 +367,14 @@
     } else {
       cell = this.diffRow.querySelector('.lineNum.' + this.side);
     }
-    if (!cell) { return null; }
+    if (!cell) {
+      return null;
+    }
 
     const number = cell.getAttribute('data-value');
-    if (!number || number === 'FILE') { return null; }
+    if (!number || number === 'FILE') {
+      return null;
+    }
 
     return {
       leftSide: cell.matches('.left'),
@@ -386,20 +394,22 @@
     }
   }
 
-  _rowHasSide(row) {
-    const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
-        ' + .content';
+  _rowHasSide(row: Element) {
+    const selector =
+      (this.side === DiffSide.LEFT ? '.left' : '.right') + ' + .content';
     return !!row.querySelector(selector);
   }
 
-  _isFirstRowOfChunk(row) {
-    const parentClassList = row.parentNode.classList;
-    return parentClassList.contains('section') &&
-        parentClassList.contains('delta') &&
-        !row.previousSibling;
+  _isFirstRowOfChunk(row: HTMLElement) {
+    const parentClassList = (row.parentNode as HTMLElement).classList;
+    return (
+      parentClassList.contains('section') &&
+      parentClassList.contains('delta') &&
+      !row.previousSibling
+    );
   }
 
-  _rowHasThread(row) {
+  _rowHasThread(row: HTMLElement) {
     return row.querySelector('.thread-group');
   }
 
@@ -408,10 +418,11 @@
    * switch to the alternate side.
    */
   _fixSide() {
-    if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
-        this._isTargetBlank()) {
-      this.side = this.side === DiffSides.LEFT ?
-        DiffSides.RIGHT : DiffSides.LEFT;
+    if (
+      this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
+      this._isTargetBlank()
+    ) {
+      this.side = this.side === DiffSide.LEFT ? DiffSide.RIGHT : DiffSide.LEFT;
     }
   }
 
@@ -421,45 +432,60 @@
     }
 
     const actions = this._getActionsForRow();
-    return (this.side === DiffSides.LEFT && !actions.left) ||
-        (this.side === DiffSides.RIGHT && !actions.right);
+    return (
+      (this.side === DiffSide.LEFT && !actions.left) ||
+      (this.side === DiffSide.RIGHT && !actions.right)
+    );
   }
 
-  _rowChanged(newRow, oldRow) {
+  _rowChanged(_: HTMLElement, oldRow: HTMLElement) {
     if (oldRow) {
       oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
     }
     this._updateSideClass();
   }
 
+  @observe('side')
   _updateSideClass() {
     if (!this.diffRow) {
       return;
     }
-    this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
-        this.diffRow);
-    this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
-        this.diffRow);
+    this.toggleClass(
+      LEFT_SIDE_CLASS,
+      this.side === DiffSide.LEFT,
+      this.diffRow
+    );
+    this.toggleClass(
+      RIGHT_SIDE_CLASS,
+      this.side === DiffSide.RIGHT,
+      this.diffRow
+    );
   }
 
-  _isActionType(type) {
-    return type !== 'blank' && type !== 'contextControl';
+  _isActionType(type: GrDiffRowType) {
+    return (
+      type !== GrDiffLineType.BLANK && type !== GrDiffGroupType.CONTEXT_CONTROL
+    );
   }
 
   _getActionsForRow() {
     const actions = {left: false, right: false};
     if (this.diffRow) {
       actions.left = this._isActionType(
-          this.diffRow.getAttribute('left-type'));
+        this.diffRow.getAttribute('left-type') as GrDiffRowType
+      );
       actions.right = this._isActionType(
-          this.diffRow.getAttribute('right-type'));
+        this.diffRow.getAttribute('right-type') as GrDiffRowType
+      );
     }
     return actions;
   }
 
   _getStops() {
     return this.diffs.reduce(
-        (stops, diff) => stops.concat(diff.getCursorStops()), []);
+      (stops: HTMLElement[], diff) => stops.concat(diff.getCursorStops()),
+      []
+    );
   }
 
   _updateStops() {
@@ -469,58 +495,83 @@
   /**
    * Setup and tear down on-render listeners for any diffs that are added or
    * removed from the cursor.
-   *
-   * @private
    */
-  _diffsChanged(changeRecord) {
-    if (!changeRecord) { return; }
+  @observe('diffs.splices')
+  _diffsChanged(changeRecord: PolymerSpliceChange<GrDiff[]>) {
+    if (!changeRecord) {
+      return;
+    }
 
     this._updateStops();
 
     let splice;
     let i;
-    for (let spliceIdx = 0;
-      changeRecord.indexSplices &&
-          spliceIdx < changeRecord.indexSplices.length;
-      spliceIdx++) {
+    for (
+      let spliceIdx = 0;
+      changeRecord.indexSplices && spliceIdx < changeRecord.indexSplices.length;
+      spliceIdx++
+    ) {
       splice = changeRecord.indexSplices[spliceIdx];
 
+      // Removals must come before additions, because the gr-diff instances
+      // might be the same.
+      for (i = 0; i < splice?.removed.length; i++) {
+        splice.removed[i].removeEventListener(
+          'render-start',
+          this._boundHandleDiffRenderStart
+        );
+        splice.removed[i].removeEventListener(
+          'render-content',
+          this._boundHandleDiffRenderContent
+        );
+        splice.removed[i].removeEventListener(
+          'line-selected',
+          this._boundHandleDiffLineSelected
+        );
+      }
+
       for (i = splice.index; i < splice.index + splice.addedCount; i++) {
         this.diffs[i].addEventListener(
-            'render-start', this._boundHandleDiffRenderStart);
+          'render-start',
+          this._boundHandleDiffRenderStart
+        );
         this.diffs[i].addEventListener(
-            'render-content', this._boundHandleDiffRenderContent);
+          'render-content',
+          this._boundHandleDiffRenderContent
+        );
         this.diffs[i].addEventListener(
-            'line-selected', this._boundHandleDiffLineSelected);
-      }
-
-      for (i = 0; i < splice.removed && splice.removed.length; i++) {
-        splice.removed[i].removeEventListener(
-            'render-start', this._boundHandleDiffRenderStart);
-        splice.removed[i].removeEventListener(
-            'render-content', this._boundHandleDiffRenderContent);
-        splice.removed[i].removeEventListener(
-            'line-selected', this._boundHandleDiffLineSelected);
+          'line-selected',
+          this._boundHandleDiffLineSelected
+        );
       }
     }
   }
 
-  _findRowByNumberAndFile(targetNumber, side, opt_path) {
+  _findRowByNumberAndFile(
+    targetNumber: number,
+    side: DiffSide,
+    path?: string
+  ): HTMLElement | undefined {
     let stops;
-    if (opt_path) {
-      const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
+    if (path) {
+      const diff = this.diffs.filter(diff => diff.path === path)[0];
       stops = diff.getCursorStops();
     } else {
       stops = this.$.cursorManager.stops;
     }
     let selector;
     for (let i = 0; i < stops.length; i++) {
-      selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
+      selector = `.lineNum.${side}[data-value="${targetNumber}"]`;
       if (stops[i].querySelector(selector)) {
         return stops[i];
       }
     }
+    return undefined;
   }
 }
 
-customElements.define(GrDiffCursor.is, GrDiffCursor);
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-diff-cursor': GrDiffCursor;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 9cffcef..a880320 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -109,7 +109,7 @@
 
   next(
     condition?: Function,
-    getTargetHeight?: Function,
+    getTargetHeight?: (target: HTMLElement) => number,
     clipToTop?: boolean,
     navigateToNextFile?: boolean
   ) {
@@ -272,7 +272,7 @@
   _moveCursor(
     delta: number,
     condition?: Function,
-    getTargetHeight?: Function,
+    getTargetHeight?: (target: HTMLElement) => number,
     clipToTop?: boolean,
     navigateToNextFile?: boolean
   ) {
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 909f719..69a550b 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -34,3 +34,15 @@
    */
   NOT_INSTRUMENTED = 'NOT_INSTRUMENTED',
 }
+
+/**
+ * If Polymer would have exported DomApiNative from its dom.js utility, then we
+ * would probably not need this type. We just use it for casting the return
+ * value of dom(element).
+ */
+export interface PolymerDomWrapper {
+  getOwnerRoot(): Node & OwnerRoot;
+}
+export interface OwnerRoot {
+  host?: HTMLElement;
+}