|
- 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 = {
- view() {
- const onclick = () => {Chat.isOn = !Chat.isOn}
- const hot = Chat.unseenCount ? '.hot' : ''
- return m('button', {onclick}, 'chat ',
- m('.badge' + hot, Chat.unseenCount),
- )
- },
- }
- const Chat = {
- posts: [],
- unseenCount: 0,
- originalTitle: 'pico.chat',
- onupdate: () => {
- if(document.hasFocus() && Chat.isOn) {
- Chat.unseenCount = 0
- textbox.focus()
- }
- const extra = Chat.unseenCount ? ` (${Chat.unseenCount})` : ``
- document.title = Chat.originalTitle + extra
- },
- view() {
- return m('.chat',
- m('.posts', Chat.posts.map(post => m(Post, {post}))),
- m(TextBox),
- )
- },
- }
- addEventListener('focus', m.redraw)
- addEventListener('post', ({detail}) => {
- Chat.posts.push(detail)
- Chat.unseenCount += !(document.hasFocus() && ChatConfig.isOn)
- m.redraw()
- })
- addEventListener('logout', () => {
- Chat.posts = []
- Chat.unseenCount = 0
- })
- marked.setOptions({
- breaks: true,
- })
|