Preact minor update batch 11
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run

Minor
- Unhide right panel when choosing "Two panels" layout option
- Refactor focusing
  - Correctly focus next room when closing currently active room
  - Correctly focus room when joining new room
- Use strict mode on all compiled files
- Fix router when started on `/` (it previously required starting on a
  non-empty room ID, which wasn't noticeable back when the URL needed
  to be `/preactalpha`)
- Update teambuilder sidebar CSS, to make it easier to add regular text
  - This is mainly for the "Tournaments" button in the main menu,
    which shares the CSS
- Fix new tournament elim tree text in Safari
- Update new tournament elim tree highlighted links to reliably
  link every still-playing game
- Remove latest gen from format name displays everywhere
  - Previously, they would only be removed from the format dropdown,
    but now they're also gone from the Ladder tab, battle tabs, and
    `/rank`
- Support async d3 loading
  - This allows chatrooms to be loaded way before all our dependencies
    are fully downloaded
- Remove "[Gen 9]" from format names everywhere (previously it was only
  removed from the format dropdown)
  - Also add "[Gen 6]" to unlabeled formats in `/rank` (Gen 6 was the
    last time we didn't have format generation as part of format names)

Trivial
- Stricter JSX linting
  - (unfortunately, most of the JSX style enforcement I actually want
    isn't possible in @stylistic)
- Make room.subscribeTo's second parameter optional
- Rearrange and comment loading order
- Rename hiddenInit -> focusNextUpdate (clarity)
- Rename PSMain -> PSView (clarity)
- Fix button spacing in Change Password
- Add `touch-action: manipulation` to <a> tags
- Refactor `nodeSize` in elim tour trees
This commit is contained in:
Guangcong Luo 2025-04-17 11:09:12 +00:00
parent 7ee7c6604a
commit 5971e5151a
26 changed files with 453 additions and 403 deletions

View File

@ -29,7 +29,9 @@
// ES3
"@babel/plugin-transform-member-expression-literals",
"@babel/plugin-transform-property-literals"
"@babel/plugin-transform-property-literals",
"@babel/plugin-transform-strict-mode"
],
"ignore": [
"src/globals.d.ts"

View File

@ -25,3 +25,6 @@ trim_trailing_whitespace = false
indent_style = tab
indent_size = 8
trim_trailing_whitespace = false
[COMMIT_EDITMSG]
indent_style = space

View File

@ -174,6 +174,7 @@ export const defaultRules = {
"@stylistic/jsx-one-expression-per-line": "off",
"@stylistic/jsx-max-props-per-line": "off",
"@stylistic/jsx-function-call-newline": "off",
"@stylistic/jsx-child-element-spacing": "error",
"no-restricted-syntax": ["error",
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
],

16
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": {
"@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx": "^7.25.9",
"@babel/plugin-transform-strict-mode": "^7.25.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0",
"babel-plugin-remove-import-export": "^1.1.1",
@ -1264,6 +1265,21 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-strict-mode": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-strict-mode/-/plugin-transform-strict-mode-7.25.9.tgz",
"integrity": "sha512-DplEwkN9xt6XCz/4oC9l8FJGn7LnOGPU7v08plq+OclMT55zAR9lkX7QIbQ9XscvvJNYpLUfYO4IYz/7JGkbXQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-template-literals": {
"version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz",

View File

@ -18,6 +18,7 @@
"dependencies": {
"@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx": "^7.25.9",
"@babel/plugin-transform-strict-mode": "^7.25.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0",
"babel-plugin-remove-import-export": "^1.1.1",

View File

@ -612,8 +612,8 @@
TournamentBox.nodeSize = {
width: 160, height: 30,
radius: 5,
separationX: 20, separationY: 20,
textOffset: -1
separationX: 20, separationY: 10,
textOffset: 4
};
TournamentBox.prototype.generateBracket = function (data, abbreviated) {
@ -655,9 +655,15 @@
child.highlightLink = true;
}
}
} else if (node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging') {
} else if (
node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging' ||
node.state === 'unavailable'
) {
for (var i = 0; i < node.children.length; i++) {
node.children[i].highlightLink = true;
var child = node.children[i];
if (child.team && !child.team.startsWith('(')) {
child.highlightLink = true;
}
}
} else if (highlightName) {
for (var i = 0; i < node.children.length; i++) {
@ -763,6 +769,7 @@
if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
elem.append('svg:text').classed('tournament-bracket-tree-node-team', true)
.attr('y', nodeSize.textOffset)
.classed('tournament-bracket-tree-node-team-draw', true)
.text(node.team || '');
} else {

View File

@ -0,0 +1,4 @@
"use strict";
PS.libsLoaded.loaded();
//# sourceMappingURL=client-endload.js.map

View File

@ -1339,11 +1339,6 @@
bufs[curBuf] += BattleLog.escapeHTML(curSection) + '</strong></summary>';
}
var formatName = BattleLog.escapeFormat(format.id);
if (formatName.charAt(0) !== '[') formatName = '[Gen 6] ' + formatName;
formatName = formatName.replace('[Gen 9] ', '');
formatName = formatName.replace('[Gen 9 ', '[');
formatName = formatName.replace('[Gen 8 ', '[');
formatName = formatName.replace('[Gen 7 ', '[');
bufs[curBuf] += (
'<li><button name="selectFormat" value="' + i +
'" class="option' + (curFormat === i ? ' cur' : '') + '">' + formatName +

View File

@ -119,7 +119,7 @@
if (room.title && room.title.charAt(0) === '[') {
var closeBracketIndex = room.title.indexOf(']');
if (closeBracketIndex > 0) {
return buf + ' draggable="true"><i class="text">' + BattleLog.escapeFormat(room.title.slice(1, closeBracketIndex)) + '</i><span>' + BattleLog.escapeHTML(room.title.slice(closeBracketIndex + 1)) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>';
return buf + ' draggable="true"><i class="text">' + BattleLog.escapeHTML(room.title.slice(1, closeBracketIndex)) + '</i><span>' + BattleLog.escapeHTML(room.title.slice(closeBracketIndex + 1)) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>';
}
}
return buf + ' draggable="true"><i class="fa fa-file-text-o"></i> <span>' + (BattleLog.escapeHTML(room.title) || id) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>';

View File

@ -72,13 +72,14 @@ https://psim.us/dev
document.head.appendChild(linkEl);
}
linkStyle("/style/sim-types.css");
linkStyle("/style/teambuilder.css");
linkStyle("/style/teambuilder.css?");
linkStyle("style/battle-search.css");
linkStyle("/style/font-awesome.css");
</script>
<script nomodule defer src="/js/lib/ps-polyfill.js"></script>
<script defer src="/config/config.js?"></script>
<script defer src="/js/client-core.js?"></script>
<!-- At this point, background and dark mode are loaded -->
<script defer src="/js/battle-dex-data.js?"></script>
<script defer src="/js/battle-dex.js?"></script>
@ -97,10 +98,16 @@ https://psim.us/dev
<script defer src="/js/panel-mainmenu.js?"></script>
<script defer src="/js/panel-rooms.js?"></script>
<script defer src="/js/panel-topbar.js?"></script>
<script defer src="/js/panel-popups.js?"></script>
<!-- at this point, the main view is loaded and usable -->
<script defer src="/js/miniedit.js?"></script>
<script defer src="/js/panel-chat-tournament.js?"></script>
<script defer src="/js/panel-chat.js?"></script>
<!-- at this point, chatrooms are usable -->
<script defer src="/js/panel-popups.js?"></script>
<script defer src="/js/panel-page.js?"></script>
<script defer src="/js/panel-ladder.js?"></script>
<script defer src="/js/battle-sound.js"></script>
<script defer src="/js/lib/jquery-2.2.4.min.js"></script>
@ -122,11 +129,11 @@ https://psim.us/dev
<script defer src="/js/battle-dex-search.js?"></script>
<script defer src="/js/battle-searchresults.js?"></script>
<script defer src="/js/panel-teambuilder-team.js?"></script>
<script defer src="/js/panel-ladder.js?"></script>
<script defer src="/js/panel-page.js?"></script>
<script defer src="/data/pokedex-mini.js?"></script>
<script defer src="/data/pokedex-mini-bw.js?"></script>
<script defer src="/js/lib/d3.v3.min.js"></script>
<script defer src="/js/client-endload.js?"></script>
</body></html>

View File

@ -1056,30 +1056,42 @@ export class BattleLog {
static escapeFormat(formatid = ''): string {
let atIndex = formatid.indexOf('@@@');
if (atIndex >= 0) {
return this.escapeFormat(formatid.slice(0, atIndex)) +
return this.escapeHTML(this.formatName(formatid.slice(0, atIndex))) +
'<br />Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3));
}
if (window.BattleFormats && BattleFormats[formatid]) {
return this.escapeHTML(BattleFormats[formatid].name);
}
if (window.NonBattleGames && NonBattleGames[formatid]) {
return this.escapeHTML(NonBattleGames[formatid]);
}
return this.escapeHTML(formatid);
return this.escapeHTML(this.formatName(formatid));
}
/**
* Do not store this output anywhere; it removes the generation number
* for the current gen.
*/
static formatName(formatid = ''): string {
if (!formatid) return '';
let atIndex = formatid.indexOf('@@@');
if (atIndex >= 0) {
return this.formatName(formatid.slice(0, atIndex)) +
' (Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3)) + ')';
}
if (!formatid.startsWith('gen')) {
formatid = `gen6${formatid}`;
}
let name = formatid;
if (window.BattleFormats && BattleFormats[formatid]) {
return BattleFormats[formatid].name;
name = BattleFormats[formatid].name;
}
if (window.NonBattleGames && NonBattleGames[formatid]) {
return NonBattleGames[formatid];
name = NonBattleGames[formatid];
}
return formatid;
if (name.startsWith('gen')) {
name = name.replace(/^gen([0-9])/, '[Gen $1] ');
}
if (name.startsWith(`[Gen ${Dex.gen}] `)) {
name = name.slice(`[Gen ${Dex.gen}] `.length);
} else if (name.startsWith(`[Gen ${Dex.gen} `)) {
name = '[' + name.slice(`[Gen ${Dex.gen} `.length);
}
return name;
}
static escapeHTML(str: string | number, jsEscapeToo?: boolean) {

View File

@ -0,0 +1,3 @@
import { PS } from "./client-main";
PS.libsLoaded.loaded();

View File

@ -696,6 +696,18 @@ type ParsedClientCommands = {
[command: `parsed${string}`]: (this: PSRoom, target: string, cmd: string) => string | boolean | null | void,
};
export class PSLoadTracker extends Promise<void> {
resolver!: (value: void) => void;
constructor() {
super(resolve => {
this.resolver = resolve;
});
}
loaded() {
this.resolver();
}
}
/**
* As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message",
* and `null` to mean "tell Preact to re-render this room"
@ -728,11 +740,13 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
width = 0;
height = 0;
/**
* popups sometimes initialize hidden, to calculate their position from their
* width/height without flickering. But hidden popups can't be focused, so
* we need to track their focus timing here.
* Preact means that the DOM state lags behind the app state. This means
* rooms frequently have `display: none` at the time we want to focus them.
* And popups sometimes initialize hidden, to calculate their position from
* their width/height without flickering. But hidden HTML elements can't be
* focused, so this is a note-to-self to focus the next time they can be.
*/
hiddenInit = false;
focusNextUpdate = false;
parentElem: HTMLElement | null = null;
parentRoomid: RoomID | null = null;
rightPopup = false;
@ -1134,6 +1148,8 @@ export const PS = new class extends PSModel {
newsHTML = document.querySelector('#room-news .mini-window-body')?.innerHTML || '';
libsLoaded = new PSLoadTracker();
constructor() {
super();
@ -1300,12 +1316,11 @@ export const PS = new class extends PSModel {
room = PS.rooms[roomid2];
const [, type] = args;
if (!room) {
this.addRoom({
room = this.addRoom({
id: roomid2,
type,
connected: true,
}, roomid === 'staff' || roomid === 'upperstaff');
room = PS.rooms[roomid2];
} else {
room.type = type;
room.connected = true;
@ -1471,8 +1486,11 @@ export const PS = new class extends PSModel {
if (this.leftPanel === room) this.leftPanel = newRoom;
if (this.rightPanel === room) this.rightPanel = newRoom;
if (this.panel === room) this.panel = newRoom;
if (this.room === room) this.room = newRoom;
if (roomid === '') this.mainmenu = newRoom as MainMenuRoom;
if (this.room === room) {
this.room = newRoom;
newRoom.focusNextUpdate = true;
}
updated = true;
}
@ -1490,7 +1508,7 @@ export const PS = new class extends PSModel {
}
this.closePopupsAbove(room, true);
if (!this.isVisible(room)) {
room.hiddenInit = true;
room.focusNextUpdate = true;
}
if (PS.isNormalRoom(room)) {
if (room.location === 'right' && !this.prefs.onepanel) {
@ -1574,42 +1592,6 @@ export const PS = new class extends PSModel {
}
return this.focusRoom(rooms[index + 1]);
}
focusPreview(room: PSRoom) {
if (room !== this.room) return '';
const verticalBuf = this.verticalFocusPreview();
if (verticalBuf) return verticalBuf;
const isMiniRoom = this.room.location === 'mini-window';
const { rooms, index } = this.horizontalNav();
if (index === -1) return '';
let buf = ' ';
const leftRoom = this.rooms[rooms[index - 1]];
if (leftRoom) buf += `\u2190 ${leftRoom.title}`;
buf += (this.arrowKeysUsed || isMiniRoom ? " | " : " (use arrow keys) ");
const rightRoom = this.rooms[rooms[index + 1]];
if (rightRoom) buf += `${rightRoom.title} \u2192`;
return buf;
}
verticalFocusPreview() {
const { rooms, index } = this.verticalNav();
if (index === -1) return '';
const upRoom = this.rooms[rooms[index - 1]];
let downRoom = this.rooms[rooms[index + 1]];
if (index === rooms.length - 2 && rooms[index + 1] === 'news') downRoom = undefined;
if (!upRoom && !downRoom) return '';
let buf = ' ';
// const altLabel = navigator.platform?.startsWith('Mac') ? '⌥' : 'ᴀʟᴛ';
const altLabel = navigator.platform?.startsWith('Mac') ? 'ᴏᴘᴛ' : 'ᴀʟᴛ';
if (upRoom) buf += `${altLabel}\u2191 ${upRoom.title}`;
buf += " | ";
if (downRoom) buf += `${altLabel}\u2193 ${downRoom.title}`;
return buf;
}
alert(message: string) {
this.join(`popup-${this.popups.length}` as RoomID, {
args: { message },
@ -1676,6 +1658,7 @@ export const PS = new class extends PSModel {
room.receiveLine(args);
}
}
if (!noFocus) room.focusNextUpdate = true;
return room;
}
hideRightRoom() {
@ -1809,6 +1792,7 @@ export const PS = new class extends PSModel {
}
}
removeRoom(room: PSRoom) {
const wasFocused = this.room === room;
room.destroy();
delete PS.rooms[room.id];
@ -1848,7 +1832,9 @@ export const PS = new class extends PSModel {
PS.room = this.popups.length ? PS.rooms[this.popups[this.popups.length - 1]]! : PS.panel;
}
PS.setFocus(PS.room);
if (wasFocused) {
this.room.focusNextUpdate = true;
}
}
/** do NOT use this in a while loop: see `closePopupsUntil */
closePopup(skipUpdate?: boolean) {

View File

@ -662,7 +662,7 @@ export class TournamentBracket extends preact.Component<{
export class TournamentTreeBracket extends preact.Component<{
data: TournamentTreeBracketData, abbreviated?: boolean,
}> {
d3Loaded = true;
d3Loader: Promise<void> | null = null;
forEachTreeNode<T extends TreeNode>(node: T, callback: (node: T, depth: number) => void, depth = 0) {
callback(node, depth);
if (node.children) {
@ -678,11 +678,15 @@ export class TournamentTreeBracket extends preact.Component<{
}
return clonedNode;
}
/**
* Customize tree size. Height is for a single player, a full node is double that.
*/
static nodeSize = {
width: 160, height: 30,
width: 160, height: 15,
radius: 5,
separationX: 20, separationY: 10,
textOffset: -1,
// Safari bug: some issue with dominant-baseline. whatever, we can just manually v-align text
textOffset: 4,
};
generateTreeBracket(data: TournamentTreeBracketData, abbreviated?: boolean) {
const div = document.createElement('div');
@ -698,11 +702,13 @@ export class TournamentTreeBracket extends preact.Component<{
return div;
}
if (!window.d3) {
this.d3Loaded = false;
div.innerHTML = `<b>d3 not loaded yet</b>`;
this.d3Loader ||= PS.libsLoaded.then(() => {
this.forceUpdate();
});
return div;
}
this.d3Loaded = true;
this.d3Loader = null;
let name = PS.user.name;
@ -729,9 +735,14 @@ export class TournamentTreeBracket extends preact.Component<{
child.highlightLink = true;
}
}
} else if (node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging') {
} else if (
node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging' ||
node.state === 'unavailable'
) {
for (const child of node.children) {
child.highlightLink = true;
if (child.team && !child.team.startsWith('(')) {
child.highlightLink = true;
}
}
} else if (highlightName) {
for (const child of node.children) {
@ -755,24 +766,26 @@ export class TournamentTreeBracket extends preact.Component<{
}
});
// Setting `breadthCompression` to 0.8 for 3+ depths with leaves is
// an extremely approximate guess for how tall a double+ elim tree
// should be. The old algorithm also approximated, but its
// approximation was arguably worse. This one is basically perfect
// for single elim and pretty good for double elim.
const depthsWithLeaves = hasLeafAtDepth.filter(Boolean).length;
const breadthCompression = depthsWithLeaves > 2 ? 0.8 : 2;
const maxBreadth = numLeaves - (depthsWithLeaves - 1) / breadthCompression;
const maxDepth = hasLeafAtDepth.length;
const nodeSize: any = { ...TournamentTreeBracket.nodeSize };
nodeSize.realWidth = nodeSize.width;
nodeSize.realHeight = nodeSize.height;
nodeSize.smallRealHeight = nodeSize.height / 2;
const nodeSize = TournamentTreeBracket.nodeSize;
const size = {
width: nodeSize.realWidth * maxDepth + nodeSize.separationX * (maxDepth + 1),
height: nodeSize.realHeight * (maxBreadth + 0.5) + nodeSize.separationY * maxBreadth,
width: nodeSize.width * maxDepth + nodeSize.separationX * (maxDepth + 1),
height: nodeSize.height * 2 * (maxBreadth + 0.5) + nodeSize.separationY * maxBreadth,
};
// Make d3 layout the tree
const tree = d3.layout.tree<TournamentElimBracketNode>()
.size([size.height, size.width - nodeSize.realWidth - nodeSize.separationX])
.size([size.height, size.width - nodeSize.width - nodeSize.separationX])
.separation(() => 1)
.children(node => (
node.children?.length ? node.children : null!
@ -785,16 +798,16 @@ export class TournamentTreeBracket extends preact.Component<{
const layoutRoot = d3.select(div)
.append('svg:svg').attr('width', size.width).attr('height', size.height)
.append('svg:g')
.attr('transform', `translate(${-(nodeSize.realWidth) / 2 - 6},0)`);
.attr('transform', `translate(${-(nodeSize.width) / 2 - 6},0)`);
// Style the links between the nodes
const diagonalLink = d3.svg.diagonal()
.source(link => ({
x: link.source.x, y: link.source.y + nodeSize.realWidth / 2,
x: link.source.x, y: link.source.y + nodeSize.width / 2,
}))
.target(link => ({
x: link.target.x, y: link.target.y - nodeSize.realWidth / 2,
x: link.target.x, y: link.target.y - nodeSize.width / 2,
}))
.projection(link => [
size.width - link.y, link.x,
@ -818,8 +831,8 @@ export class TournamentTreeBracket extends preact.Component<{
const outerElem = elem;
if (node.abbreviated) {
elem.append('svg:text').attr('y', -nodeSize.realHeight / 4 + 4)
.attr('x', -nodeSize.realWidth / 2 - 7).classed('tournament-bracket-tree-abbreviated', true)
elem.append('svg:text').attr('y', -nodeSize.height / 2 + 4)
.attr('x', -nodeSize.width / 2 - 7).classed('tournament-bracket-tree-abbreviated', true)
.text('...');
}
@ -841,28 +854,29 @@ export class TournamentTreeBracket extends preact.Component<{
if (node.team && !node.team1 && !node.team2) {
const rect = elem.append('svg:rect').classed('tournament-bracket-tree-draw', true)
.attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth);
rect.attr('y', -nodeSize.smallRealHeight / 2).attr('height', nodeSize.smallRealHeight);
.attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
.attr('y', -nodeSize.height / 2).attr('height', nodeSize.height);
if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
elem.append('svg:text').classed('tournament-bracket-tree-node-team', true)
.attr('y', nodeSize.textOffset)
.classed('tournament-bracket-tree-node-team-draw', true)
.text(node.team || '');
} else {
const rect1 = elem.append('svg:rect')
.attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth)
.attr('y', -nodeSize.smallRealHeight).attr('height', nodeSize.smallRealHeight);
.attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
.attr('y', -nodeSize.height).attr('height', nodeSize.height);
const rect2 = elem.append('svg:rect')
.attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth)
.attr('y', 0).attr('height', nodeSize.smallRealHeight);
.attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
.attr('y', 0).attr('height', nodeSize.height);
if (node.team1 === name) rect1.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
if (node.team2 === name) rect2.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
const row1 = elem.append('svg:text').attr('y', -nodeSize.realHeight / 4 + nodeSize.textOffset)
const row1 = elem.append('svg:text').attr('y', -nodeSize.height / 2 + nodeSize.textOffset)
.classed('tournament-bracket-tree-node-row1', true);
const row2 = elem.append('svg:text').attr('y', nodeSize.realHeight / 4 + nodeSize.textOffset)
const row2 = elem.append('svg:text').attr('y', nodeSize.height / 2 + nodeSize.textOffset)
.classed('tournament-bracket-tree-node-row2', true);
const team1 = row1.append('svg:tspan').classed('tournament-bracket-tree-team', true)
@ -907,7 +921,7 @@ export class TournamentTreeBracket extends preact.Component<{
this.base!.appendChild(this.generateTreeBracket(this.props.data, this.props.abbreviated));
}
override shouldComponentUpdate(props: { data: TournamentTreeBracketData }) {
if (props.data === this.props.data && this.d3Loaded) return false;
if (props.data === this.props.data && !this.d3Loader) return false;
this.base!.replaceChild(this.generateTreeBracket(props.data), this.base!.children[0]);
return false;
}
@ -923,7 +937,7 @@ export class TourPopOutPanel extends PSRoomPanel {
static readonly noURL = true;
override componentDidMount() {
const tour = this.props.room.args?.tour as ChatTournament;
if (tour) this.subscribeTo(tour, () => this.forceUpdate());
if (tour) this.subscribeTo(tour);
}
override render() {
const room = this.props.room;

View File

@ -8,7 +8,7 @@
import preact from "../js/lib/preact";
import type { PSSubscription } from "./client-core";
import { PS, PSRoom, type RoomOptions, type RoomID, type Team } from "./client-main";
import { PSMain, PSPanelWrapper, PSRoomPanel } from "./panels";
import { PSView, PSPanelWrapper, PSRoomPanel } from "./panels";
import { TeamForm } from "./panel-mainmenu";
import { BattleLog } from "./battle-log";
import type { Battle } from "./battle";
@ -113,7 +113,8 @@ export class ChatRoom extends PSRoom {
this.title = `Console`;
} else if (this.id.startsWith('dm-')) {
const id = this.id.slice(3);
if (!name || toID(name) !== id) name = this.pmTarget || id;
if (toID(name) !== id) name = null;
name ||= this.pmTarget || id;
if (/[A-Za-z0-9]/.test(name.charAt(0))) name = ` ${name}`;
const nameWithGroup = name;
name = name.slice(1);
@ -194,7 +195,7 @@ export class ChatRoom extends PSRoom {
if (!formatTargeting ||
formats[formatId] ||
gens[formatId.slice(0, 4)] ||
(gens['gen6'] && formatId.substr(0, 3) !== 'gen')) {
(gens['gen6'] && !formatId.startsWith('gen'))) {
buffer += '<tr>';
} else {
buffer += '<tr class="hidden">';
@ -704,10 +705,10 @@ export class ChatTextEntry extends preact.Component<{
onInput={this.update}
onKeyDown={this.onKeyDown}
style={{ resize: 'none', width: '100%', height: '16px', padding: '2px 3px 1px 3px' }}
placeholder={PS.focusPreview(this.props.room)}
placeholder={PSView.focusPreview(this.props.room)}
/> : <ChatTextBox
disabled={!this.props.room.connected || !canTalk}
placeholder={PS.focusPreview(this.props.room)}
placeholder={PSView.focusPreview(this.props.room)}
/>}
</form>
{!canTalk && <button data-href="login" class="button autofocus">
@ -725,14 +726,14 @@ class ChatTextBox extends preact.Component<{ placeholder: string, disabled?: boo
return false;
}
handleFocus = () => {
PSMain.setTextboxFocused(true);
PSView.setTextboxFocused(true);
};
handleBlur = () => {
PSMain.setTextboxFocused(false);
PSView.setTextboxFocused(false);
};
override render() {
return <pre
class={`textbox textbox-empty ${this.props.disabled ? ' disabled' : ''}`} placeholder={this.props.placeholder}
class={`textbox textbox-empty ${this.props.disabled ? ' disabled' : ' autofocus'}`} placeholder={this.props.placeholder}
onFocus={this.handleFocus} onBlur={this.handleBlur}
>{'\n'}</pre>;
}
@ -745,10 +746,10 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
static readonly location = 'right';
static readonly icon = <i class="fa fa-comment-o"></i>;
override componentDidMount(): void {
super.componentDidMount();
this.subscribeTo(PS.user, () => {
this.props.room.updateTarget();
});
super.componentDidMount();
}
send = (text: string) => {
this.props.room.send(text);
@ -818,7 +819,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
return <PSPanelWrapper room={room} focusClick>
<ChatLog class="chat-log" room={this.props.room} left={tinyLayout ? 0 : 146} top={room.tour?.info.isActive ? 30 : 0}>
{challengeTo || challengeFrom && [challengeTo, challengeFrom]}
{challengeTo}{challengeFrom}
</ChatLog>
{room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />}
<ChatTextEntry

View File

@ -229,9 +229,7 @@ class LadderListPanel extends PSRoomPanel {
static readonly title = 'Ladder';
override componentDidMount() {
this.subscribeTo(PS.teams, () => {
this.forceUpdate();
});
this.subscribeTo(PS.teams);
}
renderList() {
if (!window.BattleFormats) {

View File

@ -129,22 +129,19 @@ class UserPanel extends PSRoomPanel<UserRoom> {
buttonbar.push(isSelf ? (
<p class="buttonbar">
<button class="button" disabled>Challenge</button> {}
<button class="button" data-href="/dm-">Chat Self</button>
<button class="button" data-href="dm-">Chat Self</button>
</p>
) : !PS.user.named ? (
<p class="buttonbar">
<button class="button" disabled>Challenge</button> {}
<button class="button" disabled>Chat</button>
<button class="button" disabled>Chat</button> {}
<button class="button" disabled>{'\u2026'}</button>
</p>
) : (
<p class="buttonbar">
<button class="button" data-href={`/challenge-${user.userid}`}>Challenge</button> {}
<button class="button" data-href={`/dm-${user.userid}`}>Chat</button> {}
<button
class="button"
data-href="/useroptions"
value={`${room.userid as string},${room.parentRoomid as string}`}
>{'\u2026'}</button>
<button class="button" data-href={`challenge-${user.userid}`}>Challenge</button> {}
<button class="button" data-href={`dm-${user.userid}`}>Chat</button> {}
<button class="button" data-href={`useroptions-${user.userid}-${room.parentRoomid || ''}`}>{'\u2026'}</button>
</p>
));
if (isSelf) {
@ -158,13 +155,13 @@ class UserPanel extends PSRoomPanel<UserRoom> {
}
}
const avatar = user.avatar !== '[loading]' ? Dex.resolveAvatar(`${user.avatar || 'unknown'}`) : null;
return [<div class="userdetails">
{user.avatar !== '[loading]' &&
<img
{...(room.isSelf ? { 'data-href': 'avatars' } : {})}
class={'trainersprite' + (room.isSelf ? ' yours' : '')}
src={Dex.resolveAvatar(`${user.avatar || 'unknown'}`)}
/>}
{avatar && (room.isSelf ? (
<img src={avatar} class="trainersprite yours" data-href="avatars" />
) : (
<img src={avatar} class="trainersprite" />
))}
<strong><a
href={`//${Config.routes.users}/${user.userid}`} target="_blank"
style={{ color: away ? '#888888' : BattleLog.usernameColor(user.userid) }}
@ -201,7 +198,7 @@ class UserPanel extends PSRoomPanel<UserRoom> {
return <PSPanelWrapper room={room}><div class="pad">
{showLookup && <form onSubmit={this.lookup} style={{ minWidth: '278px' }}>
<label class="label">
Username:
Username: {}
<input type="search" name="username" class="textbox autofocus" onInput={this.maybeReset} onChange={this.maybeReset} />
</label>
{!room.userid && <p class="buttonbar">
@ -218,7 +215,7 @@ class UserPanel extends PSRoomPanel<UserRoom> {
class UserOptionsPanel extends PSRoomPanel {
static readonly id = 'useroptions';
static readonly routes = ['useroptions'];
static readonly routes = ['useroptions-*'];
static readonly location = 'popup';
static readonly noURL = true;
declare state: {
@ -228,6 +225,12 @@ class UserOptionsPanel extends PSRoomPanel {
requestSent?: boolean,
data?: Record<string, string>,
};
getTargets() {
const [, targetUser, targetRoomid] = this.props.room.id.split('-');
let targetRoom = (PS.rooms[targetRoomid] || null) as ChatRoom | null;
if (targetRoom?.type !== 'chat') targetRoom = null;
return { targetUser: targetUser as ID, targetRoomid: targetRoomid as RoomID, targetRoom };
}
handleMute = (ev: Event) => {
this.setState({ showMuteInput: true, showBanInput: false });
@ -247,52 +250,42 @@ class UserOptionsPanel extends PSRoomPanel {
};
handleConfirm = (ev: Event) => {
let data = this.state.data;
const data = this.state.data;
if (!data) return;
let roomid = toRoomid(data.room);
let room = PS.rooms[roomid];
const { targetUser, targetRoom } = this.getTargets();
let cmd = '';
if (data.action === "Mute") {
cmd += data.duration === "1 hour" ? "/hourmute " : "/mute ";
cmd += `${data.targetUser} ${data.reason ? ',' + data.reason : ''}`;
cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`;
} else {
cmd += data.duration === "1 week" ? "/weekban " : "/ban ";
cmd += `${data.targetUser} ${data.reason ? ',' + data.reason : ''}`;
cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`;
}
room?.send(cmd);
targetRoom?.send(cmd);
this.close();
};
handleAddFriend = (ev: Event) => {
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(",");
let [targetUser, roomid] = args;
PS.rooms[roomid]?.send(`/friend add ${targetUser}`);
const { targetUser, targetRoom } = this.getTargets();
targetRoom?.send(`/friend add ${targetUser}`);
this.setState({ requestSent: true });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleIgnore = () => {
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(",");
let [targetUser, roomid] = args;
let room = PS.rooms[roomid];
room?.send(`/ignore ${targetUser}`);
const { targetUser, targetRoom } = this.getTargets();
targetRoom?.send(`/ignore ${targetUser}`);
this.close();
};
muteUser = (ev: Event) => {
this.setState({ showMuteInput: false });
let hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr";
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(",");
let [targetUser, roomid] = args;
let room = PS.rooms[roomid];
if (room?.type !== "chat") return; // should never happen
let reason = this.base?.querySelector<HTMLInputElement>("input[name=mutereason]")?.value;
let data = {
const hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr";
const reason = this.base?.querySelector<HTMLInputElement>("input[name=mutereason]")?.value;
const data = {
action: 'Mute',
targetUser,
room: room?.title,
reason,
duration: hrMute ? "1 hour" : "7 minutes",
};
@ -303,16 +296,10 @@ class UserOptionsPanel extends PSRoomPanel {
banUser = (ev: Event) => {
this.setState({ showBanInput: false });
let weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk";
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(",");
let [targetUser, roomid] = args;
let room = PS.rooms[roomid];
if (room?.type !== "chat") return; // should never happen
let reason = this.base?.querySelector<HTMLInputElement>("input[name=banreason]")?.value;
let data = {
const weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk";
const reason = this.base?.querySelector<HTMLInputElement>("input[name=banreason]")?.value;
const data = {
action: 'Ban',
targetUser,
room: room?.title,
reason,
duration: weekBan ? "1 week" : "2 days",
};
@ -321,20 +308,16 @@ class UserOptionsPanel extends PSRoomPanel {
ev.stopImmediatePropagation();
};
update = () => {
this.forceUpdate();
};
override render() {
const room = this.props.room;
const parentRoom = PS.rooms[this.props.room.parentRoomid! || ''] as ChatRoom;
let canMute = false;
let canBan = false;
if (parentRoom?.type === "chat") {
let banPerms = ["@", "#", "~"];
let mutePerms = ["%", ...banPerms];
canMute = mutePerms.includes(parentRoom.users[PS.user.userid].charAt(0));
canBan = banPerms.includes(parentRoom.users[PS.user.userid].charAt(0));
const { targetUser, targetRoom } = this.getTargets();
if (targetRoom) {
const banPerms = ["@", "#", "~"];
const mutePerms = ["%", ...banPerms];
canMute = mutePerms.includes(targetRoom.users[PS.user.userid]?.charAt(0));
canBan = banPerms.includes(targetRoom.users[PS.user.userid]?.charAt(0));
}
return <PSPanelWrapper room={room} width={280}><div class="pad">
@ -344,16 +327,15 @@ class UserOptionsPanel extends PSRoomPanel {
</button>
</p>
<p>
<button
class="button"
data-href={`view-help-request-report-user-${(room.parentElem as HTMLInputElement).value.split(",")[0]}`}
>
<button data-href={`view-help-request-report-user-${targetUser}`} class="button">
Report
</button>
</p>
<p>
{this.state.requestSent ? (
<button class="button disabled"> Sent request </button>
<button class="button disabled">
Sent request
</button>
) : (
<button onClick={this.handleAddFriend} class="button">
Add friend
@ -378,7 +360,8 @@ class UserOptionsPanel extends PSRoomPanel {
<p class="buttonbar">
{canMute && !this.state.showBanInput && !this.state.showConfirm && (this.state.showMuteInput ? (
<div>
<label class="inputlabel"> Reason:
<label class="label">
Reason: {}
<input name="mutereason" class="textbox autofocus" placeholder="Mute reason (optional)" />
</label> {} <br />
<button class="button" onClick={this.muteUser} value="7min">For 7 Mins</button> {}
@ -392,9 +375,10 @@ class UserOptionsPanel extends PSRoomPanel {
))} {}
{canBan && !this.state.showMuteInput && !this.state.showConfirm && (this.state.showBanInput ? (
<div>
<label class="inputlabel"> Reason:
<label class="label">
Reason: {}
<input name="banreason" class="textbox autofocus" placeholder="Ban reason (optional)" />
</label> <br />
</label><br />
<button class="button" onClick={this.banUser} value="2d">For 2 Days</button> {}
<button class="button" onClick={this.banUser} value="1wk">For 1 Week</button> {}
<button class="button" onClick={this.handleCancel}>Cancel</button>
@ -483,6 +467,10 @@ class OptionsPanel extends PSRoomPanel {
static readonly location = 'popup';
declare state: { showStatusInput?: boolean, showStatusUpdated?: boolean };
override componentDidMount() {
super.componentDidMount();
this.subscribeTo(PS.user);
}
setTheme = (e: Event) => {
const theme = (e.currentTarget as HTMLSelectElement).value as 'light' | 'dark' | 'system';
PS.prefs.set('theme', theme);
@ -493,6 +481,7 @@ class OptionsPanel extends PSRoomPanel {
switch (layout) {
case '':
PS.prefs.set('onepanel', null);
PS.rightPanel ||= PS.rooms['rooms'] || null;
break;
case 'onepanel':
PS.prefs.set('onepanel', true);
@ -580,7 +569,7 @@ class OptionsPanel extends PSRoomPanel {
</p>
)}
{PS.user.named && (PS.user.registered ?
{PS.user.named && (PS.user.registered?.userid === PS.user.userid ?
<button className="button" data-href="changepassword">Password...</button> :
<button className="button" data-href="register">Register</button>)}
@ -767,7 +756,8 @@ class LoginPanel extends PSRoomPanel {
</p>}
{loginState?.needsPassword && <p>
<i class="fa fa-level-up fa-rotate-90"></i> <strong>if you registered this name:</strong>
<label class="label">Password: {}
<label class="label">
Password: {}
<input
class="textbox" type={this.state.passwordShown ? 'text' : 'password'} name="password"
autocomplete="current-password" style="width:173px"
@ -960,7 +950,9 @@ class ReplacePlayerPanel extends PSRoomPanel {
<input name="newplayer" class="textbox autofocus" />
</p>
<p>
<button type="submit" class="button"><strong>Replace</strong></button> {}
<button type="submit" class="button">
<strong>Replace</strong>
</button> {}
<button type="button" data-cmd="/close" class="button">
Cancel
</button>
@ -1017,58 +1009,36 @@ class ChangePasswordPanel extends PSRoomPanel {
</p> }
<p>Change your password:</p>
<p>
<label class="label">Username:
<strong><input
type="text"
name="username"
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
<label class="label">
Username: {}
<input name="username" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
</label>
</p>
<p>
<label class="label">Old password:
<input
class="textbox autofocus"
type="password"
name="oldpassword"
autocomplete="current-password"
/></label>
<label class="label">
Old password: {}
<input name="oldpassword" type="password" autocomplete="current-password" class="textbox autofocus" />
</label>
</p>
<p>
<label class="label">New password:
<input
class="textbox"
type="password"
name="password"
autocomplete="new-password"
/></label>
<label class="label">
New password: {}
<input name="password" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p>
<label class="label">New password (confirm):
<input
class="textbox"
type="password"
name="cpassword"
autocomplete="new-password"
/></label>
<label class="label">
New password (confirm): {}
<input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p class="buttonbar">
<button type="submit" class="button">
<strong>Change password</strong>
</button>
</button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>
</div>
</PSPanelWrapper>;
}
@ -1083,10 +1053,6 @@ class RegisterPanel extends PSRoomPanel {
declare state: { errorMsg: string };
update = () => {
this.forceUpdate();
};
handleRegisterUser = (ev: Event) => {
ev.preventDefault();
let captcha = this.base?.querySelector<HTMLInputElement>('input[name=captcha]')?.value;
@ -1130,55 +1096,37 @@ class RegisterPanel extends PSRoomPanel {
</p> }
<p>Register your account:</p>
<p>
<label class="label">Username:
<strong><input
type="text"
name="name"
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
<label class="label">
Username: {}
<input name="name" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
</label>
</p>
<p>
<label class="label">Password:
<input
class="textbox autofocus"
type="password"
name="password"
autocomplete="new-password"
/></label>
<label class="label">
Password: {}
<input name="password" type="password" autocomplete="new-password" class="textbox autofocus" />
</label>
</p>
<p>
<label class="label">Password (confirm):
<input
class="textbox"
type="password"
name="cpassword"
autocomplete="new-password"
/></label>
<label class="label">
Password (confirm): {}
<input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p>
<label class="label"> <img
<label class="label"><img
src="https://play.pokemonshowdown.com/sprites/gen5ani/pikachu.gif"
alt="An Electric-type mouse that is the mascot of the Pokémon franchise."
/></label>
</p>
<p>
<label class="label">What is this pokemon?
<input
class="textbox" type="text" name="captcha" value=""
/></label>
<label class="label">
What is this pokemon?{}
<input name="captcha" class="textbox" />
</label>
</p>
<p class="buttonbar">
<button type="submit" class="button"><strong>Register</strong></button>
<button type="submit" class="button"><strong>Register</strong></button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>

View File

@ -233,7 +233,7 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
<i class="fa fa-chevron-left"></i> List
</button>
<label class="label teamname">
Team name:
Team name:{}
<input
class="textbox" type="text" value={team.name} onInput={this.rename} onChange={this.rename} onKeyUp={this.rename}
/>

View File

@ -11,7 +11,7 @@
import preact from "../js/lib/preact";
import { PS, type PSRoom, type RoomID } from "./client-main";
import { PSMain } from "./panels";
import { PSView } from "./panels";
import type { Battle } from "./battle";
import { BattleLog } from "./battle-log";
@ -194,7 +194,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
</span>;
}
renderVertical() {
return <div id="header" class="header-vertical" style={this.props.style} onClick={PSMain.scrollToHeader}>
return <div id="header" class="header-vertical" style={this.props.style} onClick={PSView.scrollToHeader}>
<div class="maintabbarbottom"></div>
<div class="scrollable-part">
<img
@ -233,7 +233,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
}
override render() {
if (PS.leftPanelWidth === null) {
if (!PSMain.textboxFocused) {
if (!PSView.textboxFocused) {
document.documentElement.classList?.add('scroll-snap-enabled');
}
return this.renderVertical();
@ -301,11 +301,11 @@ export class PSMiniHeader extends preact.Component {
const menuButton = showMenuButton ? (
null
) : window.scrollX ? (
<button onClick={PSMain.scrollToHeader} class={`mini-header-left ${notifying}`} aria-label="Menu">
<button onClick={PSView.scrollToHeader} class={`mini-header-left ${notifying}`} aria-label="Menu">
<i class="fa fa-arrow-left"></i>
</button>
) : (
<button onClick={PSMain.scrollToRoom} class="mini-header-left" aria-label="Menu">
<button onClick={PSView.scrollToRoom} class="mini-header-left" aria-label="Menu">
<i class="fa fa-arrow-right"></i>
</button>
);
@ -319,4 +319,4 @@ export class PSMiniHeader extends preact.Component {
}
}
preact.render(<PSMain />, document.body, document.getElementById('ps-frame')!);
preact.render(<PSView />, document.body, document.getElementById('ps-frame')!);

View File

@ -24,7 +24,7 @@ export class PSRouter {
panelState = '';
constructor() {
const currentRoomid = location.pathname.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) {
if (/^[a-z0-9-]*$/.test(currentRoomid)) {
this.subscribeHistory();
} else if (location.pathname.endsWith('.html')) {
this.subscribeHash();
@ -96,8 +96,6 @@ export class PSRouter {
const currentRoomid = location.hash.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) {
PS.join(currentRoomid as RoomID);
} else {
return;
}
}
PS.subscribeAndRun(() => {
@ -120,8 +118,6 @@ export class PSRouter {
const currentRoomid = location.pathname.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) {
PS.join(currentRoomid as RoomID);
} else {
return;
}
if (!window.history) return;
PS.subscribeAndRun(() => {
@ -157,13 +153,14 @@ PS.router = new PSRouter();
export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ room: T }> {
subscriptions: PSSubscription[] = [];
subscribeTo<M>(model: PSModel<M> | PSStreamModel<M>, callback: (value: M) => void): PSSubscription {
subscribeTo<M>(
model: PSModel<M> | PSStreamModel<M>, callback: (value: M) => void = () => { this.forceUpdate(); }
): PSSubscription {
const subscription = model.subscribe(callback);
this.subscriptions.push(subscription);
return subscription;
}
override componentDidMount() {
if (PS.room === this.props.room) this.focus();
this.props.room.onParentEvent = (id: string, e?: Event) => {
if (id === 'focus') this.focus();
};
@ -171,7 +168,7 @@ export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ r
if (!args) this.forceUpdate();
else this.receiveLine(args);
}));
this.updateDimensions();
this.componentDidUpdate();
}
justUpdatedDimensions = false;
updateDimensions() {
@ -194,14 +191,11 @@ export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ r
}
override componentDidUpdate() {
const room = this.props.room;
if (this.base && ['popup', 'semimodal-popup'].includes(room.location)) {
if (room.width && room.hiddenInit) {
room.hiddenInit = false;
this.focus();
}
this.updateDimensions();
} else if (this.base && room.hiddenInit) {
room.hiddenInit = false;
const currentlyHidden = !room.width && room.parentElem && ['popup', 'semimodal-popup'].includes(room.location);
this.updateDimensions();
if (currentlyHidden) return;
if (room.focusNextUpdate) {
room.focusNextUpdate = false;
this.focus();
}
}
@ -259,49 +253,86 @@ export function PSPanelWrapper(props: {
}
return <div
id={`room-${room.id}`} class={'mini-window-contents ps-room-light' + (props.scrollable === true ? ' scrollable' : '')}
onClick={props.focusClick ? PSMain.focusIfNoSelection : undefined}
onClick={props.focusClick ? PSView.focusIfNoSelection : undefined}
>
{props.children}
</div>;
}
if (PS.isPopup(room)) {
const style = PSMain.getPopupStyle(room, props.width, props.fullSize);
const style = PSView.getPopupStyle(room, props.width, props.fullSize);
return <div class="ps-popup" id={`room-${room.id}`} style={style}>
{props.children}
</div>;
}
const style = PSMain.posStyle(room) as any;
const style = PSView.posStyle(room) as any;
if (props.scrollable === 'hidden') style.overflow = 'hidden';
return <div
class={'ps-room' + (room.id === '' ? '' : ' ps-room-light') + (props.scrollable === true ? ' scrollable' : '')}
id={`room-${room.id}`}
style={style} onClick={props.focusClick ? PSMain.focusIfNoSelection : undefined}
style={style} onClick={props.focusClick ? PSView.focusIfNoSelection : undefined}
>
{room.caughtError ? <div class="broadcast broadcast-red"><pre>{room.caughtError}</pre></div> : props.children}
</div>;
}
export class PSMain extends preact.Component {
export class PSView extends preact.Component {
static readonly isChrome = navigator.userAgent.includes(' Chrome/');
static readonly isSafari = !this.isChrome && navigator.userAgent.includes(' Safari/');
static readonly isMac = navigator.platform?.startsWith('Mac');
static textboxFocused = false;
static setTextboxFocused(focused: boolean) {
if (!PSMain.isChrome || PS.leftPanelWidth !== null) return;
if (!PSView.isChrome || PS.leftPanelWidth !== null) return;
// Chrome bug: on Android, it insistently scrolls everything leftmost when scroll snap is enabled
this.textboxFocused = focused;
if (focused) {
document.documentElement.classList.remove('scroll-snap-enabled');
PSMain.scrollToRoom();
PSView.scrollToRoom();
} else {
document.documentElement.classList.add('scroll-snap-enabled');
}
}
static focusPreview(room: PSRoom) {
if (room !== PS.room) return '';
const verticalBuf = this.verticalFocusPreview();
if (verticalBuf) return verticalBuf;
const isMiniRoom = PS.room.location === 'mini-window';
const { rooms, index } = PS.horizontalNav();
if (index === -1) return '';
let buf = ' ';
const leftRoom = PS.rooms[rooms[index - 1]];
if (leftRoom) buf += `\u2190 ${leftRoom.title}`;
buf += (PS.arrowKeysUsed || isMiniRoom ? " | " : " (use arrow keys) ");
const rightRoom = PS.rooms[rooms[index + 1]];
if (rightRoom) buf += `${rightRoom.title} \u2192`;
return buf;
}
static verticalFocusPreview() {
const { rooms, index } = PS.verticalNav();
if (index === -1) return '';
const upRoom = PS.rooms[rooms[index - 1]];
let downRoom = PS.rooms[rooms[index + 1]];
if (index === rooms.length - 2 && rooms[index + 1] === 'news') downRoom = undefined;
if (!upRoom && !downRoom) return '';
let buf = ' ';
// const altLabel = PSMain.isMac ? '⌥' : 'ᴀʟᴛ';
const altLabel = PSView.isMac ? 'ᴏᴘᴛ' : 'ᴀʟᴛ';
if (upRoom) buf += `${altLabel}\u2191 ${upRoom.title}`;
buf += " | ";
if (downRoom) buf += `${altLabel}\u2193 ${downRoom.title}`;
return buf;
}
constructor() {
super();
PS.subscribe(() => this.forceUpdate());
if (PSMain.isSafari) {
if (PSView.isSafari) {
// I don't want to prevent users from being able to zoom, but iOS Safari
// auto-zooms when focusing textboxes (unless the font size is 16px),
// and this apparently fixes it while still allowing zooming.
@ -401,7 +432,7 @@ export class PSMain extends preact.Component {
PS.update();
}
if (clickedRoom && !PS.isPopup(clickedRoom)) {
PSMain.scrollToRoom();
PSView.scrollToRoom();
}
});
@ -494,7 +525,7 @@ export class PSMain extends preact.Component {
}
static scrollToRoom() {
if (document.documentElement.scrollWidth > document.documentElement.clientWidth && window.scrollX === 0) {
if (PSMain.isSafari && PS.leftPanelWidth === null) {
if (PSView.isSafari && PS.leftPanelWidth === null) {
// Safari bug: `scrollBy` doesn't actually work when scroll snap is enabled
// note: interferes with the `PSMain.textboxFocused` workaround for a Chrome bug
document.documentElement.classList.remove('scroll-snap-enabled');
@ -600,7 +631,7 @@ export class PSMain extends preact.Component {
return { maxWidth: width || 480 };
}
if (!room.width || !room.height) {
room.hiddenInit = true;
room.focusNextUpdate = true;
return {
position: 'absolute',
visibility: 'hidden',

View File

@ -59,7 +59,7 @@
* Buttons
*********************************************************/
button, summary {
a, button, summary {
cursor: pointer;
touch-action: manipulation;
}

View File

@ -1006,6 +1006,9 @@ p.or:after {
.roomlist {
max-width: 480px;
text-align: left;
list-style: none;
margin: 0;
padding: 0;
}
.roomlist .subrooms {
font-size: 8pt;
@ -1344,7 +1347,6 @@ a.ilink.yours {
.tournament-bracket-tree-node text, .tournament-bracket-tree-node {
font-size: 8.5pt;
text-anchor: middle;
dominant-baseline: central;
font-weight: bold;
}
.tournament-bracket-tree-node a {
@ -2131,6 +2133,7 @@ a.ilink.yours {
width: 16px;
color: #777777;
}
.folderpane .text,
.folderpane h3 {
margin: 0;
padding: 13px 0 0 0;
@ -2140,6 +2143,10 @@ a.ilink.yours {
color: black;
border-right: 1px solid #888888;
}
.folderpane .text {
height: 23px;
padding: 7px 0 0 7px;
}
.folder .selectFolder {
display: block;
padding: 0 0 0 7px;
@ -2185,6 +2192,7 @@ a.ilink.yours {
padding-left: 6px;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
text-decoration: none;
}
/* believe me, there was no other way to do this */
@ -3215,6 +3223,7 @@ a.ilink.yours {
.dark .folderlistafter:before,
.dark .folderlistbefore:before,
.dark .folderpane h3,
.dark .folderpane .text,
.dark .folder .selectFolder,
.dark .folderhack1,
.dark .folderhack2 {

View File

@ -1228,6 +1228,7 @@ pre.textbox.textbox-empty[placeholder]:before {
width: 100%;
text-align: left;
overflow: hidden;
white-space: nowrap;
}
.tournament-title:hover {
background: rgba(242, 247, 250, 0.85);
@ -1249,6 +1250,7 @@ pre.textbox.textbox-empty[placeholder]:before {
line-height: 30px;
font-weight: normal;
display: inline-block;
font-size: 11px;
}
.tournament-box {
@ -1375,7 +1377,6 @@ pre.textbox.textbox-empty[placeholder]:before {
.tournament-bracket-tree-node text, .tournament-bracket-tree-node {
font-size: 8.5pt;
text-anchor: middle;
dominant-baseline: central;
font-weight: bold;
}
.tournament-bracket-tree-node a {
@ -2331,104 +2332,3 @@ pre.textbox.textbox-empty[placeholder]:before {
.dark .chat.mine {
background: rgba(255,255,255,0.05);
}
/* teambuilder */
.dark .folderpane {
border-left-color: #484848;
}
.dark .folderlist .foldersep:before,
.dark .folderlistafter:before,
.dark .folderlistbefore:before,
.dark .folderpane h3,
.dark .folder .selectFolder,
.dark .folderhack1,
.dark .folderhack2 {
background: #484848;
color: #ddd;
}
.dark .folder .selectFolder:hover {
background: #686868;
}
.dark .folderpane i {
color: #ddd;
}
.dark .folder .selectFolder.active,
.dark .folder .selectFolder:active {
background: #27333c;
color: white;
}
.dark .folder.cur .selectFolder {
background: transparent;
}
/* teambuilder set */
.dark .utilichart h3, .dark .dexentry h3, .dark .resultheader h3 {
background: #636363;
color: #F1F1F1;
border: 1px solid #A9A9A9;
text-shadow: 1px 1px 0 rgb(40, 43, 45);
box-shadow: inset 0px 1px 0 rgb(49, 49, 49);
border-right: none;
}
.dark .teambar button {
background: #5A6570;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:hover {
background: #444C54;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:disabled, .dark .teambar button:disabled:hover, .dark .teambar button:disabled:active {
color: #ffffff;
background: #2d343a;
border-color: #6bacc5;
opacity: 1;
}
.dark .teambuilder-results .result a.hover,
.dark .teambuilder-results .result a:hover,
.dark .setmenu button:hover,
.dark .teamlist button:hover {
border-color: #777777;
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
}
.dark .teambuilder-results .result a.cur {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.2);
}
.dark .teambuilder-results .result a.cur:hover {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.4);
color: #FFFFFF;
}
.dark .setmenu button,
.dark .teamlist button,
.dark .folder.cur .selectFolder,
.dark .utilichart .namecol,
.dark .utilichart .pokemonnamecol,
.dark .utilichart .movenamecol {
color: #DDD;
}
.dark .utilichart .col {
color: #DDD;
}
.dark .utilichart .cur .col {
color: #FFF;
}
.dark .utilichart a:hover .col {
color: #FFF;
}

View File

@ -88,6 +88,7 @@
width: 16px;
color: #777777;
}
.folderpane .text,
.folderpane h3 {
margin: 0;
padding: 13px 0 0 0;
@ -97,6 +98,10 @@
color: black;
border-right: 1px solid #888888;
}
.folderpane .text {
height: 23px;
padding: 7px 0 0 7px;
}
.folder .selectFolder {
display: block;
padding: 0 0 0 7px;
@ -199,6 +204,41 @@
margin-right: 2px;
}
/* dark */
.dark .folderpane {
border-left-color: #484848;
}
.dark .folderlist .foldersep:before,
.dark .folderlistafter:before,
.dark .folderlistbefore:before,
.dark .folderpane h3,
.dark .folderpane .text,
.dark .folder .selectFolder,
.dark .folderhack1,
.dark .folderhack2 {
background: #484848;
color: #ddd;
}
.dark .folder .selectFolder:hover {
background: #686868;
}
.dark .folderpane i {
color: #ddd;
}
.dark .folder .selectFolder.active,
.dark .folder .selectFolder:active {
background: #27333c;
color: white;
}
.dark .folder.cur .selectFolder {
background: transparent;
}
/*********************************************************
* Teambuilder editor
@ -271,3 +311,69 @@
left: 10px;
right: 10px;
}
/* teambuilder set */
.dark .utilichart h3, .dark .dexentry h3, .dark .resultheader h3 {
background: #636363;
color: #F1F1F1;
border: 1px solid #A9A9A9;
text-shadow: 1px 1px 0 rgb(40, 43, 45);
box-shadow: inset 0px 1px 0 rgb(49, 49, 49);
border-right: none;
}
.dark .teambar button {
background: #5A6570;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:hover {
background: #444C54;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:disabled, .dark .teambar button:disabled:hover, .dark .teambar button:disabled:active {
color: #ffffff;
background: #2d343a;
border-color: #6bacc5;
opacity: 1;
}
.dark .teambuilder-results .result a.hover,
.dark .teambuilder-results .result a:hover,
.dark .setmenu button:hover,
.dark .teamlist button:hover {
border-color: #777777;
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
}
.dark .teambuilder-results .result a.cur {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.2);
}
.dark .teambuilder-results .result a.cur:hover {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.4);
color: #FFFFFF;
}
.dark .setmenu button,
.dark .teamlist button,
.dark .folder.cur .selectFolder,
.dark .utilichart .namecol,
.dark .utilichart .pokemonnamecol,
.dark .utilichart .movenamecol {
color: #DDD;
}
.dark .utilichart .col {
color: #DDD;
}
.dark .utilichart .cur .col {
color: #FFF;
}
.dark .utilichart a:hover .col {
color: #FFF;
}

View File

@ -121,10 +121,16 @@
<script src="js/panel-mainmenu.js"></script>
<script src="js/panel-rooms.js"></script>
<script src="js/panel-topbar.js"></script>
<script src="js/panel-popups.js"></script>
<!-- at this point, the main view is loaded and usable -->
<script src="js/miniedit.js"></script>
<script src="js/panel-chat-tournament.js"></script>
<script src="js/panel-chat.js"></script>
<!-- at this point, chatrooms are usable -->
<script src="js/panel-popups.js"></script>
<script src="js/panel-page.js?"></script>
<script src="js/panel-ladder.js?"></script>
<script src="js/battle-sound.js"></script>
<script src="js/lib/jquery-2.2.4.min.js"></script>
@ -146,11 +152,11 @@
<script src="js/battle-dex-search.js?"></script>
<script src="js/battle-searchresults.js?"></script>
<script src="js/panel-teambuilder-team.js?"></script>
<script src="js/panel-ladder.js?"></script>
<script src="js/panel-page.js?"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini.js"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini-bw.js"></script>
<script src="js/lib/d3.v3.min.js"></script>
<script src="js/client-endload.js?"></script>
</body></html>

View File

@ -422,7 +422,7 @@ export class BattlePanel extends preact.Component<{ id: string }> {
</form>
<p>
<em>Pro tip:</em> You don't need to click "Skip to turn" if you have a keyboard, just start typing
the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd>
the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd> {}
when a text box isn't focused.
</p>
</section></div>;