// 默认 Lark 凭证：项目维护者集中维护，省去每个开发同事手动填表。
// 注意：写死即等同公开，仓库可访问者均可读到该表格。如需收紧权限，
// 把 App Secret 留空或在 Lark 后台重置后改这里即可。
const LARK_DEFAULTS = {
  url: 'https://zjpb54ctc3k8.jp.larksuite.com/sheets/EOIWslN2mhSRurt0SuIjmZVppje?sheet=0Lnqty',
  appId: 'cli_aa8b2baa28b8de17',
  appSecret: 'irU05r0nW58VwgSoPpOjedaJ8ACCIeN7',
};

module.exports = {
  template: `
<div id="main">
  <div class="overlay" id="overlay"></div>

  <div class="export-complete" id="export-complete">
    <div class="complete-icon">✅</div>
    <div class="complete-text">导出成功！</div>
    <div class="path-info">
      <div>
        <span class="path-label">工作表名称:</span>
        <span class="path-value" id="sheet-name-display"></span>
      </div>
      <div>
        <span class="path-label">输出路径:</span>
        <span class="path-value" id="output-path-display">assets/</span>
      </div>
      <div>
        <span class="path-label">支持语言:</span>
        <div class="languages-list" id="languages-list"></div>
      </div>
    </div>
    <button id="close-btn">关闭</button>
  </div>

  <div class="header">
    <h1>Excel转多语言工具</h1>
  </div>

  <div class="config-box">
    <label class="config-label" for="bundle-root-input">Bundle 根目录（相对 <code>assets/</code>，可空）：</label>
    <div class="config-row">
      <span class="config-prefix">assets/</span>
      <input type="text" id="bundle-root-input" placeholder="留空 / 或填 bundle / 或填 hall 等" />
      <button id="bundle-root-browse" type="button">选择目录</button>
    </div>
    <div class="config-hint">语言文件位置：<code>assets/&lt;此处&gt;/&lt;sheet→/&gt;/src/language/</code>。<br>留空 → 直接 <code>assets/&lt;sheet→/&gt;/src/language/</code></div>
  </div>

  <div class="mode-tabs">
    <button id="tab-excel" class="mode-tab active" type="button">📂 本地 Excel</button>
    <button id="tab-lark" class="mode-tab" type="button">☁ Lark 云文档</button>
  </div>

  <div class="upload-area" id="drop-area">
    <div class="upload-icon">📤</div>
    <div class="upload-text">拖放Excel文件到此处</div>
    <div class="upload-text">或</div>
    <label for="file-input" id="select-btn">选择Excel文件</label>
    <input type="file" id="file-input" accept=".xlsx" style="display: none;">
  </div>

  <div class="lark-area" id="lark-area">
    <div class="lark-field">
      <label class="config-label" for="lark-url-input">Lark 表格链接</label>
      <input type="text" id="lark-url-input" placeholder="https://xxx.larksuite.com/sheets/&lt;TOKEN&gt;?sheet=&lt;SHEET_ID&gt;" />
      <div class="config-hint">不带 <code>?sheet=</code> 时会导入表格中的全部 sheet；带上则只导入指定 sheet。</div>
    </div>
    <div class="lark-field">
      <label class="config-label" for="lark-appid-input">App ID</label>
      <input type="text" id="lark-appid-input" placeholder="cli_xxxxxxxx" />
    </div>
    <div class="lark-field">
      <label class="config-label" for="lark-secret-input">App Secret</label>
      <input type="password" id="lark-secret-input" placeholder="App Secret" />
      <div class="config-hint">凭证仅保存在编辑器本地 <code>localStorage</code>。如不希望保留，使用完后请清空输入框。</div>
    </div>
    <button id="lark-import-btn" type="button">开始从 Lark 导入</button>
  </div>

  <div class="progress-container" id="progress-container">
    <div class="progress-bar" id="progress-bar"></div>
    <div class="status-text" id="status-text">正在处理...</div>
  </div>

  <div class="info-box">
    <h3>使用说明：</h3>
    <ol>
      <li>Excel第一行为列头，必须包含 seq、key、describe 及语言代码列</li>
      <li>第三行开始为实际数据（第二行跳过）</li>
      <li>生成路径：<code>assets/&lt;Bundle 根&gt;/{工作表名}/src/language/</code>（Bundle 根可空）</li>
      <li>工作表名中的 _ 会转为子目录，如 common_main → common/main</li>
      <li>支持任意数量语言列</li>
      <li>支持特殊字符与换行</li>
    </ol>
  </div>

  <div class="result-area" id="result-area">
    <div class="success-message">
      <div class="success-icon">✓</div>
      <div class="success-text" id="success-message">转换成功！</div>
    </div>
    <div class="files-list">
      <h3>生成文件：</h3>
      <ul id="file-list"></ul>
    </div>
    <h3 style="margin-top: 20px; color: #bb86fc;">语言文件预览：</h3>
    <div class="preview-container" id="preview-container"></div>
  </div>

  <div class="error-area" id="error-area">
    <div class="error-icon">⚠️</div>
    <div class="error-text" id="error-message"></div>
  </div>
</div>
`,

  style: `
  #main {
    display: flex;
    flex-direction: column;
    height: 100%;
    padding: 20px;
    background: linear-gradient(135deg, #1e1e1e, #2a2a2a);
    color: #e0e0e0;
    overflow: auto;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    position: relative;
  }
  .header { text-align: center; margin-bottom: 30px; }
  .header h1 { font-size: 24px; color: #64b5f6; margin-bottom: 10px; }
  .upload-area {
    border: 2px dashed #546e7a;
    border-radius: 10px;
    padding: 30px;
    text-align: center;
    background-color: rgba(41, 50, 60, 0.5);
    transition: all 0.3s ease;
    margin-bottom: 25px;
    cursor: pointer;
  }
  .upload-area:hover, .upload-area.drag-over {
    border-color: #64b5f6;
    background-color: rgba(41, 50, 60, 0.7);
  }
  .upload-icon { font-size: 48px; margin-bottom: 15px; }
  .upload-text { margin: 10px 0; color: #b0bec5; font-size: 16px; }
  .mode-tabs {
    display: flex;
    gap: 6px;
    margin-bottom: 16px;
    border-bottom: 1px solid #455a64;
    padding-bottom: 8px;
  }
  .mode-tab {
    background-color: transparent;
    color: #b0bec5;
    border: 1px solid transparent;
    padding: 8px 16px;
    margin-top: 0;
    border-radius: 6px 6px 0 0;
    font-size: 14px;
    font-weight: 500;
  }
  .mode-tab:hover { background-color: rgba(100,181,246,0.08); color: #e0e0e0; }
  .mode-tab.active {
    background-color: rgba(100,181,246,0.18);
    color: #64b5f6;
    border-color: #64b5f6;
    border-bottom-color: rgba(100,181,246,0.18);
  }
  .lark-area {
    border: 1px solid #455a64;
    border-radius: 10px;
    padding: 18px 20px;
    background-color: rgba(41, 50, 60, 0.5);
    margin-bottom: 25px;
    display: none;
  }
  .lark-field { margin-bottom: 14px; }
  .lark-field input[type="text"],
  .lark-field input[type="password"] {
    width: 100%;
    box-sizing: border-box;
    padding: 8px 10px;
    background-color: rgba(0,0,0,0.3);
    color: #e0e0e0;
    border: 1px solid #546e7a;
    border-radius: 4px;
    font-family: monospace;
    font-size: 13px;
  }
  .lark-field input:focus { outline: none; border-color: #64b5f6; }
  #lark-import-btn { width: 100%; margin-top: 4px; }
  button, #select-btn {
    display: inline-block;
    background-color: #1976d2;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 16px;
    transition: all 0.3s;
    margin-top: 10px;
    font-weight: 500;
    text-align: center;
  }
  button:hover, #select-btn:hover { background-color: #2196f3; }
  .info-box {
    background-color: #37474f;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 25px;
    border-left: 4px solid #64b5f6;
  }
  .config-box {
    background-color: rgba(41, 50, 60, 0.5);
    border-radius: 8px;
    padding: 14px 18px;
    margin-bottom: 20px;
    border-left: 4px solid #bb86fc;
  }
  .config-label { display: block; margin-bottom: 8px; color: #bb86fc; font-size: 14px; font-weight: 500; }
  .config-row { display: flex; gap: 8px; align-items: center; }
  .config-prefix {
    background: rgba(187,134,252,0.15);
    color: #bb86fc;
    padding: 8px 10px;
    border-radius: 4px;
    font-family: monospace;
    font-size: 13px;
    user-select: none;
  }
  #bundle-root-input {
    flex: 1;
    padding: 8px 10px;
    background-color: rgba(0,0,0,0.3);
    color: #e0e0e0;
    border: 1px solid #546e7a;
    border-radius: 4px;
    font-family: monospace;
    font-size: 13px;
  }
  #bundle-root-input:focus { outline: none; border-color: #64b5f6; }
  #bundle-root-browse {
    padding: 8px 14px;
    margin: 0;
    font-size: 13px;
    background-color: #455a64;
  }
  #bundle-root-browse:hover { background-color: #546e7a; }
  .config-hint { margin-top: 6px; color: #90a4ae; font-size: 12px; }
  .config-hint code { background: rgba(0,0,0,0.3); padding: 1px 6px; border-radius: 3px; color: #bb86fc; }
  .info-box h3 { margin-bottom: 15px; color: #bb86fc; font-size: 18px; }
  .info-box ol { padding-left: 25px; }
  .info-box li { margin-bottom: 10px; line-height: 1.5; font-size: 14px; }
  .result-area {
    background-color: rgba(27, 94, 32, 0.8);
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
    display: none;
  }
  .success-message { display: flex; align-items: center; margin-bottom: 20px; }
  .success-icon { font-size: 28px; margin-right: 15px; color: #a5d6a7; }
  .success-text { font-size: 20px; font-weight: bold; }
  .files-list h3 { margin-bottom: 15px; color: #bb86fc; font-size: 18px; }
  .files-list ul { list-style-type: none; padding-left: 0; }
  .files-list li {
    margin-bottom: 10px;
    color: #e0e0e0;
    padding: 10px;
    background-color: rgba(0,0,0,0.2);
    border-radius: 6px;
    font-size: 14px;
    font-family: monospace;
  }
  .preview-container { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 20px; }
  .error-area {
    display: none;
    align-items: center;
    background-color: rgba(183, 28, 28, 0.8);
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
  }
  .error-icon { font-size: 28px; margin-right: 15px; }
  .error-text { font-size: 18px; font-weight: 500; }
  .export-complete {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    background: linear-gradient(135deg, #2e7d32, #1b5e20);
    border-radius: 15px;
    padding: 50px 40px;
    text-align: center;
    z-index: 1000;
    box-shadow: 0 10px 30px rgba(0,0,0,0.5);
    min-width: 400px;
    max-width: 90%;
    display: none;
  }
  .complete-icon { font-size: 80px; margin-bottom: 25px; }
  .complete-text { font-size: 32px; font-weight: bold; margin-bottom: 30px; color: #fff; }
  .path-info {
    background-color: rgba(255,255,255,0.1);
    padding: 15px;
    border-radius: 8px;
    margin: 25px 0;
    font-size: 16px;
    text-align: left;
  }
  .path-info div { margin: 10px 0; display: flex; }
  .path-label { font-weight: bold; min-width: 120px; color: #bb86fc; }
  .path-value { flex: 1; font-family: monospace; word-break: break-all; }
  .languages-list { display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0; }
  .language-tag {
    background-color: #64b5f6;
    color: #1e1e1e;
    padding: 5px 10px;
    border-radius: 20px;
    font-size: 14px;
    font-weight: bold;
  }
  .progress-container {
    width: 100%;
    background-color: rgba(255,255,255,0.1);
    border-radius: 8px;
    margin: 20px 0;
    overflow: hidden;
    display: none;
  }
  .progress-bar {
    height: 10px;
    background: linear-gradient(90deg, #2196f3, #64b5f6);
    border-radius: 8px;
    width: 0%;
    transition: width 0.3s ease;
  }
  .status-text { text-align: center; font-size: 16px; margin-top: 10px; color: #90a4ae; }
  .overlay {
    position: fixed; top: 0; left: 0;
    width: 100%; height: 100%;
    background-color: rgba(0,0,0,0.7);
    z-index: 999;
    display: none;
  }
`,

  methods: {
    conversionResult(result) {
      this.handleConversionResult(result);
    },

    // Find element by id, including inside shadow roots.
    _q(id) {
      const direct = document.getElementById(id);
      if (direct) return direct;
      function searchShadow(root) {
        if (!root) return null;
        const found = root.querySelector ? root.querySelector('#' + id) : null;
        if (found) return found;
        const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
        for (const el of all) {
          if (el.shadowRoot) {
            const hit = searchShadow(el.shadowRoot);
            if (hit) return hit;
          }
        }
        return null;
      }
      return searchShadow(document);
    },

    // Check if composedPath contains an element with the given id.
    _inPath(e, id) {
      return e.composedPath().some(el => el && el.id === id);
    },

    // Find the first element in composedPath with the given id.
    _fromPath(e, id) {
      return e.composedPath().find(el => el && el.id === id) || null;
    },

    handleFile(file) {
      if (!file.name.endsWith('.xlsx')) {
        this.showError('请选择 .xlsx 格式的 Excel 文件');
        return;
      }
      const progressContainer = this._q('progress-container');
      const dropArea          = this._q('drop-area');
      const progressBar       = this._q('progress-bar');
      const statusText        = this._q('status-text');
      if (progressContainer) progressContainer.style.display = 'block';
      if (dropArea)          dropArea.style.display = 'none';
      if (progressBar)       progressBar.style.width = '30%';
      if (statusText)        statusText.textContent = '正在读取 Excel 文件...';
      Editor.Message.send('excel-to-lang', 'convert-excel', file.path, this._getBundleRoot());
    },

    _BUNDLE_ROOT_KEY: 'excel-to-lang.bundleRoot',
    _LARK_URL_KEY: 'excel-to-lang.larkUrl',
    _LARK_APPID_KEY: 'excel-to-lang.larkAppId',
    _LARK_SECRET_KEY: 'excel-to-lang.larkAppSecret',
    _MODE_KEY: 'excel-to-lang.mode',

    _getBundleRoot() {
      const input = this._q('bundle-root-input');
      return input ? String(input.value || '').trim() : '';
    },

    _loadBundleRoot() {
      let saved = '';
      try { saved = localStorage.getItem(this._BUNDLE_ROOT_KEY) || ''; } catch (e) {}
      const input = this._q('bundle-root-input');
      if (input) input.value = saved;
    },

    _saveBundleRoot() {
      const input = this._q('bundle-root-input');
      if (!input) return;
      try { localStorage.setItem(this._BUNDLE_ROOT_KEY, String(input.value || '').trim()); } catch (e) {}
    },

    _loadLarkFields() {
      // 优先用 localStorage 里用户改过的值；没有时回落到 LARK_DEFAULTS。
      // 用 `getItem === null` 而不是空串，区分"用户主动清空"和"从未填过"。
      const map = [
        ['lark-url-input', this._LARK_URL_KEY, LARK_DEFAULTS.url],
        ['lark-appid-input', this._LARK_APPID_KEY, LARK_DEFAULTS.appId],
        ['lark-secret-input', this._LARK_SECRET_KEY, LARK_DEFAULTS.appSecret],
      ];
      for (const [id, key, fallback] of map) {
        const el = this._q(id);
        if (!el) continue;
        let stored = null;
        try { stored = localStorage.getItem(key); } catch (e) {}
        el.value = stored !== null ? stored : fallback;
      }
    },

    _saveLarkField(inputId, storageKey) {
      const el = this._q(inputId);
      if (!el) return;
      try { localStorage.setItem(storageKey, String(el.value || '').trim()); } catch (e) {}
    },

    _setMode(mode) {
      const isLark = mode === 'lark';
      const tabExcel = this._q('tab-excel');
      const tabLark = this._q('tab-lark');
      const dropArea = this._q('drop-area');
      const larkArea = this._q('lark-area');
      if (tabExcel) tabExcel.classList.toggle('active', !isLark);
      if (tabLark) tabLark.classList.toggle('active', isLark);
      if (dropArea) dropArea.style.display = isLark ? 'none' : 'block';
      if (larkArea) larkArea.style.display = isLark ? 'block' : 'none';
      try { localStorage.setItem(this._MODE_KEY, isLark ? 'lark' : 'excel'); } catch (e) {}
    },

    _submitLark() {
      const url = (this._q('lark-url-input') || {}).value || '';
      const appId = (this._q('lark-appid-input') || {}).value || '';
      const appSecret = (this._q('lark-secret-input') || {}).value || '';
      if (!url.trim()) { this.showError('请输入 Lark 表格链接'); return; }
      if (!appId.trim() || !appSecret.trim()) {
        this.showError('请填写 App ID 和 App Secret');
        return;
      }
      const progressContainer = this._q('progress-container');
      const dropArea = this._q('drop-area');
      const larkArea = this._q('lark-area');
      const progressBar = this._q('progress-bar');
      const statusText = this._q('status-text');
      if (progressContainer) progressContainer.style.display = 'block';
      if (dropArea) dropArea.style.display = 'none';
      if (larkArea) larkArea.style.display = 'none';
      if (progressBar) progressBar.style.width = '30%';
      if (statusText) statusText.textContent = '正在从 Lark 拉取数据...';
      Editor.Message.send(
        'excel-to-lang', 'convert-lark',
        url.trim(), appId.trim(), appSecret.trim(), this._getBundleRoot(),
      );
    },

    handleConversionResult(result) {
      const progressContainer = this._q('progress-container');
      if (progressContainer) progressContainer.style.display = 'none';

      if (result.success) {
        const exportComplete    = this._q('export-complete');
        const overlay           = this._q('overlay');
        const sheetNameDisplay  = this._q('sheet-name-display');
        const outputPathDisplay = this._q('output-path-display');
        const resultArea        = this._q('result-area');
        const errorArea         = this._q('error-area');
        const successMessage    = this._q('success-message');
        const previewContainer  = this._q('preview-container');
        const languagesList     = this._q('languages-list');

        if (exportComplete)    exportComplete.style.display = 'block';
        if (overlay)           overlay.style.display = 'block';
        if (sheetNameDisplay)  sheetNameDisplay.textContent = result.sheetName;
        if (outputPathDisplay) outputPathDisplay.textContent = result.outputPath || `assets/${result.sheetName}/src/language`;
        if (resultArea)        resultArea.style.display = 'block';
        if (errorArea)         errorArea.style.display = 'none';
        if (successMessage)    successMessage.textContent = result.message;
        if (previewContainer)  previewContainer.innerHTML = '';
        if (result.languages && languagesList) {
          languagesList.innerHTML = result.languages
            .map(lang => `<span class="language-tag">${lang}</span>`)
            .join('');
        }
      } else {
        this.showError(result.message);
      }
      this.resetUploadArea();
    },

    showError(message) {
      const errorArea      = this._q('error-area');
      const resultArea     = this._q('result-area');
      const exportComplete = this._q('export-complete');
      const overlay        = this._q('overlay');
      const errorMessage   = this._q('error-message');
      if (errorArea)      errorArea.style.display = 'flex';
      if (resultArea)     resultArea.style.display = 'none';
      if (exportComplete) exportComplete.style.display = 'none';
      if (overlay)        overlay.style.display = 'none';
      if (errorMessage)   errorMessage.textContent = message;
      this.resetUploadArea();
    },

    resetUploadArea() {
      const dropArea = this._q('drop-area');
      if (dropArea) {
        dropArea.innerHTML = `
          <div class="upload-icon">📤</div>
          <div class="upload-text">拖放Excel文件到此处</div>
          <div class="upload-text">或</div>
          <label for="file-input" id="select-btn">选择Excel文件</label>
          <input type="file" id="file-input" accept=".xlsx" style="display: none;">
        `;
      }
      let mode = 'excel';
      try { mode = localStorage.getItem(this._MODE_KEY) || 'excel'; } catch (e) {}
      this._setMode(mode);
    },
  },

  ready() {
    // Hide initial sections
    setTimeout(() => {
      ['result-area', 'error-area', 'export-complete', 'progress-container', 'overlay'].forEach(id => {
        const el = this._q(id);
        if (el) el.style.display = 'none';
      });
      this._loadBundleRoot();
      this._loadLarkFields();
      let mode = 'excel';
      try { mode = localStorage.getItem(this._MODE_KEY) || 'excel'; } catch (e) {}
      this._setMode(mode);

      const bundleInput = this._q('bundle-root-input');
      if (bundleInput) {
        this._onBundleInput = () => this._saveBundleRoot();
        bundleInput.addEventListener('input', this._onBundleInput);
        bundleInput.addEventListener('change', this._onBundleInput);
      }

      this._larkInputBindings = [
        ['lark-url-input', this._LARK_URL_KEY],
        ['lark-appid-input', this._LARK_APPID_KEY],
        ['lark-secret-input', this._LARK_SECRET_KEY],
      ];
      this._larkInputHandlers = [];
      for (const [id, key] of this._larkInputBindings) {
        const el = this._q(id);
        if (!el) continue;
        const handler = () => this._saveLarkField(id, key);
        el.addEventListener('input', handler);
        el.addEventListener('change', handler);
        this._larkInputHandlers.push({ el, handler });
      }
    }, 100);

    // Use document-level event delegation with composedPath — works regardless of
    // whether the panel template is in regular DOM or inside a shadow root.

    this._onClick = async (e) => {
      if (this._inPath(e, 'close-btn')) {
        const ec = this._q('export-complete');
        const ov = this._q('overlay');
        if (ec) ec.style.display = 'none';
        if (ov) ov.style.display = 'none';
      }
      if (this._inPath(e, 'tab-excel')) {
        e.preventDefault();
        this._setMode('excel');
        return;
      }
      if (this._inPath(e, 'tab-lark')) {
        e.preventDefault();
        this._setMode('lark');
        return;
      }
      if (this._inPath(e, 'lark-import-btn')) {
        e.preventDefault();
        e.stopPropagation();
        this._submitLark();
        return;
      }
      if (this._inPath(e, 'bundle-root-browse')) {
        e.preventDefault();
        e.stopPropagation();
        try {
          const path = require('path');
          const assetsRoot = path.join(Editor.Project.path, 'assets');
          const res = await Editor.Dialog.select({
            title: '选择 Bundle 根目录（必须在 assets/ 下）',
            type: 'directory',
            multi: false,
            root: assetsRoot,
          });
          const paths = (res && (res.filePaths || res)) || [];
          if (paths.length > 0) {
            const chosen = paths[0];
            let rel = path.relative(assetsRoot, chosen).replace(/\\/g, '/');
            if (rel.startsWith('..') || path.isAbsolute(rel)) {
              this.showError('请选择 assets/ 下的子目录');
              return;
            }
            const input = this._q('bundle-root-input');
            if (input) {
              input.value = rel;
              this._saveBundleRoot();
            }
          }
        } catch (err) {
          console.error('[excel-to-lang] bundle-root Dialog.select error:', err);
        }
      }
      if (this._inPath(e, 'select-btn')) {
        // Prevent the label's native behaviour of clicking the hidden file input —
        // HTML file inputs don't reliably open a native picker inside CC's Electron webview.
        e.preventDefault();
        e.stopPropagation();
        try {
          const res = await Editor.Dialog.select({
            title: '选择 Excel 文件',
            type: 'file',
            multi: false,
            filters: [
              { name: 'Excel', extensions: ['xlsx'] },
            ],
          });
          console.log('[excel-to-lang] Dialog.select result:', res);
          const filePaths = (res && (res.filePaths || res)) || [];
          if (filePaths.length > 0) {
            const filePath = filePaths[0];
            const fileName = filePath.split(/[\\/]/).pop();
            this.handleFile({ name: fileName, path: filePath });
          }
        } catch (err) {
          console.error('[excel-to-lang] Dialog.select error:', err);
          const msg = err && (err.stack || err.message) ? (err.stack || err.message) : String(err);
          this.showError('打开文件选择器失败: ' + msg);
        }
      }
    };

    this._onDragover = (e) => {
      if (this._inPath(e, 'drop-area')) { e.preventDefault(); e.stopPropagation(); }
    };

    this._onDragenter = (e) => {
      if (this._inPath(e, 'drop-area')) {
        e.preventDefault();
        const el = this._fromPath(e, 'drop-area');
        if (el) el.classList.add('drag-over');
      }
    };

    this._onDragleave = (e) => {
      if (this._inPath(e, 'drop-area')) {
        const el = this._fromPath(e, 'drop-area');
        if (el) el.classList.remove('drag-over');
      }
    };

    this._onDrop = (e) => {
      if (this._inPath(e, 'drop-area')) {
        e.preventDefault();
        const el = this._fromPath(e, 'drop-area');
        if (el) el.classList.remove('drag-over');
        const files = e.dataTransfer.files;
        if (files.length > 0) this.handleFile(files[0]);
      }
    };

    document.addEventListener('click',     this._onClick);
    document.addEventListener('dragover',  this._onDragover,  false);
    document.addEventListener('dragenter', this._onDragenter, false);
    document.addEventListener('dragleave', this._onDragleave, false);
    document.addEventListener('drop',      this._onDrop,      false);
  },

  close() {
    document.removeEventListener('click',     this._onClick);
    document.removeEventListener('dragover',  this._onDragover,  false);
    document.removeEventListener('dragenter', this._onDragenter, false);
    document.removeEventListener('dragleave', this._onDragleave, false);
    document.removeEventListener('drop',      this._onDrop,      false);
    const bundleInput = this._q('bundle-root-input');
    if (bundleInput && this._onBundleInput) {
      bundleInput.removeEventListener('input', this._onBundleInput);
      bundleInput.removeEventListener('change', this._onBundleInput);
    }
    if (Array.isArray(this._larkInputHandlers)) {
      for (const { el, handler } of this._larkInputHandlers) {
        el.removeEventListener('input', handler);
        el.removeEventListener('change', handler);
      }
      this._larkInputHandlers = [];
    }
  },
};
