Triển khai đối tượng có thể kéo
(redblobgames.com)- 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/mousemovecó 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()trongtouchstartđể giảm xung đột - Công thức cuối cùng kết hợp
pointerdown,pointermove,pointerup,pointercancelvớisetPointerCapture(), đồ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, haylostpointercaptuređề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 ởpointeruphoặ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ếuevent.button !== 0thì 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ậtstate.pos - Ở
pointerup,pointercancel, đặtstate.dragging = null - Gọi
preventDefault()trongtouchstart
- Ở
- 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,mousemovevà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
mousedowntrên hình tròn, còn trong lúc kéo thì tạm thời gắnmousemovevàmouseupvàodocument - Nếu ở
mouseupgỡ các handlermousemove,mouseupđã gắn vàodocument, 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,touchmovethay chomousedown,mouseup,mousemove - Sau
touchstart, sự kiện cảm ứng tự động được capture, nên mọitouchmovesẽ đượ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
documentnhư 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()trongtouchstart, rồi cập nhật vị trí bằng tọa độevent.changedTouches[0]trongtouchmove - 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
documentnhư 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: nonecho 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: nonecho 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: nonechỉ cho hình tròn: không hoạt động như mong đợipreventDefault()trongtouchstart: 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àotouchstart, không phảipointerstart- 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.draggingtừ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
pointerdownnhưng không phải lúc nào cũng nhìn thấypointerup - 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
draggingvới nút chuột trái - Trong thuộc tính
buttoncủa đặc tả Pointer Events,button = 0nghĩ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ư
pointerdowncủa nút trái nên cũng cần kiểm traevent.ctrlKey - Có các lựa chọn sau
- Có thể chặn menu bằng
preventDefault()trongcontextmenu, 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
- Có thể chặn menu bằng
- Dù thay đổi nút không được truyền qua
pointerdownhaypointerup, vẫn có thể kiểm tra một phần qua trạng thái nút trongpointermove
Đố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-selectkhông tiền tố nên cần đặt thêmwebkitUserSelect - 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()trongdragstartcó 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.pointerIdvàostate.dragginghoặc state trongpointerdown, rồi trongpointermovethì bỏ qua nếu không phải cùngpointerId - 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
mousedowncho mỗi lần nhấn nút, vàmouseupcho mỗi lần nhả nút - Theo đặc tả Pointer Events,
pointerdownxảy ra ở nút được nhấn đầu tiên, cònpointerupxảy ra ở nút được nhả cuối cùng - Vì vậy
pointerdownvàpointerupcó 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 & 1trongpointermove, và nếu không còn được nhấn thì gọiend(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ả
pointerdownlẫnpointermove - 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ề ngaypointermove: 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)trongpointermove - 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,
pointerdownsẽ lưu màu vẽ ban đầu - Sau đó mọi
pointermovecho tớipointerupđều dùng cùng một màu vẽ - Vì vậy
state.draggingkhông chỉ cần chứax,yban đầ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
lostpointercapturesẽ 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
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ó.
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.
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/
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.
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.
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/
Tôi dùng các handler
mousedownvàmouseupthô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 ramouseup, 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ừ 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
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ấuposphí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ộcCó 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
draggable="true"và sự kiện kéo chưaTô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...
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ệndragstart/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ốtXé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
documenttrong 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 eventsTô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?
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 đó
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
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