Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

148 lines
4.5KB

  1. const TextBox = {
  2. autoSize: () => {
  3. textbox.style.height = `0px`
  4. textbox.style.height = `${textbox.scrollHeight}px`
  5. },
  6. sendPost: () => {
  7. if(textbox.value) {
  8. wire({kind: 'post', value: textbox.value})
  9. textbox.value = ''
  10. textbox.focus()
  11. TextBox.autoSize()
  12. }
  13. },
  14. blockIndent: (text, iStart, iEnd, nLevels) => {
  15. const startLine = text.slice(0, iStart).split('\n').length - 1
  16. const endLine = text.slice(0, iEnd).split('\n').length - 1
  17. const newText = text
  18. .split('\n')
  19. .map((line, i) => {
  20. if(i < startLine || i > endLine || nLevels === 0) {
  21. newLine = line
  22. }
  23. else if(nLevels > 0) {
  24. newLine = line.replace(/^/, ' ')
  25. }
  26. else if(nLevels < 0) {
  27. newLine = line.replace(/^ /, '')
  28. }
  29. if(i === startLine) {
  30. iStart = iStart + newLine.length - line.length
  31. }
  32. iEnd = iEnd + newLine.length - line.length
  33. return newLine
  34. })
  35. .join('\n')
  36. return [newText, Math.max(0, iStart), Math.max(0, iEnd)]
  37. },
  38. hotKey: (e) => {
  39. // if isDesktop, Enter posts, unless Shift+Enter
  40. isDesktop = true
  41. if(e.key === 'Enter' && isDesktop && !e.shiftKey) {
  42. e.preventDefault()
  43. TextBox.sendPost()
  44. }
  45. // indent and dedent
  46. const modKey = e.ctrlKey || e.metaKey
  47. const {value: text, selectionStart: A, selectionEnd: B} = textbox
  48. if(e.key === 'Tab') {
  49. e.preventDefault()
  50. const regex = new RegExp(`([\\s\\S]{${A}})([\\s\\S]{${B - A}})`)
  51. textbox.value = text.replace(regex, (m, a, b) => a + ' '.repeat(4))
  52. textbox.setSelectionRange(A + 4, A + 4)
  53. }
  54. if(']['.includes(e.key) && modKey) {
  55. e.preventDefault()
  56. const nLevels = {']': 1, '[': -1}[e.key]
  57. const [newText, newA, newB] = TextBox.blockIndent(text, A, B, nLevels)
  58. textbox.value = newText
  59. textbox.setSelectionRange(newA, newB)
  60. }
  61. },
  62. view() {
  63. return m('.actions',
  64. m('textarea#textbox', {
  65. oncreate: (vnode) => {
  66. TextBox.autoSize()
  67. autoFocus(vnode)
  68. },
  69. onkeydown: TextBox.hotKey,
  70. oninput: TextBox.autoSize,
  71. }),
  72. m('button', {onclick: TextBox.sendPost}, 'Send'),
  73. )
  74. },
  75. }
  76. const Post = {
  77. oncreate: ({dom}) => {
  78. dom.scrollIntoView()
  79. },
  80. prettifyTime: (ts) => {
  81. const dt = new Date(ts)
  82. const H = `0${dt.getHours()}`.slice(-2)
  83. const M = `0${dt.getMinutes()}`.slice(-2)
  84. const S = `0${dt.getSeconds()}`.slice(-2)
  85. return `${H}:${M}:${S}`
  86. },
  87. fixPost: ({dom}) => {
  88. dom.querySelectorAll('a').forEach(anchor => {
  89. anchor.target = '_blank'
  90. anchor.rel = 'noopener'
  91. })
  92. },
  93. view({attrs: {post}}) {
  94. return m('.post',
  95. m('.ts', this.prettifyTime(post.ts)),
  96. m('.source', post.source || '~'),
  97. m('.text', {oncreate: this.fixPost}, m.trust(DOMPurify.sanitize(marked(post.value)))),
  98. )
  99. }
  100. }
  101. const ChatConfig = {
  102. isOn: false,
  103. toggle() {
  104. ChatConfig.isOn = !ChatConfig.isOn
  105. },
  106. view() {
  107. const on = Chat.unseenCount ? '.on' : ''
  108. return m('button', {onclick: this.toggle}, 'chat ',
  109. m('.badge' + on, Chat.unseenCount),
  110. )
  111. },
  112. }
  113. const Chat = {
  114. posts: [],
  115. unseenCount: 0,
  116. originalTitle: 'pico.chat',
  117. onupdate: () => {
  118. if(document.hasFocus() && ChatConfig.isOn) {
  119. Chat.unseenCount = 0
  120. }
  121. const extra = Chat.unseenCount ? ` (${Chat.unseenCount})` : ``
  122. document.title = Chat.originalTitle + extra
  123. },
  124. view() {
  125. return ChatConfig.isOn ? [
  126. m('.not-chat', {onclick: () => ChatConfig.isOn = false}),
  127. m('.chat',
  128. m('.posts', Chat.posts.map(post => m(Post, {post}))),
  129. m(TextBox),
  130. )
  131. ] : null
  132. },
  133. }
  134. addEventListener('focus', m.redraw)
  135. addEventListener('post', ({detail}) => {
  136. Chat.posts.push(detail)
  137. Chat.unseenCount += !(document.hasFocus() && ChatConfig.isOn)
  138. m.redraw()
  139. })
  140. addEventListener('logout', () => {
  141. Chat.posts = []
  142. Chat.unseenCount = 0
  143. })
  144. marked.setOptions({
  145. breaks: true,
  146. })