4 điểm bởi GN⁺ 2023-09-30 | 1 bình luận | Chia sẻ qua WhatsApp
  • Các sơ đồ tương tác của Red Blob Games đòi hỏi màn hình phải phản hồi ngay khi người đọc tự di chuyển đối tượng, nên phần xử lý đầu vào được tổ chức xoay quanh Pointer Events để xử lý chung cả chuột lẫn cảm ứng
  • Cách cũ dùng mousedown/mouseup/mousemove có thể khiến trạng thái kéo bị rối khi con trỏ ra ngoài đối tượng hoặc thả nút ở bên ngoài, nên cần các biện pháp bổ sung như handler toàn cục
  • Đầu vào cảm ứng có cách truyền sự kiện khác với chuột, và thao tác kéo bằng một ngón còn có thể làm trang cuộn, nên gọi preventDefault() trong touchstart để giảm xung đột
  • Công thức cuối cùng kết hợp pointerdown, pointermove, pointerup, pointercancel với setPointerCapture(), đồng thời kiểm tra nút trái và offset ban đầu để đối tượng không bị nhảy đột ngột
  • Các ngoại lệ theo từng nền tảng như đối tượng chứa văn bản/hình ảnh, đa chạm, kéo lồng nhau, <canvas>, kết hợp hover và drag, hay lostpointercapture đều cần được xử lý riêng trong UI thực tế

Mục tiêu của trình xử lý đầu vào kéo

  • Nhiều trang tương tác có đối tượng có thể kéo để người đọc tự di chuyển, và sơ đồ cần phản ứng theo sự thay đổi vị trí của đối tượng
  • Mục tiêu là tạo một trình xử lý sự kiện đầu vào hoạt động trên cả chuột lẫn cảm ứng
  • Luồng cơ bản là bắt đầu kéo ở pointerdown, cập nhật vị trí ở pointermove, và kết thúc kéo ở pointerup hoặc sự kiện hủy
  • Trong triển khai thực tế, mọi thứ phức tạp hơn mô hình đơn giản vì nút chuột, chạm nhiều ngón, đối tượng nhận sự kiện, và menu ngữ cảnh khi nhấp phải
  • Công thức cơ bản kết hợp các thành phần sau
    • pointerdown, nếu event.button !== 0 thì return để chỉ xử lý nút chính
    • Chuyển đổi tọa độ sự kiện sang tọa độ sơ đồ
    • Lưu dx, dy, tức chênh lệch giữa vị trí đối tượng và vị trí con trỏ tại thời điểm bắt đầu
    • Bắt con trỏ bằng el.setPointerCapture(event.pointerId)
    • pointermove, cộng offset đã lưu để cập nhật state.pos
    • pointerup, pointercancel, đặt state.dragging = null
    • Gọi preventDefault() trong touchstart
  • Công thức này là trình xử lý sự kiện đầu vào; phần xuất/hiển thị thực tế cần được nối với một trình xử lý trạng thái riêng
  • Mã hoàn chỉnh có tại examples page, bao gồm cả ví dụ kéo trái/phải với số thay vì di chuyển đối tượng, vẽ trên canvas, và kéo có giới hạn
  • Phạm vi kiểm thử là Gecko/Firefox, Blink/Chrome, WebKit/Safari trên Mac, Windows, Linux, Android, iPhone và iPad
  • Chưa kiểm thử stylus có hover, thiết bị lai cảm ứng+chuột, hay đầu vào bằng giọng nói
  • Cách này không phải HTML Drag and Drop API dùng để đặt phần tử lên phần tử khác; với sơ đồ, cần đọc trực tiếp sự kiện chuột/cảm ứng

Giới hạn khi chỉ dùng sự kiện chuột

  • Bản triển khai ban đầu là gắn trực tiếp mousedown, mouseup, mousemove vào phần tử được kéo
  • Nhìn qua thì có vẻ hoạt động, nhưng phát sinh hai vấn đề
    • Nếu di chuyển con trỏ quá nhanh, nó không còn nằm trên hình tròn nên không nhận được sự kiện nữa
    • Nếu thả nút ở ngoài hình tròn, có thể bị kẹt trong trạng thái dragging
  • Cách cải thiện là nhận mousedown trên hình tròn, còn trong lúc kéo thì tạm thời gắn mousemovemouseup vào document
  • Nếu ở mouseup gỡ các handler mousemove, mouseup đã gắn vào document, thao tác kéo bằng chuột sẽ ổn định hơn
  • Cách này chỉ xử lý chuột, không xử lý sự kiện cảm ứng

Sự khác biệt của sự kiện cảm ứng

  • Đầu vào cảm ứng dùng touchstart, touchend, touchmove thay cho mousedown, mouseup, mousemove
  • Sau touchstart, sự kiện cảm ứng tự động được capture, nên mọi touchmove sẽ được gửi về phần tử ban đầu
  • Vì đặc tính này, không cần tạm thời gắn handler sự kiện toàn cục lên document như với sự kiện chuột
  • Khi trình duyệt cần hủy chuỗi sự kiện cảm ứng, nó sẽ gửi touchcancel
  • Cách triển khai kéo bằng cảm ứng là gọi preventDefault() trong touchstart, rồi cập nhật vị trí bằng tọa độ event.changedTouches[0] trong touchmove
  • Cách này chỉ xử lý cảm ứng, không xử lý sự kiện chuột

Hợp nhất chuột và cảm ứng bằng Pointer Events

  • Để xử lý cả chuột lẫn cảm ứng trước đây cần rất nhiều handler sự kiện, và trước năm 2021 tác giả đã dùng cách đó
  • Trong giai đoạn 2011~2014, tác giả dùng d3-drag từ dự án d3, còn với các dự án không dùng d3 thì tự viết mã riêng cho chuột + cảm ứng
  • Pointer Events hợp nhất và đơn giản hóa việc xử lý chuột và cảm ứng
    • MS IE thêm hỗ trợ vào năm 2012
    • Chrome thêm hỗ trợ năm 2017, Firefox năm 2018, Safari năm 2020
  • Hành vi trình duyệt đã thay đổi theo thời gian, và vào năm 2017 Chrome chuyển một số sự kiện sang passive mode mặc định, gây ra vấn đề trang bị cuộn khi cố kéo đối tượng
  • Safari và Firefox cũng áp dụng thay đổi tương tự vào năm 2018
  • Dùng pointer capture cho phép xử lý bằng logic đơn giản hơn mà không cần thêm/gỡ handler toàn cục trên document như với sự kiện chuột

Duy trì theo dõi con trỏ bằng Pointer Capture

  • Pointer capture cho phép tiếp tục theo dõi ngay cả khi con trỏ đi ra ngoài hình tròn, sơ đồ, hoặc cửa sổ trình duyệt
  • Với sự kiện chuột, để đạt hiệu quả tương tự phải gắn handler sự kiện vào document, nhưng với Pointer Events thì setPointerCapture(event.pointerId) đơn giản hơn
  • Nếu không có bắt con trỏ, thao tác kéo có thể dừng lại khi kéo quá nhanh hoặc đi ra ngoài sơ đồ rồi quay lại
  • Các trường hợp thả nút ở ngoài sơ đồ hoặc chuyển sang cửa sổ khác bằng Alt+Tab cũng khiến cách xử lý kết thúc kéo khác nhau tùy có capture hay không
  • Trên thiết bị cảm ứng, pointer capture là hành vi mặc định, nhưng với chuột thì phải thiết lập rõ ràng

Cuộn cảm ứng và preventDefault()

  • Trên thiết bị cảm ứng, thao tác kéo bằng một ngón mặc định sẽ làm trang cuộn
  • Nếu cùng thao tác kéo một ngón đó cũng di chuyển hình tròn thì cuộn trang và kéo đối tượng sẽ xảy ra đồng thời
  • Cách đơn giản nhất là áp dụng CSS touch-action: none cho sơ đồ, nhưng khi đó sẽ không thể cuộn ở bất kỳ chỗ nào trong sơ đồ
  • Hành vi mong muốn là chỉ chặn cuộn khi kéo hình tròn, còn khi kéo chính sơ đồ thì vẫn cho phép cuộn
  • Kết quả so sánh như sau
    • touch-action: none cho toàn bộ sơ đồ: cả kéo hình tròn lẫn kéo sơ đồ đều ngừng cuộn
    • Giá trị mặc định: cả hai đều không chặn được cuộn
    • touch-action: none chỉ cho hình tròn: không hoạt động như mong đợi
    • preventDefault() trong touchstart: chỉ chặn cuộn khi kéo hình tròn, vẫn cho phép cuộn sơ đồ
  • preventDefault() được gắn vào touchstart, không phải pointerstart
  • Cách này hoạt động trong hầu hết tình huống, nhưng không hoạt động trong “mobile emulation mode” của Firefox trên desktop, trong khi lại hoạt động trên Firefox mobile của Android

Lưu offset khi bắt đầu kéo

  • Khi nắm vào mép của đối tượng để kéo mà tâm đối tượng lập tức nhảy tới vị trí con trỏ thì trải nghiệm sử dụng sẽ kém
  • Cách giải quyết là lưu vị trí tương đối giữa tâm đối tượng và vị trí con trỏ khi bắt đầu kéo
  • Sau đó khi di chuyển, tính vị trí đối tượng bằng cách cộng dx, dy đã lưu vào tọa độ con trỏ hiện tại
  • Cách này biến state.dragging từ true/false đơn thuần thành thông tin vị trí tương đối như {dx, dy}
  • Trên chuột, khi nắm từ mép thì độ nhảy sẽ thấy rất rõ, còn trên cảm ứng thì ngón tay che mất nên có thể ít thấy hơn
  • Những chi tiết nhỏ liên quan cũng được tổng hợp trong trang little details

Menu ngữ cảnh và nút chuột

  • Cách kích hoạt menu ngữ cảnh khác nhau tùy nền tảng
    • Windows: nhấn rồi thả chuột phải, Shift+F10
    • Linux: nhấn nút phải, Shift+F10
    • Mac: nhấn nút phải, Ctrl+nhấp chuột trái
    • iOS: chỉ long press trên văn bản
    • Android: long press ở bất cứ đâu
  • Vấn đề là có trường hợp nhìn thấy pointerdown nhưng không phải lúc nào cũng nhìn thấy pointerup
  • Nút trên thực tế có thể đã được thả ra nhưng trong code vẫn bị xem là đang được nhấn
  • Cách xử lý là bỏ qua nút phải và chỉ thiết lập trạng thái dragging với nút chuột trái
  • Trong thuộc tính button của đặc tả Pointer Events, button = 0 nghĩa là nút chính, có thể dùng để loại trừ nút giữa và nút phải
  • Trên Chrome và Safari của Mac, Ctrl+click có thể đi vào như pointerdown của nút trái nên cũng cần kiểm tra event.ctrlKey
  • Có các lựa chọn sau
    • Có thể chặn menu bằng preventDefault() trong contextmenu, nhưng điều này có thể xung đột với hành vi nhấp giữa hoặc kéo mặc định
    • Chỉ cho kéo bằng nút trái có thể tránh được phần lớn khác biệt của thao tác nhấp phải
    • Nếu muốn tránh cả Ctrl+click trên Mac thì cũng phải loại trừ ctrlKey
  • Dù thay đổi nút không được truyền qua pointerdown hay pointerup, vẫn có thể kiểm tra một phần qua trạng thái nút trong pointermove

Đối tượng chứa văn bản và hình ảnh

  • Nếu trong phần tử có thể kéo có chứa văn bản hoặc hình ảnh thì cần xử lý bổ sung
  • Khi kéo hình tròn, phần văn bản bên trong có thể bị chọn
  • Cách giải quyết là áp dụng CSS user-select: none
    • Nếu luôn áp dụng thì văn bản bên trong sẽ không bao giờ chọn được
    • Nếu chỉ áp dụng trong lúc kéo thì bình thường vẫn có thể chọn văn bản
  • Tính đến đầu năm 2023, Safari vẫn chưa hỗ trợ user-select không tiền tố nên cần đặt thêm webkitUserSelect
  • Windows, Linux và Mac hỗ trợ drag and drop liên ứng dụng cho văn bản và hình ảnh, và điều này xung đột với kéo đối tượng
  • Gọi preventDefault() trong dragstart có thể ưu tiên kéo đối tượng hơn kéo văn bản hoặc hình ảnh đã chọn
  • Hành vi thực tế có khác nhau đôi chút tùy trình duyệt và hệ điều hành

Nhiều con trỏ và kéo đồng thời

  • Việc có thể kéo nhiều đối tượng cùng lúc bằng nhiều ngón tay hoặc nhiều chuột là một trường hợp biên
  • Trên cảm ứng, đoạn mã được đưa ra đã hoạt động để kéo nhiều đối tượng cùng lúc
  • Trường hợp dùng hai ngón tay kéo cùng một đối tượng thì cần xử lý riêng
  • Cách giải quyết là lưu event.pointerId vào state.dragging hoặc state trong pointerdown, rồi trong pointermove thì bỏ qua nếu không phải cùng pointerId
  • Ví dụ triển khai này chưa được áp dụng đầy đủ trong phần thân bài, nhưng có thể xem tại canvas dragging test
  • Với chuột, đặc tả Pointer Events cho biết hệ điều hành phổ biến và user agent thường không có khái niệm nhiều đầu vào chuột
  • Kể cả khi có cả trackpad và chuột ngoài, nhìn chung chúng vẫn được xem là một con trỏ chuột duy nhất, nên tác giả cho rằng không có cách nào kéo các đối tượng khác nhau bằng các chuột khác nhau

Khi nhấn đồng thời nhiều nút chuột

  • Khi nhấn nhiều nút chuột cùng lúc, cách hoạt động của Pointer Events và Mouse Events sẽ khác nhau
  • Mouse Events gửi mousedown cho mỗi lần nhấn nút, và mouseup cho mỗi lần nhả nút
  • Theo đặc tả Pointer Events, pointerdown xảy ra ở nút được nhấn đầu tiên, còn pointerup xảy ra ở nút được nhả cuối cùng
  • Vì vậy pointerdownpointerup có thể xảy ra trong các trạng thái nút khác nhau
  • Ví dụ, nếu nhấn nút trái, rồi nhấn nút phải, sau đó thả nút trái thì đối tượng có thể vẫn tiếp tục ở trạng thái đang kéo
  • Cách giải quyết là kiểm tra trạng thái nút trái bằng event.buttons & 1 trong pointermove, và nếu không còn được nhấn thì gọi end(event)
  • Pointer capture có thể tiếp tục cho đến khi mọi nút đều được thả ra, và có thể duy trì như vậy nếu không chủ động giải phóng capture
  • Cách triển khai này không xử lý hết vấn đề này và mọi trường hợp biên khác

Phần tử kéo lồng nhau

  • Nếu bên trong một phần tử có thể kéo lại có thêm một phần tử có thể kéo khác, thì cả phần tử trong lẫn ngoài đều sẽ xử lý kéo
  • Cách giải quyết là gọi event.stopPropagation() trong phần tử kéo bên trong để sự kiện không nổi bọt lên phần tử kéo bên ngoài
  • Cách làm này được dùng ở cả pointerdown lẫn pointermove
  • Demo không có trong phần thân bài, nhưng có thể xem trong ví dụ Vue playground với trường hợp phần tử kéo màu đỏ là phần tử con của phần tử kéo màu vàng

Kéo trong Canvas

  • Thông thường người ta dùng SVG, nhưng trong <canvas> không thể gán trình xử lý sự kiện hay hình dạng con trỏ chuột trực tiếp cho chính phần tử có thể kéo
  • Với 2D Canvas hoặc WebGL, cần gắn trình xử lý sự kiện vào <canvas> và tự kiểm tra xem tọa độ có nằm trên đối tượng có thể kéo hay không
  • Cách xử lý như sau
    • pointerdown, touchstart, dragstart: nếu không nằm trên đối tượng có thể kéo thì trả về ngay
    • pointermove: thiết lập con trỏ tùy theo việc có đang nằm trên đối tượng có thể kéo hay không
  • Có demo tại examples page

Kết hợp hover chuột và drag cảm ứng

  • Một số sơ đồ phản ứng bằng hover khi dùng chuột mà không cần nhấn nút, còn trên thiết bị cảm ứng thì vì không có hover nên phải phản ứng bằng drag
  • Công thức cơ bản giả định cả chuột lẫn cảm ứng đều dùng drag
  • Để dùng đồng thời hover chuột và drag cảm ứng, hãy bỏ điều kiện if (!state.dragging) trong pointermove
  • Trên Responsive Design page, khi hover lên một hàng bằng chuột thì bố cục thay đổi, còn trên cảm ứng thì bố cục thay đổi bằng drag
  • Trong Hexagons Guide cũng có nhiều sơ đồ dùng đồng thời hover chuột và drag cảm ứng
  • Khi kéo từ mục A sang B trên cảm ứng, nếu capture được giữ nguyên thì sự kiện move sẽ được gửi tới A, nên để tô sáng B có thể cần phải giải phóng capture

Mở rộng Toggle Paint và trạng thái drag

  • Trong Rounded Cell Painter, pointerdown sẽ lưu màu vẽ ban đầu
  • Sau đó mọi pointermove cho tới pointerup đều dùng cùng một màu vẽ
  • Vì vậy state.dragging không chỉ cần chứa x, y ban đầu mà còn phải bao gồm màu vẽ ban đầu
  • Có demo trên examples page

Trường hợp biên lostpointercapture

  • Có thể dùng lostpointercapture để phát hiện việc mất pointer capture
  • Vẫn chưa hoàn toàn rõ nó xảy ra trong những tình huống nào và cần xử lý ra sao
  • Các kịch bản được đề cập trong phần bình luận như sau
    • Bắt đầu kéo hình tròn bằng nút trái
    • Giữ nút trái và di chuyển ra ngoài vùng kéo
    • Nhấn nút phải rồi thả nút trái
    • Nhấp vào bất kỳ đâu bên ngoài vùng kéo
  • standard nói rằng sau khi pointer capture được giải phóng, user agent bắt buộc phải phát ra sự kiện lostpointercapture
  • Hành vi thực tế của trình duyệt khác nhau đáng kể; trên một số hệ thống lostpointercapture sẽ phát ra, nhưng trên một số hệ thống khác thì không phát ra cho tới khi nhấp vào nơi khác

1 bình luận

 
GN⁺ 2023-09-30
Các ý kiến trên Hacker News
  • Đây là một bài viết về kéo thả, và tôi đã gặp gần như tất cả những cạm bẫy mà Amit nói đến, rồi đi đến cùng một giải pháp. Bài viết rất hay.
    Từ góc nhìn người dùng, kéo để đổi thứ tự trông rất tự nhiên, nhưng khi đi vào các mục có kích thước khác nhau, tạo placeholder giữa các mục, phát hiện mép, rồi cả hang thỏ của thuật toán căn khớp hộp, thì tự triển khai thuộc loại khá khó.

    • Có một mẹo khá tổng quát mà tôi thích dùng trong những trường hợp này: khi người dùng bắt đầu kéo, hãy tính tất cả các đích thả khả dĩ, hoặc layout của các đích hiện đang thấy, rồi ghi lại vị trí kết thúc của đối tượng đang kéo trong từng layout.
      Mỗi khi chuột di chuyển, chỉ cần chọn vị trí gần nhất với vị trí hiện tại của đối tượng đang kéo trong số đó, rồi render layout được chọn. Cách này hoạt động cho cảm giác tốt ngay cả với layout phức tạp hoặc khi reflow.
    • Tôi đã phải giải quyết vấn đề này trong ứng dụng thẻ chỉ mục Card Buddy của mình. [1] Đó là một thử thách thú vị, và về sau tôi cũng tìm ra cách tốt hơn.
      Cuối cùng, khi nhấc một thẻ lên, tôi tính layout như thể thẻ đó đã bị xóa khỏi bảng, nhờ vậy mọi thứ trở nên dễ hơn. Chỉ cần đẩy những thứ ở vị trí con trỏ đang hover ra là được.
      Dù vậy vẫn có rất nhiều ngoại lệ phải xử lý, đặc biệt khó khi bắt đầu chỉnh sửa một thẻ mới chưa được “commit” vào data model. Tôi cũng phải thêm tùy chọn đẩy các thẻ hiện có ra để tạo chỗ cho một thẻ ma.
      Việc thừa nhận rằng có nhiều ngoại lệ phải xử lý thủ công là rất hữu ích. Nếu tiếp cận như thể sẽ có một giải pháp tổng quát và đồng nhất hơn, bạn sẽ cứ quay vòng trong thiết kế. Có lẽ một ngày nào đó tôi nên tổng hợp các trường hợp ngoại lệ khác nhau thành một bài blog.
      Tôi cũng tìm ra một cách layout tốt hơn, giúp giảm các tính toán không cần thiết và làm cho layout engine linh hoạt, thân thiện với người dùng hơn. Thật đáng ngạc nhiên là cách chọn biểu diễn data model lại tạo ra khác biệt rất lớn cho giải pháp.
      [1] https://www.ussherpress.com/cardbuddy/
    • Nếu còn phải hỗ trợ đổi thứ tự bằng kéo thả có lồng nhau nữa thì còn khó hơn.
    • Cùng suy nghĩ. Ý tưởng thì đơn giản nhưng triển khai có thể rất tàn nhẫn. Tôi cũng đã xem chọn bằng cách kéo là điều quá hiển nhiên trong nhiều năm.
  • Có vẻ bài viết không đề cập đến xử lý hủy. Ví dụ trên Windows, có quy ước rằng nhấn Escape sẽ hủy thao tác kéo.
    Đôi khi bạn cũng muốn hủy thao tác kéo nếu người dùng thả chuột bên ngoài một vùng đã định nghĩa. Hủy là một cách undo nhanh hơn cho người dùng, hoặc một lối thoát kiểu “à, thật ra mình đâu có định kéo”.
    Vì vậy, ngay cả khi không có chức năng undo riêng, bạn vẫn cần lưu trạng thái gốc tại thời điểm bắt đầu kéo và khôi phục nếu bị hủy.
    Với trường hợp hủy kéo khi ra ngoài vùng, còn có cả bỏ hủy. Khi con trỏ ở ngoài vùng, giao diện hiển thị trực quan như thể đã quay về trạng thái ban đầu; khi con trỏ quay lại bên trong vùng, thao tác kéo được tiếp tục. Việc hủy thật sự xảy ra khi thả chuột, nhưng trong lúc kéo, nó đã báo trước bằng hình ảnh rằng thao tác sẽ bị hủy.

    • Trên Windows có hành vi nếu kéo một thứ gì đó đi quá xa, đối tượng đang kéo và con trỏ đột ngột biến mất, rồi bật về vị trí ban đầu mà không có cảnh báo.
      Thật khó tin là Microsoft vẫn tin đây là thiết kế UI tốt. Nó làm người dùng hoàn toàn bối rối, và có thể thấy họ sợ thả nút chuột trái vì không biết chuyện gì đang xảy ra, hoặc cứ lặp đi lặp lại như yo-yo.
      Tôi đồng ý rằng hủy là một vấn đề lớn thường bị bỏ qua, và nó không đơn giản. Người dùng phổ thông thậm chí còn không biết ESC có thể giúp.
    • Tôi tự hỏi trên các nền tảng khác có phím hủy nào ngoài ESC cần xử lý không.
  • Có thêm vài chi tiết tôi học được sau 20 năm kéo đủ loại đối tượng trong GUI của Ardour [0]: mọi sự kiện nhấn và nhả nút đều phải được xử lý như sự kiện kéo không có chuyển động.
    Tức là thao tác nhấn luôn bắt đầu kéo và thao tác nhả kết thúc nó; đoạn code xử lý nhả nút sẽ đặc biệt xử lý điều kiện không có chuyển động.
    [0] https://ardour.org/

    • Cách đó chắc cũng hoạt động, nhưng tôi đã không làm vậy, và sau khi nắm đúng những điều cơ bản thì không gặp vấn đề.
      Tôi dùng các handler mousedownmouseup thông thường, rồi dùng một timeout có thể cấu hình khoảng 150ms để xác định đó là click, bắt đầu kéo, hay một hành động khác. Nếu xảy ra mouseup, hoặc chuột rời khỏi màn hình và đang có thao tác kéo diễn ra, thì stopdrag. Với tôi, kéo chỉ là một trong nhiều trường hợp đặc biệt.
    • Tôi tò mò vì sao lại làm như vậy.
  • Từ rất lâu trước đây, ở Minneapolis có một geometry center được NSF tài trợ. Một hội nghị về lý thuyết nhóm tính toán được tổ chức, và họ mời vài người giống như linh vật từ các lĩnh vực lân cận.
    Tôi được mời vì đã viết một hệ thống hình học đại số, và trong cái lạnh -20°F, tôi dậy rất sớm để giành được một workstation Silicon Graphics dùng trong ngày hôm đó, rồi code một trò chơi nhằm hiểu rõ hơn về các bộ sinh và quan hệ của nhóm.
    Trò chơi đó có thao tác kéo. Kết quả là có những cuộc trò chuyện thú vị kéo dài đến 2 giờ sáng. Suy nghĩ của tôi là kéo không nhất thiết phải tuân theo các định luật vật lý của thế giới thực; kéo nên có cảm giác như một tab LSD cực mạnh. Mọi người đều bị cuốn vào ý tưởng này và ai cũng đưa ra ý tưởng của riêng mình.

  • Bài viết triển khai thao tác kéo tương đối cơ bản, nếu bỏ qua nhiều ngoại lệ phát sinh trong trình duyệt web. Tôi tò mò liệu có tài liệu nào về kéo có ràng buộc như snap vào guideline, ngăn va chạm với biên hoặc đối tượng khác, hay vùng thả có hoạt ảnh thay đổi kích thước/vị trí để phản ứng với thao tác kéo không
    Trước đây tôi từng muốn làm một UI Pomodoro timer tùy biến, chia một đồng hồ tròn thành các miếng hình quạt có độ dài khác nhau để định nghĩa các khoảng tập trung/nghỉ ngơi. Việc triển khai đổi thứ tự các miếng hình quạt đó bằng kéo thì không tiến triển được bao nhiêu

    • Với cách viết code này, chuyển động kéo sẽ cập nhật một trạng thái nào đó. Khi thiết lập trạng thái, ta có thể áp dụng ràng buộc, và trạng thái đó lại điều khiển phần hiển thị
      Tôi muốn tách hệ thống ràng buộc khỏi hệ thống xử lý sự kiện. Những thư viện như jquery-ui lại buộc hai phần này với nhau, khiến hệ thống xử lý sự kiện phải biết mọi ràng buộc có thể có. jquery-ui hỗ trợ hộp giới hạn, trục, lưới vuông và snap vào phần tử DOM. Nhưng nếu muốn snap vào lưới lục giác, lưới thang log, hay biên hình tròn thì lại không được hỗ trợ
      Trong code có thể thấy state.pos = …, đó là nơi thiết lập trạng thái. Với ràng buộc, tôi sẽ giấu pos phía sau một setter. Như vậy code xử lý sự kiện kéo không cần biết cần ràng buộc nào, setter vẫn có thể áp dụng ràng buộc
      Có lẽ tôi nên cập nhật trang để đưa ví dụ về ràng buộc. Tôi đã hoàn toàn quên nhắc đến khía cạnh này trong công thức code
      Ví dụ ràng buộc cũ nằm ở https://www.redblobgames.com/articles/curved-paths/making-of..., còn ví dụ tránh va chạm nằm ở https://redblobgames.github.io/circular-obstacle-pathfinding.... Tuy vậy tôi vẫn chưa thử đổi thứ tự bằng kéo hay vùng thả có hoạt ảnh
  • Tôi cũng biết ơn trang này. Khi thiết kế hệ thống lục giác cho phân tích địa lý ở Uber, tôi đã tham khảo rất nhiều https://www.redblobgames.com/grids/hexagons/

  • Đây là bài viết tổng hợp tốt các cạm bẫy và điểm cần chú ý khi xử lý tương tác cho ra hồn
    Về phía “có thể dùng ngay” hơn, tôi đã dùng interactjs khá lâu trong nhiều dự án

  • Cảm ơn mọi người. Tôi ngạc nhiên khi hôm nay nó lên HN, và tôi có thể trả lời câu hỏi

    • Không biết bạn đã xem qua thuộc tính HTML tích hợp draggable="true"sự kiện kéo chưa
      Tôi cũng tò mò liệu tính năng tích hợp này có thiếu sót gì so với đoạn code này không
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
    • Đây là một thảo luận tuyệt vời về kéo trong JavaScript và thông tin thật sự hữu ích
      Tôi có một câu hỏi nhanh. Nếu dùng các sự kiện DOM tích hợp như dragstart, làm sao để giới hạn chuyển động kéo theo một trục? Tôi đã triển khai chức năng kéo-thả bằng các sự kiện dragstart/dragenter/dragover/drop, nhưng chưa tìm được cách nhanh để giới hạn chuyển động theo trục x. Tôi nhớ API kéo-thả của JQuery từng hỗ trợ việc này. Tôi đang cố chỉ dùng sự kiện/API DOM native, nên nếu có thông tin hay gợi ý liên quan thì rất tốt
    • Trước đây tôi luôn viết code UI trình duyệt, nhưng đã nghỉ vài năm. Đây là lần đầu tôi nghe ai đó dùng pointer events
      Xét việc Safari mới thêm hỗ trợ vào năm 2020, tôi tò mò bạn có lo trình duyệt không hỗ trợ nó không. Người dùng macOS Mojave có lẽ cũng đang dùng Safari 13 hoặc 14, nên tôi hy vọng Safari 12 thực tế không còn được dùng nữa, nhưng với một ứng dụng production nhắm tới người dùng web phổ thông, nếu một tính năng đơn giản như kéo không hoạt động thì sẽ khá tệ
      Việc thêm trình xử lý sự kiện vào document trong lúc kéo là một thực hành cũ, còn trình duyệt thì liên tục thêm tính năng mới để đơn giản hóa một số use case, nhưng mỗi tính năng lại có ngoại lệ và cạm bẫy riêng. Bài viết cũng nói là vẫn chưa xử lý hoàn toàn. Kết quả cuối cùng vẫn còn sự kết hợp giữa pointer events và touch events
      Tôi tò mò liệu sự “đơn giản” theo nghĩa ít code hơn có đáng để đánh đổi bằng các ngoại lệ bổ sung, mức hỗ trợ trình duyệt thấp hơn, và việc lập trình viên phải hiểu các chi tiết cũng như khác biệt giữa trình duyệt của pointer events—thứ có lẽ kém được hiểu và kém được tài liệu hóa hơn mouse events—hay không
  • Còn thiếu một thứ: khả năng truy cập. Làm thế nào để có thể kéo bằng thao tác bàn phím?

    • Ở đây có vẻ câu hỏi hơi lệch. Nó giống như hỏi trong một chương trình vẽ, người dùng bàn phím sẽ làm thế nào để giữ trạng thái bấm chuột nhằm pan canvas. Không làm như vậy. Pan bằng chuột hoặc kéo đối tượng chỉ là một giao diện; nhiệm vụ là pan canvas, và có nhiều cách để đạt được việc đó
      Trong ví dụ của tôi, câu trả lời về khả năng truy cập là cung cấp thêm thao tác bàn phím để hoàn tất tác vụ đó. Nếu có thể, tốt nhất là theo cách không cần giữ phím liên tục. Ví dụ, vào chế độ pan bằng phím tắt, rồi dùng các phím mũi tên để di chuyển viewport quanh canvas là giải quyết được
      Với đối tượng có thể kéo, tác vụ là thay đổi thứ tự. Để làm cho tác vụ này có thể truy cập, chỉ cần cung cấp một phương tiện thay thế để đổi thứ tự. Ví dụ: dùng TAB để chuyển focus qua lại giữa các đối tượng, dùng phím để chọn đối tượng đang được focus, rồi dùng phím mũi tên để di chuyển phần xem trước khi kéo tới vị trí hợp lệ gần nhất theo hướng đó
    • Nhìn chung câu trả lời gần với “không làm như vậy”. UI nên cung cấp phương án thay thế cho thao tác kéo để những người không thể dùng thiết bị trỏ hoặc cảm ứng vẫn có thể tương tác với trang
      Tức là đừng cố nhồi hỗ trợ bàn phím vào bên trong phần triển khai kéo; hãy thêm chức năng bàn phím có ý nghĩa, tách biệt với tính năng kéo
    • Khi đó nó sẽ trở thành một đối tượng có thể di chuyển, và có vẻ có thể dễ dàng xây dựng dựa trên triển khai này
    • Chỉ cần chọn mục bằng TAB / Space / Enter, rồi di chuyển phần tử “đang được kéo” bằng Ctrl + các phím mũi tên. Tất nhiên mỗi lần nhấn phím chỉ có thể di chuyển từng khối một
  • Hay đấy. Đầu tuần này tôi đang triển khai kéo phần tử SVG trong một ứng dụng Vue, và đã phải tự khám phá gần như mọi thứ tác giả mô tả theo đúng cùng thứ tự; cuối cùng cũng ra một component gần như giống hệt
    Tuy nhiên, thay vì functional component, tôi viết thành một tiện ích composable tên là useDragging

    • Tuyệt. Tôi đã thử component có directive và slot, nhưng composable thì chưa thử. Lý do là tôi không chắc nên thiết lập các event handler theo cách đó như thế nào. Tôi cũng chưa có nhiều kinh nghiệm với composable