瀏覽代碼

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 查看文件

@@ -49,7 +49,7 @@ body {
position: relative;
}
#textbox {
resize: vertical;
resize: none;
}
.post > div {
display: inline;

+ 86
- 70
pico.js 查看文件

@@ -120,66 +120,85 @@ const autoFocus = (vnode) => {
const scrollIntoView = (vnode) => {
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 = {
available: ['mirror', 'square', 'full-screen'],
@@ -215,7 +234,6 @@ const Video = {
dom.autoplay = true
dom.muted = (username === State.username)
dom.srcObject = stream
dom.ondblclick = toggleFullscreen(dom)
},
view({attrs}) {
const classList = VideoOptions.getClassListFor(attrs.username)
@@ -330,25 +348,23 @@ const Login = {
},
}
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() {
return m('.chat',
m('.posts',
State.posts.map(post => m('.post', {oncreate: scrollIntoView},
m('.ts', prettyTime(post.ts)),
m('.ts', Chat.prettifyTime(post.ts)),
m('.source', post.source || '~'),
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('button', {onclick: Login.sendLogout}, 'Logout'),
m('.user-list', State.online.map(username =>

Loading…
取消
儲存