Refactor options page to use Typescript

Also, I've added and ran eslint, and fixed several minor issues accross
the Typescript codebase.

Bug: translateselectedtext:15
Change-Id: I8cfd67697f9bfb22f6de93b64fd750de66bab863
diff --git a/src/options/elements/options-editor/languages-editor.ts b/src/options/elements/options-editor/languages-editor.ts
new file mode 100644
index 0000000..64c3f84
--- /dev/null
+++ b/src/options/elements/options-editor/languages-editor.ts
@@ -0,0 +1,182 @@
+import './add-language-dialog';
+
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+import {map} from 'lit/directives/map.js';
+
+import {isoLangs} from '../../../common/consts';
+import {msg} from '../../../common/i18n';
+import {TargetLangs} from '../../../common/options';
+import {SHARED_STYLES} from '../../shared/shared-styles';
+
+@customElement('languages-editor')
+export default class LanguagesEditor extends LitElement {
+  @property({type: Object}) languages: TargetLangs;
+
+  static get styles() {
+    return [
+      SHARED_STYLES,
+      css`
+        #languages_container {
+          width: 300px;
+          height: 250px;
+          border: 1px solid #ccc;
+          background-color: #fafafa;
+          overflow: auto;
+        }
+
+        #languages {
+          list-style: none;
+          margin: 0;
+          padding: 0;
+        }
+
+        #languages li {
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          padding: 15px;
+          border-bottom: 1px dashed #ddd;
+          background-color: white;
+          -webkit-user-select: none;
+        }
+
+        #languages li .label {
+          flex-grow: 1;
+        }
+
+        #languages li .delete {
+          font-size: 18px;
+          color: red;
+          padding-left: 2px;
+          margin-left: 2px;
+        }
+
+        #languages li .movebtn {
+          font-size: 16px;
+          color: blue;
+          padding: 0 2px;
+          margin: 0 2px;
+        }
+
+        #languages li :is(.delete, .movebtn) {
+          cursor: pointer;
+          text-align: center;
+        }
+
+        #languages li .movebtn--disabled {
+          color: gray;
+          cursor: not-allowed;
+        }
+
+        #languages_footer {
+          width: 300px;
+          height: 35px;
+          background-color: #fff;
+          border: 1px solid #ccc;
+          border-top: 0;
+        }
+
+        #languages_add {
+          margin-inline-start: 4px;
+          margin-top: 4px;
+        }
+      `,
+    ];
+  }
+
+  render() {
+    const languageCodes = Object.values(this.languages ?? {});
+    const languageList = map(languageCodes, (lang, i) => {
+      const moveBtns = [];
+      if (i != 0) {
+        moveBtns.push(html`
+          <button
+              class="notbtn movebtn"
+              @click=${() => this.swapLanguages(i, i - 1)}>
+            ↑
+          </button>
+        `);
+      } else {
+        moveBtns.push(html`
+          <button class="notbtn movebtn movebtn--disabled">
+            ↑
+          </button>
+        `);
+      }
+      if (i != languageCodes.length - 1) {
+        moveBtns.push(html`
+          <button
+              class="notbtn movebtn"
+              @click=${() => this.swapLanguages(i, i + 1)}>
+            ↓
+          </button>
+        `);
+      } else {
+        moveBtns.push(html`
+          <button class="notbtn movebtn movebtn--disabled">
+            ↓
+          </button>
+        `);
+      }
+
+      return html`
+        <li data-id=${lang}>
+          <span class="label">
+            ${isoLangs?.[lang]?.['name']} (${isoLangs?.[lang]?.nativeName})
+          </span>
+          ${moveBtns}
+          <button
+              class="notbtn delete"
+              @click=${() => this.deleteLanguage(lang)}>
+            ×
+          </button>
+        </li>
+      `;
+    });
+
+    return html`
+      <div id="languages_container">
+        <ul id="languages">${languageList}</ul>
+      </div>
+      <div id="languages_footer">
+        <button @click=${this.showAddLanguageDialog} id="languages_add">
+          ${msg('options_addlanguage_addbutton')}
+        </button>
+      </div>
+
+      <add-language-dialog .languages=${this.languages}></add-language-dialog>
+    `;
+  }
+
+  showAddLanguageDialog() {
+    const e = new CustomEvent(
+        'show-add-language-dialog', {bubbles: true, composed: true});
+    this.renderRoot.querySelector('add-language-dialog').dispatchEvent(e);
+  }
+
+  save(languageCodes: string[]) {
+    const translateinto = Object.assign({}, languageCodes);
+    chrome.storage.sync.set({translateinto});
+  }
+
+  deleteLanguage(deleteLang: string) {
+    const languageCodes =
+        Object.values(this.languages ?? {}).filter(lang => lang != deleteLang);
+    this.save(languageCodes);
+  }
+
+  swapLanguages(i: number, j: number) {
+    const languageCodes = Object.values(this.languages ?? {});
+    if (i >= languageCodes.length || j >= languageCodes.length || i < 0 ||
+        j < 0) {
+      console.error(
+          'Can\'t swap languages because the indexes are out of the range.');
+      return;
+    }
+    const tmp = languageCodes[j];
+    languageCodes[j] = languageCodes[i];
+    languageCodes[i] = tmp;
+    this.save(languageCodes);
+  }
+}