Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

129 lines
4.0KB

  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. view() {
  104. return m('button', {onclick: () => {ChatConfig.isOn = !ChatConfig.isOn}}, 'chat')
  105. }
  106. }
  107. const Chat = {
  108. posts: [],
  109. oncreate() {
  110. listen('post', ({detail}) => {
  111. this.posts.push(detail)
  112. m.redraw()
  113. })
  114. listen('logout', () => {
  115. this.posts = []
  116. })
  117. marked.setOptions({
  118. breaks: true,
  119. })
  120. },
  121. view() {
  122. return ChatConfig.isOn ? m('.chat',
  123. m('.posts', this.posts.map(post => m(Post, {post}))),
  124. m(TextBox),
  125. ) : null
  126. },
  127. }