You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Lit+TypeScript中实现长按颜色缩略图显示上下文菜单

使用Lit和TypeScript实现长按颜色缩略图显示上下文菜单

核心思路

  • 实现长按检测:通过监听mousedown/touchstart启动定时器,mouseup/touchend/mouseleave清除定时器,达到设定阈值(比如500ms)时触发菜单显示
  • 绑定目标元素:记录当前被长按的颜色缩略图DOM,确保菜单仅对应目标显示
  • 状态管理:用Lit响应式属性控制菜单的可见性与关联目标

完整实现代码

1. 颜色缩略图父组件(含长按逻辑)

import { LitElement, html, css, property, state } from 'lit';
import { customElement } from 'lit/decorators.js';

// 假设已有rgbToHex工具函数
function rgbToHex(r: number, g: number, b: number): string {
  return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}

@customElement('color-grid')
export class ColorGrid extends LitElement {
  static styles = css`
    .color-container {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      padding: 16px;
    }
    .color {
      width: 60px;
      height: 60px;
      border-radius: 4px;
      cursor: pointer;
      position: relative;
    }
    context-menu {
      position: absolute;
      display: none;
      background: #fff;
      border: 1px solid #e0e0e0;
      border-radius: 4px;
      padding: 8px 0;
      min-width: 120px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      z-index: 100;
    }
    context-menu.show {
      display: block;
    }
  `;

  // 传入的颜色列表数据
  @property({ type: Array }) colors = [];

  // 响应式状态:记录激活的菜单目标与可见性
  @state() private activeMenuTarget: HTMLElement | null = null;
  @state() private isMenuVisible = false;
  private longPressTimer: number | null = null;
  private readonly LONG_PRESS_THRESHOLD = 500; // 长按触发阈值(毫秒)

  render() {
    return html`
      <div class="color-container">
        ${this.colors.map((color) => html`
          <div
            class="color"
            style="background:${rgbToHex(color.value.r, color.value.g, color.value.b)}"
            @mousedown=${(e: MouseEvent) => this.startLongPress(e, color)}
            @mouseup=${this.cancelLongPress}
            @mouseleave=${this.cancelLongPress}
            @touchstart=${(e: TouchEvent) => this.startLongPress(e, color)}
            @touchend=${this.cancelLongPress}
            @touchcancel=${this.cancelLongPress}
            ref=${(el: HTMLElement) => (color._el = el)}
          >
            <context-menu
              class=${this.activeMenuTarget === color._el && this.isMenuVisible ? 'show' : ''}
              .color=${color}
              @menu-closed=${this.hideMenu}
            ></context-menu>
          </div>
        `)}
      </div>
    `;
  }

  private startLongPress(event: MouseEvent | TouchEvent, color: any) {
    // 阻止浏览器默认右键菜单
    event.preventDefault();
    
    // 清除已有定时器,避免重复触发
    if (this.longPressTimer) clearTimeout(this.longPressTimer);

    this.longPressTimer = window.setTimeout(() => {
      this.activeMenuTarget = color._el;
      this.isMenuVisible = true;
    }, this.LONG_PRESS_THRESHOLD);
  }

  private cancelLongPress() {
    if (this.longPressTimer) {
      clearTimeout(this.longPressTimer);
      this.longPressTimer = null;
    }
  }

  private hideMenu() {
    this.isMenuVisible = false;
    this.activeMenuTarget = null;
  }

  connectedCallback() {
    super.connectedCallback();
    // 监听全局点击,点击菜单外区域隐藏菜单
    document.addEventListener('click', this.handleGlobalClick);
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    document.removeEventListener('click', this.handleGlobalClick);
  }

  private handleGlobalClick = (e: MouseEvent) => {
    const activeMenu = this.shadowRoot?.querySelector('context-menu.show');
    if (activeMenu && !activeMenu.contains(e.target as Node)) {
      this.hideMenu();
    }
  };
}

2. 上下文菜单子组件

import { LitElement, html, css, property } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('context-menu')
export class ContextMenu extends LitElement {
  static styles = css`
    .menu-item {
      padding: 8px 16px;
      cursor: pointer;
      font-size: 14px;
    }
    .menu-item:hover {
      background: #f5f5f5;
    }
  `;

  // 接收父组件传入的当前颜色数据
  @property() color: any = null;

  render() {
    return html`
      <div class="menu-item" @click=${this.copyHex}>复制HEX值</div>
      <div class="menu-item" @click=${this.triggerDelete}>删除颜色</div>
    `;
  }

  private copyHex() {
    const hex = rgbToHex(this.color.value.r, this.color.value.g, this.color.value.b);
    navigator.clipboard.writeText(hex);
    this.closeMenu();
  }

  private triggerDelete() {
    // 触发自定义事件,通知父组件删除对应颜色
    this.dispatchEvent(new CustomEvent('color-delete', {
      detail: this.color,
      bubbles: true,
      composed: true
    }));
    this.closeMenu();
  }

  private closeMenu() {
    this.dispatchEvent(new CustomEvent('menu-closed'));
  }
}

关键细节说明

  • 跨设备兼容:同时处理鼠标和触摸事件,适配桌面与移动端
  • 精准关联:通过给每个颜色元素绑定引用,确保菜单只在被长按的缩略图上显示
  • 交互优化:全局点击隐藏菜单,避免菜单停留在页面上影响操作
  • 组件解耦:子菜单通过自定义事件与父组件通信,保持组件独立性

内容的提问来源于stack exchange,提问作者Rue Vitale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 05:25:26