|
- const TextBox = {
- autoSize: () => {
- textbox.style.height = `0px`
- textbox.style.height = `${textbox.scrollHeight}px`
- },
- sendPost: () => {
- if(textbox.value) {
- wire({kind: 'post', value: textbox.value})
- textbox.value = ''
- textbox.focus()
- TextBox.autoSize()
- }
- },
- blockIndent: (text, iStart, iEnd, nLevels) => {
- const startLine = text.slice(0, iStart).split('\n').length - 1
- const endLine = text.slice(0, iEnd).split('\n').length - 1
- const newText = text
- .split('\n')
- .map((line, i) => {
- if(i < startLine || i > endLine || nLevels === 0) {
- newLine = line
- }
- else if(nLevels > 0) {
- newLine = line.replace(/^/, ' ')
- }
- else if(nLevels < 0) {
- newLine = line.replace(/^ /, '')
- }
-
- if(i === startLine) {
- iStart = iStart + newLine.length - line.length
- }
- iEnd = iEnd + newLine.length - line.length
- return newLine
- })
- .join('\n')
- return [newText, Math.max(0, iStart), Math.max(0, iEnd)]
- },
- hotKey: (e) => {
- // if isDesktop, Enter posts, unless Shift+Enter
- isDesktop = true
- if(e.key === 'Enter' && isDesktop && !e.shiftKey) {
- e.preventDefault()
- TextBox.sendPost()
- }
- // indent and dedent
- const modKey = e.ctrlKey || e.metaKey
- const {value: text, selectionStart: A, selectionEnd: B} = textbox
- if(e.key === 'Tab') {
- e.preventDefault()
- const regex = new RegExp(`([\\s\\S]{${A}})([\\s\\S]{${B - A}})`)
- textbox.value = text.replace(regex, (m, a, b) => a + ' '.repeat(4))
- textbox.setSelectionRange(A + 4, A + 4)
- }
- if(']['.includes(e.key) && modKey) {
- e.preventDefault()
- const nLevels = {']': 1, '[': -1}[e.key]
- const [newText, newA, newB] = TextBox.blockIndent(text, A, B, nLevels)
- textbox.value = newText
- textbox.setSelectionRange(newA, newB)
- }
- },
- view() {
- return m('.actions',
- m('textarea#textbox', {
- oncreate: (vnode) => {
- TextBox.autoSize()
- autoFocus(vnode)
- },
- onkeydown: TextBox.hotKey,
- oninput: TextBox.autoSize,
- }),
- m('button', {onclick: TextBox.sendPost}, 'Send'),
- )
- },
- }
- const Post = {
- oncreate: ({dom}) => {
- dom.scrollIntoView()
- },
- prettifyTime: (ts) => {
- const dt = new Date(ts)
- const H = `0${dt.getHours()}`.slice(-2)
- const M = `0${dt.getMinutes()}`.slice(-2)
- const S = `0${dt.getSeconds()}`.slice(-2)
- return `${H}:${M}:${S}`
- },
- fixPost: ({dom}) => {
- dom.querySelectorAll('a').forEach(anchor => {
- anchor.target = '_blank'
- anchor.rel = 'noopener'
- })
- },
- view({attrs: {post}}) {
- return m('.post',
- m('.ts', this.prettifyTime(post.ts)),
- m('.source', post.source || '~'),
- m('.text', {oncreate: this.fixPost}, m.trust(DOMPurify.sanitize(marked(post.value)))),
- )
- }
- }
- const ChatConfig = {
- isOn: false,
- view() {
- return m('button', {onclick: () => {ChatConfig.isOn = !ChatConfig.isOn}}, 'chat')
- }
- }
- const Chat = {
- posts: [],
- oncreate() {
- listen('post', ({detail}) => {
- this.posts.push(detail)
- m.redraw()
- })
- listen('logout', () => {
- this.posts = []
- })
- marked.setOptions({
- breaks: true,
- })
- },
- view() {
- return ChatConfig.isOn ? m('.chat',
- m('.posts', this.posts.map(post => m(Post, {post}))),
- m(TextBox),
- ) : null
- },
- }
|