Partial drag/drop panel tab rearranging

Not full support, but basic rearranging is now possible!
This commit is contained in:
Guangcong Luo 2020-10-20 14:38:03 +01:00
parent 868244d49f
commit f951b6f113
6 changed files with 196 additions and 33 deletions

View File

@ -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) {

View File

@ -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;

View File

@ -283,6 +283,10 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
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<MainMenuRoom> {
const room = PS.rooms[roomid]!;
return <div class="pmbox">
<div class="mini-window">
<h3>
<h3 draggable onDragStart={this.handleDragStart} data-roomid={roomid}>
<button class="closebutton" name="closeRoom" value={roomid} aria-label="Close" tabIndex={-1}><i class="fa fa-times-circle"></i></button>
<button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button>
{room.title}

View File

@ -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 <guangcongluo@gmail.com>
* @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: {}}> {
<i class="fa fa-times-circle"></i>
</button>;
}
return <li><a class={className} href={`/${id}`} draggable={true}>{icon} <span>{title}</span></a>{closeButton}</li>;
return <li>
<a
class={className} href={`/${id}`} draggable={true}
onDragEnter={this.handleDragEnter} onDragStart={this.handleDragStart}
>
{icon} <span>{title}</span>
</a>
{closeButton}
</li>;
}
render() {
const userColor = window.BattleLog && {color: BattleLog.usernameColor(PS.user.userid)};
@ -97,7 +214,7 @@ class PSHeader extends preact.Component<{style: {}}> {
<div class="maintabbarbottom"></div>
<div class="tabbar maintabbar"><div class="inner">
<ul>
{this.renderRoomTab('' as RoomID)}
{this.renderRoomTab(PS.leftRoomList[0])}
</ul>
<ul>
{PS.leftRoomList.slice(1).map(roomid => this.renderRoomTab(roomid))}

View File

@ -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 <guangcongluo@gmail.com>
* @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) {

View File

@ -53,6 +53,30 @@
</script>
<script src="https://play.pokemonshowdown.com/config/config.js"></script>
<script>
if (!window.Config) {
// offline
window.Config = {
version: '0',
bannedHosts: [],
whitelist: [],
routes: {
root: "pokemonshowdown.com",
client: "play.pokemonshowdown.com",
dex: "dex.pokemonshowdown.com",
replays: "replay.pokemonshowdown.com",
users: "pokemonshowdown.com/users"
},
defaultserver: {
id: 'showdown',
host: 'sim3.psim.us',
port: 443,
httpport: 8000,
altport: 80,
registered: true
},
customcolors: {}
};
}
Config.testclient = true;
(function() {
if (location.search !== '') {