Refactor options page using Lit

The Javascript code for the options page was messy and decreased the
maintainability of the entire extension. This change refactors the
existing code to use Lit, and thus separates the code into several files
which define different elements of the options page. Thus, the code is
more modular now.

Aside from the refactoring, this CL also changes the way languages are
reordered. Now, instead of dragging them in the list, they are reordered
by clicking the arrow buttons next to them. This has been changed
because the Sortable library is not compatible with Lit.

Bug: translateselectedtext:11
Change-Id: I07364628c0737cf732afc7dd53846e42b082201d
diff --git a/src/options/elements/options-editor/languages-editor.js b/src/options/elements/options-editor/languages-editor.js
new file mode 100644
index 0000000..7845a65
--- /dev/null
+++ b/src/options/elements/options-editor/languages-editor.js
@@ -0,0 +1,188 @@
+import {css, html, LitElement} from 'lit';
+import {map} from 'lit/directives/map.js';
+
+import {isoLangs} from '../../../common/consts.js';
+import {msg} from '../../../common/i18n.js';
+import {SHARED_STYLES} from '../../shared/shared-styles.js';
+
+import AddLanguageDialog from './add-language-dialog.js';
+
+export class LanguagesEditor extends LitElement {
+  static properties = {
+    languages: {type: Object},
+  };
+
+  static get styles() {
+    return [
+      SHARED_STYLES,
+      css`
+        #languages_container {
+          width: 300px;
+          height: 250px;
+          border: 1px solid #ccc;
+          background-color: #E3F2FD;
+          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: #EEF7FD;
+          -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;
+        }
+      `,
+    ];
+  }
+
+  constructor() {
+    super();
+    this.addEventListener('show-credits-dialog', this.showDialog);
+    this.sortable = undefined;
+  }
+
+  render() {
+    let languageCodes = Object.values(this.languages ?? {});
+    let languageList = map(languageCodes, (lang, i) => {
+      let 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) {
+    let translateinto = Object.assign({}, languageCodes);
+    chrome.storage.sync.set({translateinto});
+  }
+
+  deleteLanguage(deleteLang) {
+    let languageCodes =
+        Object.values(this.languages ?? {}).filter(lang => lang != deleteLang);
+    this.save(languageCodes);
+  }
+
+  swapLanguages(i, j) {
+    let 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;
+    }
+    let tmp = languageCodes[j];
+    languageCodes[j] = languageCodes[i];
+    languageCodes[i] = tmp;
+    this.save(languageCodes);
+  }
+}
+customElements.define('languages-editor', LanguagesEditor);