瀏覽代碼

Compartmentalize TextBox functionality

master
Roderic Day 5 年之前
父節點
當前提交
842e04a3c7
共有 2 個檔案被更改,包括 87 行新增71 行删除
  1. +1
    -1
      pico.css
  2. +86
    -70
      pico.js

+ 1
- 1
pico.css 查看文件

position: relative; position: relative;
} }
#textbox { #textbox {
resize: vertical;
resize: none;
} }
.post > div { .post > div {
display: inline; display: inline;

+ 86
- 70
pico.js 查看文件

const scrollIntoView = (vnode) => { const scrollIntoView = (vnode) => {
vnode.dom.scrollIntoView() vnode.dom.scrollIntoView()
} }
const toggleFullscreen = (el) => (event) => {
const requestFullscreen = (el.requestFullscreen || el.webkitRequestFullscreen).bind(el)
document.fullscreenElement ? document.exitFullscreen() : requestFullscreen()
}
const prettyTime = (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}`
}
const blockIndent = (text, iStart, iEnd, nLevels) => {
// prop
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(/^ /, '')
}
const TextBox = {
autoSize: () => {
textbox.rows = textbox.value.split('\n').length
},
sendPost: () => {
if(textbox.value) {
wire({kind: 'post', value: textbox.value})
textbox.value = ''
textbox.focus()
}
},
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)]
}
const hotKey = (e) => {
// if isDesktop, Enter posts, unless Shift+Enter
// use isLandscape as proxy for isDesktop
if(e.key === 'Enter' && isLandscape && !e.shiftKey) {
e.preventDefault()
Chat.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] = blockIndent(text, A, B, nLevels)
textbox.value = newText
textbox.setSelectionRange(newA, newB)
}
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
// use isLandscape as proxy for isDesktop
if(e.key === 'Enter' && isLandscape && !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,
onkeyup: TextBox.autoSize,
}),
m('button', {
onclick: ({target}) => {
TextBox.sendPost()
TextBox.autoSize()
},
},
'Send'),
)
},
} }
const VideoOptions = { const VideoOptions = {
available: ['mirror', 'square', 'full-screen'], available: ['mirror', 'square', 'full-screen'],
dom.autoplay = true dom.autoplay = true
dom.muted = (username === State.username) dom.muted = (username === State.username)
dom.srcObject = stream dom.srcObject = stream
dom.ondblclick = toggleFullscreen(dom)
}, },
view({attrs}) { view({attrs}) {
const classList = VideoOptions.getClassListFor(attrs.username) const classList = VideoOptions.getClassListFor(attrs.username)
}, },
} }
const Chat = { const Chat = {
sendPost: () => {
if(textbox.value) {
wire({kind: 'post', value: textbox.value})
textbox.value = ''
}
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}`
}, },
view() { view() {
return m('.chat', return m('.chat',
m('.posts', m('.posts',
State.posts.map(post => m('.post', {oncreate: scrollIntoView}, State.posts.map(post => m('.post', {oncreate: scrollIntoView},
m('.ts', prettyTime(post.ts)),
m('.ts', Chat.prettifyTime(post.ts)),
m('.source', post.source || '~'), m('.source', post.source || '~'),
m('.text', m.trust(DOMPurify.sanitize(marked(post.value)))), m('.text', m.trust(DOMPurify.sanitize(marked(post.value)))),
)), )),
), ),
m('.actions',
m('textarea#textbox', {oncreate: autoFocus, onkeydown: hotKey}),
m('button', {onclick: Chat.sendPost}, 'Send'),
),
m(TextBox),
m('.online', m('.online',
m('button', {onclick: Login.sendLogout}, 'Logout'), m('button', {onclick: Login.sendLogout}, 'Logout'),
m('.user-list', State.online.map(username => m('.user-list', State.online.map(username =>

Loading…
取消
儲存