Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions client/src/comps/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@ import Form from "./forms/Form"
import View from "./views/View"
import Modal from "./Modal"

/**
* Keyboard- and layout-independent key codes that trigger autofocus
*
* Keys starting with "Key" or "Digit" don't need to be listed explictly.
* AltGraph is layout-dependent and must not be listed.
* Numpad number keys and NumpadDecimal are dependent on Numpad key state and must not be listed.
*/
const autofocusCodes = [
// Row 1 (top)
"Backquote",
"Minus",
"Equal",
"Backspace",
// Row 2
"BracketLeft",
"BracketRight",
"Enter",
// Row 3
"CapsLock",
"Semicolon",
"Quote",
"Backslash",
// Row 4
"ShiftLeft",
"IntlBackslash",
"Period",
"Comma",
"Slash",
// Row 5
"Space",
// Numpad
"NumpadDivide",
"NumpadMultiply",
"NumpadSubtract",
"NumpadAdd",
]

export default class App extends Component {
readonly roomList: RoomList
private currentView?: View | Form
Expand All @@ -27,6 +64,30 @@ export default class App extends Component {
})

void initMatrixClient()

document.addEventListener("keydown", event => {
const code = event.code // keyboard-/layout-independent key code
const key = event.key // key (respects layout, modifiers, and numpad key)
// console.log("keydown event fired on", event.target, "for key", code, key)
// console.log("modifiers:", ["ctrl", "shift", "alt", "meta"].filter(mod => event[mod + "Key"]).join(", "))

if (event.altKey || event.ctrlKey || event.metaKey) {
// console.log("modifier detected, skip")
return
}

if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || (event.target instanceof HTMLDivElement && event.target.hasAttribute("contenteditable"))) {
// console.log("is input, skip")
return
}

if (!code.startsWith("Key") && !code.startsWith("Digit") && !autofocusCodes.includes(code) && key != "AltGraph" && isNaN(Number(key)) && key != ",") {
// console.log("key doesn't trigger autofocus, skip")
Comment thread
cfpwastaken marked this conversation as resolved.
return
}

this.autofocus(true)
})
}

updateChannelList() {
Expand All @@ -40,6 +101,8 @@ export default class App extends Component {
void view.reset(...args)
this.element.appendChild(view.element)
this.currentView = view

this.autofocus()
}

clearView() {
Expand All @@ -63,6 +126,8 @@ export default class App extends Component {
const element = modal.element as HTMLDialogElement
document.body.appendChild(element)
element.showModal()

modal.form.defaultTextInput?.focus()
}

closeModal(modal: Modal) {
Expand All @@ -86,4 +151,15 @@ export default class App extends Component {
getCurrentModal(): Modal | undefined {
return this.modals[this.modals.length - 1]
}

autofocus(textOnly: boolean = false) {
const modal = this.getCurrentModal()
const getter = textOnly ? "defaultInput" : "defaultTextInput"

if (modal) {
modal.form[getter]?.focus()
} else {
this.currentView?.[getter]?.focus()
}
}
}
25 changes: 13 additions & 12 deletions client/src/comps/ChatInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import RoomView from "./views/RoomView"
// Credits to DeepSeek-R1, wow (edited though)
export default class ChatInput extends Component {
readonly emojiPicker: EmojiPicker
readonly input: HTMLDivElement

constructor(view: PostView | RoomView) {
super("div", { id: "chat-input-container" })
Expand All @@ -23,16 +24,16 @@ export default class ChatInput extends Component {
fileUploadLabel.appendChild(fileInput)

// Create chat input
const chatInput = document.createElement("div")
chatInput.className = "chat-input"
chatInput.setAttribute("contenteditable", "true")
chatInput.setAttribute("placeholder", "Message #channel")
chatInput.addEventListener("keypress", e => {
this.input = document.createElement("div")
this.input.className = "chat-input"
this.input.setAttribute("contenteditable", "true")
this.input.setAttribute("placeholder", "Message #channel")
this.input.addEventListener("keypress", e => {
if (e.code == "Enter" && !e.shiftKey) {
e.preventDefault()
const content = chatInput.innerText.trim()
const content = this.input.innerText.trim()
if (content == "") return
chatInput.innerHTML = ""
this.input.innerHTML = ""

async function createMessage() {
// const forum_id = view.getCurrentForumId()
Expand All @@ -57,13 +58,13 @@ export default class ChatInput extends Component {
})
}

console.log("Send MSG:", chatInput.innerText)
console.log("Send MSG:", this.input.innerText)
void createMessage()
}
})
chatInput.addEventListener("input", () => {
this.input.addEventListener("input", () => {
// this fixes weird browser behavior
if (chatInput.innerHTML == "<br>") chatInput.innerHTML = ""
if (this.input.innerHTML == "<br>") this.input.innerHTML = ""
})

// Create emoji button
Expand All @@ -77,13 +78,13 @@ export default class ChatInput extends Component {
"chat-input-emoji-picker",
"chat-input-container",
emoji => {
chatInput.textContent += emoji.native
this.input.textContent += emoji.native
}
)

// Div-engers, Assemble!
this.element.appendChild(fileUploadLabel)
this.element.appendChild(chatInput)
this.element.appendChild(this.input)
this.element.appendChild(emojiButton)
this.element.appendChild(this.emojiPicker.element)
}
Expand Down
24 changes: 24 additions & 0 deletions client/src/comps/forms/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,28 @@ export default abstract class Form<
const modal = new Modal(this)
app.openModal(modal)
}

get defaultTextInput(): HTMLElement | undefined {
const form = this.element as HTMLFormElement
return Array.from(form.elements).find(isTextInput)
}

get defaultInput(): HTMLElement | undefined {
const form = this.element as HTMLFormElement
return Array.from(form.elements).find(isFormInput)
}
}

function isFormInput(formControl: Element): formControl is HTMLElement {
if (formControl instanceof HTMLFieldSetElement) return false
if (formControl instanceof HTMLOutputElement) return false
return true
}

function isTextInput(formControl: Element): formControl is HTMLElement {
if (formControl instanceof HTMLTextAreaElement) return true
if (formControl instanceof HTMLInputElement) {
return ["text", "number", "email", "password", "search", "tel", "url"].includes(formControl.type)
}
return false
}
4 changes: 4 additions & 0 deletions client/src/comps/views/PostView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ export default class PostView extends View<[string]> {
getCurrentForumId() {
return this.currentForumId
}

get defaultTextInput() {
return this.chatInput.input
}
}
4 changes: 4 additions & 0 deletions client/src/comps/views/RoomView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,8 @@ export default class RoomView extends View {
getCurrentRoom() {
return this.currentRoom
}

get defaultTextInput() {
return this.chatInput.input
}
}
6 changes: 6 additions & 0 deletions client/src/comps/views/View.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ export default abstract class View<
}

abstract reset(...args: ResetArgs): void | Promise<void>

abstract defaultTextInput: HTMLElement | undefined

get defaultInput() {
return this.defaultTextInput
}
}