From f951b6f113401f49d29415bc84300ca6709b5ec0 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Tue, 20 Oct 2020 14:38:03 +0100 Subject: [PATCH] Partial drag/drop panel tab rearranging Not full support, but basic rearranging is now possible! --- build-tools/build-indexes | 8 +-- src/client-main.ts | 11 ++++ src/panel-mainmenu.tsx | 6 +- src/panel-topbar.tsx | 123 +++++++++++++++++++++++++++++++++++++- src/panels.tsx | 57 ++++++++++-------- testclient-beta.html | 24 ++++++++ 6 files changed, 196 insertions(+), 33 deletions(-) diff --git a/build-tools/build-indexes b/build-tools/build-indexes index 6af02d26a..2f1a7f13d 100755 --- a/build-tools/build-indexes +++ b/build-tools/build-indexes @@ -26,10 +26,10 @@ Dex.includeData(); console.log("DONE"); function es3stringify(obj) { - let buf = JSON.stringify(obj); - buf = buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, '$1:'); - buf = buf.replace(/return\:/g, '"return":').replace(/new\:/g, '"new":').replace(/delete\:/g, '"delete":'); - return buf; + const buf = JSON.stringify(obj); + return buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, (fullMatch, key) => ( + ['return', 'new', 'delete'].includes(key) ? fullMatch : `${key}:` + )); } function requireNoCache(pathSpec) { diff --git a/src/client-main.ts b/src/client-main.ts index 1dea48551..9b0c08939 100644 --- a/src/client-main.ts +++ b/src/client-main.ts @@ -636,6 +636,17 @@ const PS = new class extends PSModel { leftRoomWidth = 0; mainmenu: MainMenuRoom = null!; + /** + * The drag-and-drop API is incredibly dumb and doesn't let us know + * what's being dragged until the `drop` event, so we track it here. + * + * Note that `PS.dragging` will be null if the drag was initiated + * outside PS (e.g. dragging a team from File Explorer to PS), and + * for security reasons it's impossible to know what they are until + * they're dropped. + */ + dragging: {type: 'room', roomid: RoomID} | null = null; + /** Tracks whether or not to display the "Use arrow keys" hint */ arrowKeysUsed = false; diff --git a/src/panel-mainmenu.tsx b/src/panel-mainmenu.tsx index 593bc5fe5..99e096819 100644 --- a/src/panel-mainmenu.tsx +++ b/src/panel-mainmenu.tsx @@ -283,6 +283,10 @@ class MainMenuPanel extends PSRoomPanel { submit = (e: Event) => { alert('todo: implement'); }; + handleDragStart = (e: DragEvent) => { + const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID; + PS.dragging = {type: 'room', roomid}; + }; renderMiniRoom(room: PSRoom) { const roomType = PS.roomTypes[room.type]; const Panel = roomType ? roomType.Component : PSRoomPanel; @@ -293,7 +297,7 @@ class MainMenuPanel extends PSRoomPanel { const room = PS.rooms[roomid]!; return
-

+

{room.title} diff --git a/src/panel-topbar.tsx b/src/panel-topbar.tsx index dfb72f274..8b92152b8 100644 --- a/src/panel-topbar.tsx +++ b/src/panel-topbar.tsx @@ -3,13 +3,122 @@ * * Topbar view - handles the topbar and some generic popups. * - * Also sets up global event listeners. + * Also handles global drag-and-drop support. * * @author Guangcong Luo * @license AGPLv3 */ +window.addEventListener('drop', e => { + console.log('drop ' + e.dataTransfer!.dropEffect); + const target = e.target as HTMLElement; + if (/^text/.test((target as HTMLInputElement).type)) { + PS.dragging = null; + return; // Ignore text fields + } + + // The default team drop action for Firefox is to open the team as a + // URL, which needs to be prevented. + // The default file drop action for most browsers is to open the file + // in the tab, which is generally undesirable anyway. + e.preventDefault(); + PS.dragging = null; +}); +window.addEventListener('dragend', e => { + e.preventDefault(); + PS.dragging = null; +}); +window.addEventListener('dragover', e => { + // this prevents the bounce-back animation + e.preventDefault(); +}); + class PSHeader extends preact.Component<{style: {}}> { + handleDragEnter = (e: DragEvent) => { + console.log('dragenter ' + e.dataTransfer!.dropEffect); + e.preventDefault(); + if (!PS.dragging) return; // TODO: handle dragging other things onto roomtabs + /** the element being passed over */ + const target = e.currentTarget as HTMLAnchorElement; + + const draggingRoom = PS.dragging.roomid; + if (draggingRoom === null) return; + + const draggedOverRoom = PS.router.extractRoomID(target.href); + if (draggedOverRoom === null) return; // should never happen + if (draggingRoom === draggedOverRoom) return; + + const leftIndex = PS.leftRoomList.indexOf(draggedOverRoom); + if (leftIndex >= 0) { + this.dragOnto(draggingRoom, 'leftRoomList', leftIndex); + } else { + const rightIndex = PS.rightRoomList.indexOf(draggedOverRoom); + if (rightIndex >= 0) { + this.dragOnto(draggingRoom, 'rightRoomList', rightIndex); + } else { + return; + } + } + + // dropEffect !== 'none' prevents bounce-back animation in + // Chrome/Safari/Opera + // if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; + }; + handleDragStart = (e: DragEvent) => { + const roomid = PS.router.extractRoomID((e.currentTarget as HTMLAnchorElement).href); + if (!roomid) return; // should never happen + + PS.dragging = {type: 'room', roomid}; + }; + dragOnto(fromRoom: RoomID, toRoomList: 'leftRoomList' | 'rightRoomList' | 'miniRoomList', toIndex: number) { + // one day you will be able to rearrange mainmenu and rooms, but not today + if (fromRoom === '' || fromRoom === 'rooms') return; + + if (fromRoom === PS[toRoomList][toIndex]) return; + if (fromRoom === '' && toRoomList === 'miniRoomList') return; + + const roomLists = ['leftRoomList', 'rightRoomList', 'miniRoomList'] as const; + let fromRoomList; + let fromIndex = -1; + for (const roomList of roomLists) { + fromIndex = PS[roomList].indexOf(fromRoom); + if (fromIndex >= 0) { + fromRoomList = roomList; + break; + } + } + if (!fromRoomList) return; // shouldn't happen + + if (toRoomList === 'leftRoomList' && toIndex === 0) toIndex = 1; // Home is always leftmost + if (toRoomList === 'rightRoomList' && toIndex === PS.rightRoomList.length - 1) toIndex--; // Rooms is always rightmost + + PS[fromRoomList].splice(fromIndex, 1); + // if dragging within the same roomlist and toIndex > fromIndex, + // toIndex is offset by 1 now. Fortunately for us, we want to + // drag to the right of this tab in that case, so the -1 +1 + // cancel out + PS[toRoomList].splice(toIndex, 0, fromRoom); + + const room = PS.rooms[fromRoom]!; + switch (toRoomList) { + case 'leftRoomList': room.location = 'left'; break; + case 'rightRoomList': room.location = 'right'; break; + case 'miniRoomList': room.location = 'mini-window'; break; + } + if (fromRoomList !== toRoomList) { + if (fromRoom === PS.leftRoom.id) { + PS.leftRoom = PS.mainmenu; + } else if (PS.rightRoom && fromRoom === PS.rightRoom.id) { + PS.rightRoom = PS.rooms['rooms']!; + } + if (toRoomList === 'rightRoomList') { + PS.rightRoom = room; + } else if (toRoomList === 'leftRoomList') { + PS.leftRoom = room; + } + } + PS.update(); + } renderRoomTab(id: RoomID) { const room = PS.rooms[id]!; const closable = (id === '' || id === 'rooms' ? '' : ' closable'); @@ -82,7 +191,15 @@ class PSHeader extends preact.Component<{style: {}}> { ; } - return
  • {icon} {title}{closeButton}
  • ; + return
  • + + {icon} {title} + + {closeButton} +
  • ; } render() { const userColor = window.BattleLog && {color: BattleLog.usernameColor(PS.user.userid)}; @@ -97,7 +214,7 @@ class PSHeader extends preact.Component<{style: {}}> {
      - {this.renderRoomTab('' as RoomID)} + {this.renderRoomTab(PS.leftRoomList[0])}
      {PS.leftRoomList.slice(1).map(roomid => this.renderRoomTab(roomid))} diff --git a/src/panels.tsx b/src/panels.tsx index 45c2429f4..ca2d06aab 100644 --- a/src/panels.tsx +++ b/src/panels.tsx @@ -3,7 +3,7 @@ * * Main view - sets up the frame, and the generic panels. * - * Also sets up global event listeners. + * Also sets up most global event listeners. * * @author Guangcong Luo * @license AGPLv3 @@ -20,6 +20,34 @@ class PSRouter { this.subscribeHash(); } } + extractRoomID(url: string) { + if (url.startsWith(document.location.origin)) { + url = url.slice(document.location.origin.length); + } else { + if (url.startsWith('http://')) { + url = url.slice(7); + } else if (url.startsWith('https://')) { + url = url.slice(8); + } + if (url.startsWith(document.location.host)) { + url = url.slice(document.location.host.length); + } else if (PS.server.id === 'showdown' && url.startsWith('play.pokemonshowdown.com')) { + url = url.slice(24); + } else if (PS.server.id === 'showdown' && url.startsWith('psim.us')) { + url = url.slice(7); + } else if (url.startsWith('replay.pokemonshowdown.com')) { + url = url.slice(26).replace('/', '/battle-'); + } + } + if (url.startsWith('/')) url = url.slice(1); + + if (!/^[a-z0-9-]*$/.test(url)) return null; + + const redirects = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|privacy|contact|dex|insecure)$/; + if (redirects.test(url)) return null; + + return url as RoomID; + } subscribeHash() { if (location.hash) { const currentRoomid = location.hash.slice(1); @@ -196,7 +224,9 @@ class PSMain extends preact.Component { return; } if (elem.tagName === 'A' || elem.getAttribute('data-href')) { - const roomid = this.roomidFromLink(elem as HTMLAnchorElement); + const href = elem.getAttribute('data-href') || (elem as HTMLAnchorElement).href; + const roomid = PS.router.extractRoomID(href); + if (roomid !== null) { PS.addRoom({ id: roomid, @@ -294,29 +324,6 @@ class PSMain extends preact.Component { } return false; } - roomidFromLink(elem: HTMLAnchorElement) { - let href = elem.getAttribute('data-href'); - if (href) { - // yes that's what we needed - } else if (PS.server.id === 'showdown') { - if (elem.host && elem.host !== Config.routes.client && elem.host !== 'psim.us') { - return null; - } - href = elem.pathname; - } else { - if (elem.host !== location.host) { - return null; - } - href = elem.pathname; - } - const roomid = href.slice(1); - if (!/^[a-z0-9-]*$/.test(roomid)) { - return null; // not a roomid - } - const redirects = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|news|privacy|contact|dex|insecure)$/; - if (redirects.test(roomid)) return null; - return roomid as RoomID; - } static containingRoomid(elem: HTMLElement) { let curElem: HTMLElement | null = elem; while (curElem) { diff --git a/testclient-beta.html b/testclient-beta.html index ad02dce6a..ed08d9093 100644 --- a/testclient-beta.html +++ b/testclient-beta.html @@ -53,6 +53,30 @@