diff --git a/index.html b/index.html index cac5dc56..9cfa61ff 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@ + diff --git a/lib/chat.js b/lib/chat.js index 38d94b2e..3b964ac7 100644 --- a/lib/chat.js +++ b/lib/chat.js @@ -1,6 +1,9 @@ +//@ts-check const { LitElement, html, css } = require('lit') const { isMobile } = require('./menus/components/common') const { activeModalStack, hideCurrentModal } = require('./globalState') +import { repeat } from 'lit/directives/repeat.js' +import { classMap } from 'lit/directives/class-map.js' const styles = { black: 'color:#000000', @@ -36,6 +39,11 @@ function colorShadow (hex, dim = 0.25) { return `#${f(r)}${f(g)}${f(b)}` } +/** + * @typedef {{text;color?;italic?;underlined?;strikethrough?;bold?}} MessagePart + * @typedef {{parts: MessagePart[], id, fading?, faded}} Message + */ + class ChatBox extends LitElement { static get styles () { return css` @@ -44,7 +52,7 @@ class ChatBox extends LitElement { z-index: 10; } - .chat-display-wrapper { + .chat-messages-wrapper { bottom: 40px; padding: 4px; padding-left: 0; @@ -96,7 +104,7 @@ class ChatBox extends LitElement { } .chat-message { - display: block; + display: flex; padding-left: 4px; background-color: rgba(0, 0, 0, 0.5); } @@ -114,40 +122,51 @@ class ChatBox extends LitElement { transition: none !important; } - .chat-message-chat-opened { + .chat.opened .chat-message { opacity: 1 !important; transition: none !important; } + + .chat-message-part { + white-space: pre-wrap; + } ` } - render () { - return html` -
-
-
  • Welcome to prismarine-web-client! Chat appears here.
  • -
    -
    -
    -
    - -
    -
    - ` + static get properties () { + return { + messages: { + type: Array + } + } } constructor () { super() this.chatHistoryPos = 0 this.chatHistory = [] + this.messagesLimit = 200 + /** @type {Message[]} */ + this.messages = [{ + parts: [ + { + text: 'Welcome to prismarine-web-client! Chat appears here.', + } + ], + id: 0, + fading: true, + faded: true, + }] } - enableChat (isCommand) { - const chat = this.shadowRoot.querySelector('#chat') - const chatInput = this.shadowRoot.querySelector('#chatinput') + enableChat (initialText = '') { + const chat = this.shadowRoot.getElementById('chat-messages') + /** @type {HTMLInputElement} */ + // @ts-ignore + const chatInput = this.shadowRoot.getElementById('chatinput') - this.shadowRoot.querySelector('#chat-wrapper2').classList.toggle('input-mobile', isMobile()) - this.shadowRoot.querySelector('#chat-wrapper').classList.toggle('display-mobile', isMobile()) + this.shadowRoot.getElementById('chat-wrapper2').classList.toggle('input-mobile', isMobile()) + this.shadowRoot.getElementById('chat-wrapper').classList.toggle('display-mobile', isMobile()) activeModalStack.push(this) @@ -158,13 +177,13 @@ class ChatBox extends LitElement { // Show extended chat history chat.style.maxHeight = 'var(--chatHeight)' chat.scrollTop = chat.scrollHeight // Stay bottom of the list - if (isCommand) { // handle commands - chatInput.value = '/' - } + // handle / and other snippets + chatInput.value = initialText // Focus element chatInput.focus() this.chatHistoryPos = this.chatHistory.length - document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.add('chat-message-chat-opened')) + // to show + this.requestUpdate() } get inChat () { @@ -175,8 +194,10 @@ class ChatBox extends LitElement { * @param {import('minecraft-protocol').Client} client */ init (client) { - const chat = this.shadowRoot.querySelector('#chat') - const chatInput = this.shadowRoot.querySelector('#chatinput') + const chat = this.shadowRoot.getElementById('chat-messages') + /** @type {HTMLInputElement} */ + // @ts-ignore + const chatInput = this.shadowRoot.getElementById('chatinput') // Show chat chat.style.display = 'block' @@ -184,7 +205,6 @@ class ChatBox extends LitElement { // Chat events document.addEventListener('keydown', e => { if (activeModalStack.slice(-1)[0] !== this) return - e = e || window.event if (e.code === 'ArrowUp') { if (this.chatHistoryPos === 0) return chatInput.value = this.chatHistory[--this.chatHistoryPos] !== undefined ? this.chatHistory[this.chatHistoryPos] : '' @@ -199,16 +219,15 @@ class ChatBox extends LitElement { const keyBindScrn = document.getElementById('keybinds-screen') document.addEventListener('keypress', e => { - e = e || window.event if (!this.inChat && activeModalStack.length === 0) { keyBindScrn.keymaps.forEach(km => { if (e.code === km.key) { switch (km.defaultKey) { case 'KeyT': - setTimeout(() => this.enableChat(false), 0) + setTimeout(() => this.enableChat(), 0) break case 'Slash': - setTimeout(() => this.enableChat(true), 0) + setTimeout(() => this.enableChat('/'), 0) break } } @@ -238,21 +257,18 @@ class ChatBox extends LitElement { // Hide extended chat history chat.style.maxHeight = 'var(--chatHeight)' chat.scrollTop = chat.scrollHeight // Stay bottom of the list - document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.remove('chat-message-chat-opened')) + this.requestUpdate() return 'custom' // custom hide } this.hide() client.on('chat', (packet) => { - // Reading of chat message + // Handle new message const fullmessage = JSON.parse(packet.message.toString()) + /** @type {MessagePart[]} */ const msglist = [] - const colorF = (color) => { - return color.trim().startsWith('#') ? `color:${color}` : styles[color] ?? undefined - } - - const readMsg = (msglist, msg) => { + const readMsg = (msg) => { const styles = { color: msg.color, bold: !!msg.bold, @@ -264,30 +280,29 @@ class ChatBox extends LitElement { if (msg.text) { msglist.push({ + ...msg, text: msg.text, ...styles }) - } - - if (msg.translate) { + } else if (msg.translate) { const tText = window.mcData.language[msg.translate] ?? msg.translate if (msg.with) { - const splited = tText.split(/%s|%\d+\$s/g) + const splitted = tText.split(/%s|%\d+\$s/g) let i = 0 - splited.forEach((spl, j, arr) => { - msglist.push({ text: spl, ...styles }) + splitted.forEach((part, j) => { + msglist.push({ text: part, ...styles }) - if (j + 1 < arr.length) { + if (j + 1 < splitted.length) { if (msg.with[i]) { if (typeof msg.with[i] === 'string') { - readMsg(msglist, { + readMsg({ ...styles, text: msg.with[i] }) } else { - readMsg(msglist, { + readMsg({ ...styles, ...msg.with[i] }) @@ -298,6 +313,7 @@ class ChatBox extends LitElement { }) } else { msglist.push({ + ...msg, text: tText, ...styles }) @@ -306,40 +322,95 @@ class ChatBox extends LitElement { if (msg.extra) { msg.extra.forEach(ex => { - readMsg(msglist, { ...styles, ...ex }) + readMsg({ ...styles, ...ex }) }) } } - readMsg(msglist, fullmessage) + readMsg(fullmessage) + + const lastId = this.messages.at(-1)?.id ?? 0 + this.messages = [...this.messages.slice(-this.messagesLimit), { + parts: msglist, + id: lastId + 1, + fading: false, + faded: false + }] + const message = this.messages.at(-1) - const li = document.createElement('li') - msglist.forEach(msg => { - const span = document.createElement('span') - span.appendChild(document.createTextNode(msg.text)) - span.setAttribute( - 'style', - `${msg.color ? colorF(msg.color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${colorShadow(colorF(msg.color.toLowerCase()).replace('color:', ''))}` : styles.white}; ${msg.bold ? styles.bold + ';' : '' - }${msg.italic ? styles.italic + ';' : ''}${msg.strikethrough ? styles.strikethrough + ';' : '' - }${msg.underlined ? styles.underlined + ';' : ''}` - ) - li.appendChild(span) - }) - chat.appendChild(li) chat.scrollTop = chat.scrollHeight // Stay bottom of the list // fading - li.classList.add('chat-message') - if (this.inChat) { - li.classList.add('chat-message-chat-opened') - } setTimeout(() => { - li.classList.add('chat-message-fadeout') - li.classList.add('chat-message-fade') + message.fading = true + this.requestUpdate() setTimeout(() => { - li.classList.add('chat-message-faded') + message.faded = true + this.requestUpdate() }, 3000) }, 5000) }) + // todo support hover content below, {action: 'show_text', contents: {text}}, and some other types + // todo remove + window.dummyMessage = () => { + client.emit('chat', { + message: "{\"color\":\"yellow\",\"translate\":\"multiplayer.player.joined\",\"with\":[{\"insertion\":\"pviewer672\",\"clickEvent\":{\"action\":\"suggest_command\",\"value\":\"/tell pviewer672 \"},\"hoverEvent\":{\"action\":\"show_entity\",\"contents\":{\"type\":\"minecraft:player\",\"id\":\"ecd0eeb1-625e-3fea-b16e-cb449dcfa434\",\"name\":{\"text\":\"pviewer672\"}}},\"text\":\"pviewer672\"}]}", + position: 1, + sender: "00000000-0000-0000-0000-000000000000", + }) + } + // window.dummyMessage() + } + + renderMessagePart (/** @type {MessagePart} */{ bold, color, italic, strikethrough, text, underlined }) { + const colorF = (color) => { + return color.trim().startsWith('#') ? `color:${color}` : styles[color] ?? undefined + } + + /** @type {string[]} */ + const applyStyles = [ + color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${colorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : styles.white, + italic && styles.italic, + bold && styles.bold, + italic && styles.italic, + underlined && styles.underlined, + strikethrough && styles.strikethrough + ].filter(Boolean) + + return html` + ${text}` + } + + renderMessage (/** @type {Message} */message) { + const classes = { + 'chat-message-fadeout': message.fading, + 'chat-message-fade': message.fading, + 'chat-message-faded': message.faded, + 'chat-message': true + } + + return html` +
  • + ${message.parts.map(msg => this.renderMessagePart(msg))} +
  • + ` + } + + render () { + return html` +
    +
    + ${repeat(this.messages, (m) => m.id, (m) => this.renderMessage(m))} +
    +
    +
    +
    + +
    +
    + ` } } diff --git a/lib/globalState.js b/lib/globalState.js index afcf67d7..8cfa4b73 100644 --- a/lib/globalState.js +++ b/lib/globalState.js @@ -1,11 +1,13 @@ //@ts-check +import { proxy } from 'valtio' import { pointerLock } from './utils' // todo: refactor structure with support of hideNext=false /** * @typedef {(HTMLElement & Record)} Modal + * @typedef {{callback, label}} ContextMenuItem */ /** @type {Modal[]} */ @@ -74,3 +76,15 @@ export const hideCurrentModal = (_data = undefined, preActions = undefined) => { } } } + +// --- + +export const currentContextMenu = proxy({ items: /** @type {ContextMenuItem[] | null} */[], x: 0, y: 0, }) + +export const showContextmenu = (/** @type {ContextMenuItem[]} */items, { clientX, clientY }) => { + Object.assign(currentContextMenu, { + items, + x: clientX, + y: clientY, + }) +} diff --git a/lib/menus/hud.js b/lib/menus/hud.js index 446a07c4..3c896c5a 100644 --- a/lib/menus/hud.js +++ b/lib/menus/hud.js @@ -305,7 +305,7 @@ class Hud extends LitElement {