chuijs 4.0.2 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/README.md +43 -644
  3. package/framework/appLayout/default/chui_app_layout.js +602 -496
  4. package/framework/appLayout/default/global_style.css +288 -97
  5. package/framework/appLayout/default/main_theme_style.css +188 -101
  6. package/framework/components/chui_accordion/accordion.js +38 -35
  7. package/framework/components/chui_accordion/styles.css +13 -12
  8. package/framework/components/chui_action_sheet/action_sheet.js +93 -64
  9. package/framework/components/chui_action_sheet/styles.css +12 -5
  10. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  11. package/framework/components/chui_actions_bar/styles.css +139 -0
  12. package/framework/components/chui_affix/affix.js +16 -11
  13. package/framework/components/chui_affix/styles.css +5 -0
  14. package/framework/components/chui_alert/alert.js +34 -19
  15. package/framework/components/chui_alert/styles.css +7 -0
  16. package/framework/components/chui_avatar/avatar.js +36 -35
  17. package/framework/components/chui_avatar/styles.css +1 -1
  18. package/framework/components/chui_avatar_group/avatar_group.js +25 -23
  19. package/framework/components/chui_avatar_group/styles.css +1 -1
  20. package/framework/components/chui_backtop/backtop.js +60 -21
  21. package/framework/components/chui_backtop/styles.css +21 -0
  22. package/framework/components/chui_badge/badge.js +30 -28
  23. package/framework/components/chui_badge/styles.css +6 -6
  24. package/framework/components/chui_banner/banner.js +222 -0
  25. package/framework/components/chui_banner/styles.css +170 -0
  26. package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
  27. package/framework/components/chui_breadcrumb/styles.css +1 -1
  28. package/framework/components/chui_button/button.js +52 -36
  29. package/framework/components/chui_button/styles.css +7 -7
  30. package/framework/components/chui_calendar/calendar.js +18 -16
  31. package/framework/components/chui_card/card.js +52 -32
  32. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  33. package/framework/components/chui_card_scroll/styles.css +86 -0
  34. package/framework/components/chui_chat/chat.js +140 -79
  35. package/framework/components/chui_chat/styles.css +18 -7
  36. package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
  37. package/framework/components/chui_clipboard_history/styles.css +13 -9
  38. package/framework/components/chui_code/code.js +10 -10
  39. package/framework/components/chui_code/styles.css +4 -4
  40. package/framework/components/chui_code_editor/code_editor.js +73 -57
  41. package/framework/components/chui_code_editor/styles.css +6 -5
  42. package/framework/components/chui_command_palette/command_palette.js +87 -64
  43. package/framework/components/chui_command_palette/styles.css +5 -5
  44. package/framework/components/chui_console/console.js +13 -13
  45. package/framework/components/chui_console/styles.css +6 -3
  46. package/framework/components/chui_content_block/content_block.js +35 -21
  47. package/framework/components/chui_content_block/styles.css +2 -2
  48. package/framework/components/chui_context_menu/context_menu.js +171 -58
  49. package/framework/components/chui_context_menu/styles.css +82 -8
  50. package/framework/components/chui_custom_element/custom_element.js +25 -18
  51. package/framework/components/chui_data_grid/data_grid.js +633 -471
  52. package/framework/components/chui_data_grid/styles.css +24 -22
  53. package/framework/components/chui_descriptions/descriptions.js +34 -17
  54. package/framework/components/chui_descriptions/styles.css +1 -1
  55. package/framework/components/chui_details/details.js +42 -36
  56. package/framework/components/chui_details/styles.css +12 -11
  57. package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
  58. package/framework/components/chui_diff_viewer/styles.css +10 -10
  59. package/framework/components/chui_divider/divider.js +19 -17
  60. package/framework/components/chui_divider/styles.css +2 -2
  61. package/framework/components/chui_drawer/drawer.js +63 -38
  62. package/framework/components/chui_drawer/styles.css +18 -4
  63. package/framework/components/chui_drop_zone/drop_zone.js +37 -27
  64. package/framework/components/chui_drop_zone/styles.css +10 -6
  65. package/framework/components/chui_empty_state/empty_state.js +20 -15
  66. package/framework/components/chui_fieldset/fieldset.js +40 -23
  67. package/framework/components/chui_fieldset/styles.css +6 -6
  68. package/framework/components/chui_file_explorer/file_explorer.js +260 -189
  69. package/framework/components/chui_file_explorer/styles.css +8 -7
  70. package/framework/components/chui_form/form.js +40 -28
  71. package/framework/components/chui_form/styles.css +1 -1
  72. package/framework/components/chui_gantt/gantt.js +102 -78
  73. package/framework/components/chui_gantt/styles.css +5 -5
  74. package/framework/components/chui_graphs/graphs.js +376 -239
  75. package/framework/components/chui_graphs/styles.css +27 -6
  76. package/framework/components/chui_hero/hero.js +98 -37
  77. package/framework/components/chui_hero/styles.css +176 -7
  78. package/framework/components/chui_html_block/html_block.js +8 -8
  79. package/framework/components/chui_html_block/styles.css +3 -3
  80. package/framework/components/chui_hx/hx.js +3 -3
  81. package/framework/components/chui_hx/styles.css +13 -6
  82. package/framework/components/chui_icons/icons.js +2115 -2115
  83. package/framework/components/chui_icons/styles.css +4 -3
  84. package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
  85. package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
  86. package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
  87. package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
  88. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
  89. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  90. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
  91. package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
  92. package/framework/components/chui_inputs/chui_date/date.js +161 -127
  93. package/framework/components/chui_inputs/chui_date/styles.css +20 -19
  94. package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
  95. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  96. package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
  97. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  98. package/framework/components/chui_inputs/chui_email/email.js +48 -36
  99. package/framework/components/chui_inputs/chui_email/styles.css +11 -11
  100. package/framework/components/chui_inputs/chui_file/file.js +71 -44
  101. package/framework/components/chui_inputs/chui_file/styles.css +23 -19
  102. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
  103. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
  105. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
  107. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  108. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
  109. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
  110. package/framework/components/chui_inputs/chui_number/number.js +84 -70
  111. package/framework/components/chui_inputs/chui_number/styles.css +13 -13
  112. package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
  113. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  114. package/framework/components/chui_inputs/chui_password/password.js +72 -56
  115. package/framework/components/chui_inputs/chui_password/styles.css +15 -15
  116. package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
  117. package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
  118. package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
  119. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  120. package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
  121. package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
  122. package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
  123. package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
  124. package/framework/components/chui_inputs/chui_text/styles.css +14 -14
  125. package/framework/components/chui_inputs/chui_text/text.js +69 -52
  126. package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
  127. package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
  128. package/framework/components/chui_inputs/chui_time/styles.css +18 -20
  129. package/framework/components/chui_inputs/chui_time/time.js +71 -49
  130. package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
  131. package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
  132. package/framework/components/chui_json_editor/json_editor.js +137 -127
  133. package/framework/components/chui_json_editor/styles.css +5 -5
  134. package/framework/components/chui_json_viewer/json_viewer.js +133 -114
  135. package/framework/components/chui_json_viewer/styles.css +9 -8
  136. package/framework/components/chui_kanban/kanban.js +78 -60
  137. package/framework/components/chui_kanban/styles.css +8 -4
  138. package/framework/components/chui_kbd/kbd.js +12 -10
  139. package/framework/components/chui_kbd/styles.css +1 -1
  140. package/framework/components/chui_label/label.js +31 -29
  141. package/framework/components/chui_label/styles.css +3 -3
  142. package/framework/components/chui_link/link.js +36 -29
  143. package/framework/components/chui_link/styles.css +11 -4
  144. package/framework/components/chui_list/list.js +42 -38
  145. package/framework/components/chui_list/styles.css +4 -3
  146. package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
  147. package/framework/components/chui_markdown_editor/styles.css +5 -5
  148. package/framework/components/chui_media/audio.js +457 -364
  149. package/framework/components/chui_media/audio_styles.css +74 -37
  150. package/framework/components/chui_media/image.js +99 -64
  151. package/framework/components/chui_media/image_compare.js +86 -55
  152. package/framework/components/chui_media/image_compare_styles.css +5 -5
  153. package/framework/components/chui_media/image_styles.css +4 -4
  154. package/framework/components/chui_media/video.js +264 -171
  155. package/framework/components/chui_media/video_styles.css +57 -24
  156. package/framework/components/chui_menu_bar/menu_bar.js +67 -65
  157. package/framework/components/chui_menu_bar/styles.css +14 -12
  158. package/framework/components/chui_message_bar/message_bar.js +551 -0
  159. package/framework/components/chui_message_bar/styles.css +248 -0
  160. package/framework/components/chui_modal/modal.js +81 -56
  161. package/framework/components/chui_modal/styles.css +3 -2
  162. package/framework/components/chui_notification/notification.js +107 -69
  163. package/framework/components/chui_notification/notification_download.js +28 -77
  164. package/framework/components/chui_notification/notification_download_card.js +115 -0
  165. package/framework/components/chui_notification/notification_download_progress.css +5 -4
  166. package/framework/components/chui_notification/notification_download_progress.js +73 -105
  167. package/framework/components/chui_notification/notification_update.js +44 -37
  168. package/framework/components/chui_notification/status_plate.css +3 -3
  169. package/framework/components/chui_notification/status_plate.js +2 -2
  170. package/framework/components/chui_notification/styles.css +6 -1
  171. package/framework/components/chui_notification/styles_download.css +2 -2
  172. package/framework/components/chui_notification/styles_update.css +3 -3
  173. package/framework/components/chui_org_chart/org_chart.js +101 -62
  174. package/framework/components/chui_org_chart/styles.css +2 -2
  175. package/framework/components/chui_pagination/pagination.js +65 -30
  176. package/framework/components/chui_pagination/styles.css +1 -1
  177. package/framework/components/chui_paragraph/paragraph.js +2 -2
  178. package/framework/components/chui_paragraph/styles.css +3 -3
  179. package/framework/components/chui_popover/popover.js +77 -50
  180. package/framework/components/chui_popover/styles.css +1 -1
  181. package/framework/components/chui_popups/popups.js +210 -181
  182. package/framework/components/chui_popups/styles.css +7 -7
  183. package/framework/components/chui_print_button/print_button.js +48 -33
  184. package/framework/components/chui_progress_bar/progress_bar.js +49 -19
  185. package/framework/components/chui_progress_bar/styles.css +6 -6
  186. package/framework/components/chui_progress_ring/progress_ring.js +32 -30
  187. package/framework/components/chui_progress_ring/styles.css +1 -1
  188. package/framework/components/chui_qrcode/qrcode.js +85 -41
  189. package/framework/components/chui_query_builder/query_builder.js +263 -170
  190. package/framework/components/chui_query_builder/styles.css +4 -4
  191. package/framework/components/chui_radio_group/radio_group.js +36 -26
  192. package/framework/components/chui_radio_group/styles.css +2 -2
  193. package/framework/components/chui_result/result.js +41 -31
  194. package/framework/components/chui_result/styles.css +3 -3
  195. package/framework/components/chui_scheduler/scheduler.js +126 -104
  196. package/framework/components/chui_scheduler/styles.css +11 -7
  197. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  198. package/framework/components/chui_screen_spinner/styles.css +49 -0
  199. package/framework/components/chui_segmented_control/segmented_control.js +90 -27
  200. package/framework/components/chui_segmented_control/styles.css +32 -6
  201. package/framework/components/chui_settings/settings.js +160 -87
  202. package/framework/components/chui_settings/styles.css +4 -4
  203. package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
  204. package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
  205. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  206. package/framework/components/chui_skeleton/skeleton.js +29 -24
  207. package/framework/components/chui_slideshow/slideshow.js +122 -103
  208. package/framework/components/chui_slideshow/styles.css +22 -16
  209. package/framework/components/chui_spinner/spinner.js +17 -17
  210. package/framework/components/chui_spinner/styles.css +0 -1
  211. package/framework/components/chui_split_button/split_button.js +53 -30
  212. package/framework/components/chui_split_button/styles.css +22 -6
  213. package/framework/components/chui_splitter/splitter.js +46 -39
  214. package/framework/components/chui_splitter/styles.css +2 -2
  215. package/framework/components/chui_statistic/statistic.js +111 -84
  216. package/framework/components/chui_status_bar/status_bar.js +11 -9
  217. package/framework/components/chui_status_bar/styles.css +2 -2
  218. package/framework/components/chui_stepper/stepper.js +230 -54
  219. package/framework/components/chui_stepper/styles.css +79 -14
  220. package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
  221. package/framework/components/chui_sticky_bar/styles.css +11 -7
  222. package/framework/components/chui_tabs/chui_tabs.js +100 -46
  223. package/framework/components/chui_tabs/styles.css +43 -15
  224. package/framework/components/chui_text_editor/styles.css +24 -23
  225. package/framework/components/chui_text_editor/text_editor.js +103 -79
  226. package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
  227. package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
  228. package/framework/components/chui_timeline/styles.css +3 -1
  229. package/framework/components/chui_timeline/timeline.js +14 -12
  230. package/framework/components/chui_toast_queue/styles.css +11 -11
  231. package/framework/components/chui_toast_queue/toast_queue.js +82 -73
  232. package/framework/components/chui_toolbar/styles.css +2 -2
  233. package/framework/components/chui_toolbar/toolbar.js +20 -13
  234. package/framework/components/chui_tooltip/styles.css +2 -2
  235. package/framework/components/chui_tooltip/tooltip.js +40 -27
  236. package/framework/components/chui_tour/styles.css +12 -7
  237. package/framework/components/chui_tour/tour.js +132 -98
  238. package/framework/components/chui_transfer/styles.css +8 -7
  239. package/framework/components/chui_transfer/transfer.js +143 -123
  240. package/framework/components/chui_tree_table/styles.css +4 -4
  241. package/framework/components/chui_tree_table/tree_table.js +88 -74
  242. package/framework/components/chui_tree_view/styles.css +14 -10
  243. package/framework/components/chui_tree_view/tree_view.js +298 -253
  244. package/framework/components/chui_user_bar/styles.css +145 -0
  245. package/framework/components/chui_user_bar/user_bar.js +402 -0
  246. package/framework/components/chui_user_list_bar/styles.css +173 -0
  247. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  248. package/framework/components/chui_watermark/watermark.js +41 -33
  249. package/framework/components/chui_webview/styles.css +2 -2
  250. package/framework/components/chui_webview/webview.js +64 -52
  251. package/framework/components/chui_write_bar/styles.css +172 -0
  252. package/framework/components/chui_write_bar/write_bar.js +320 -0
  253. package/framework/main_window.js +1234 -0
  254. package/framework/modules/chui_a11y/chui_a11y.js +153 -123
  255. package/framework/modules/chui_animations/animations.js +105 -43
  256. package/framework/modules/chui_animations/motion.js +120 -0
  257. package/framework/modules/chui_animations/styles.css +37 -4
  258. package/framework/modules/chui_bridge/preload.js +36 -25
  259. package/framework/modules/chui_functions.js +69 -63
  260. package/framework/modules/chui_highlight/chui_highlight.js +25 -22
  261. package/framework/modules/chui_highlight/styles.css +13 -10
  262. package/framework/modules/chui_i18n/chui_i18n.js +12 -10
  263. package/framework/modules/chui_i18n/locales/a11y.js +5 -5
  264. package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
  265. package/framework/modules/chui_i18n/locales/calendar.js +3 -3
  266. package/framework/modules/chui_i18n/locales/common.js +3 -3
  267. package/framework/modules/chui_i18n/locales/data.js +36 -26
  268. package/framework/modules/chui_i18n/locales/editors.js +40 -13
  269. package/framework/modules/chui_i18n/locales/feedback.js +65 -3
  270. package/framework/modules/chui_i18n/locales/forms.js +8 -8
  271. package/framework/modules/chui_i18n/locales/graphics.js +8 -8
  272. package/framework/modules/chui_i18n/locales/media.js +4 -4
  273. package/framework/modules/chui_i18n/locales/navigation.js +12 -8
  274. package/framework/modules/chui_i18n/locales/validate.js +3 -3
  275. package/framework/modules/chui_json/chui_json.js +11 -11
  276. package/framework/modules/chui_layout/chui_layout.js +12 -10
  277. package/framework/modules/chui_logger/chui_logger.js +47 -28
  278. package/framework/modules/chui_page/page.js +39 -33
  279. package/framework/modules/chui_palette/palette.js +61 -0
  280. package/framework/modules/chui_qr/chui_qr.js +179 -88
  281. package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
  282. package/framework/modules/chui_scrollbar/styles.css +4 -2
  283. package/framework/modules/chui_services/chui_channels.js +27 -13
  284. package/framework/modules/chui_services/chui_services.js +47 -39
  285. package/framework/modules/chui_services/desktop.js +108 -101
  286. package/framework/modules/chui_sortable/chui_sortable.js +33 -28
  287. package/framework/modules/chui_validate/chui_validate.js +10 -7
  288. package/framework/tray_bar/chui_menu_items.js +136 -46
  289. package/index.d.ts +5046 -0
  290. package/index.js +199 -1254
  291. package/package.json +81 -68
  292. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
@@ -0,0 +1,551 @@
1
+ const { setStyles, markdownToHtml, optionOr } = require("../../modules/chui_functions");
2
+ const { Avatar } = require("../chui_avatar/avatar");
3
+ const { Icon, Icons } = require("../chui_icons/icons");
4
+ const { I18n } = require("../../modules/chui_i18n/chui_i18n");
5
+
6
+ /** Значки состояния отправленного сообщения: в пути, доставлено, прочитано, ошибка */
7
+ const STATUS_ICONS = {
8
+ sending: Icons.ACTIONS.SCHEDULE,
9
+ sent: Icons.NAVIGATION.CHECK,
10
+ read: Icons.ACTIONS.DONE_ALL,
11
+ failed: Icons.ALERT.ERROR_OUTLINE,
12
+ };
13
+ /** Прокрутка считается «у нижнего края», если до него осталось меньше этого, px */
14
+ const NEAR_BOTTOM = 80;
15
+ /** Высота ленты по умолчанию: лента прокручивается сама, а страница под ней не растёт */
16
+ const DEFAULT_HEIGHT = "320px";
17
+
18
+ /**
19
+ * Лента сообщений — то, что показывается над панелью ввода.
20
+ *
21
+ * Пара к `WriteBar`: панель набирает и отправляет, лента показывает написанное. Связка
22
+ * делается одним вызовом — `linkWriteBar(bar)` подписывается на отправку панели и кладёт
23
+ * её текст в ленту своим сообщением со состоянием `sending`; дальше приложение ставит
24
+ * `sent`/`read` через `updateMessage()`. Ленту можно использовать и без панели: она
25
+ * принимает сообщения из `addMessage()` — например, из сокета.
26
+ *
27
+ * Отличие от `Chat`: у ленты нет ни шапки, ни поля ввода — только сообщения. Зато она следит
28
+ * за нижним краем: если человек читает историю выше, новое сообщение не сдёргивает прокрутку
29
+ * вниз, а над нижним краем появляется кнопка «Новые сообщения» со счётчиком. Лента, стоящая
30
+ * у нижнего края, ведёт себя как в мессенджере — новое сообщение сразу видно. Нижний край она
31
+ * держит и тогда, когда вёрстка меняется уже после показа (стили приходят отдельной таблицей,
32
+ * окно меняет ширину, догружается картинка): за размером содержимого следит наблюдатель.
33
+ *
34
+ * Сообщения группируются как в мессенджерах: подряд идущие сообщения одного автора одного дня
35
+ * лежат в одной группе `message_bar_group` — у чужой группы один аватар и одна подпись автора,
36
+ * а пузыри `message_bar_bubble` идут друг под другом. Свои сообщения (`own: true`) стоят справа
37
+ * и окрашены акцентом, чужие — слева. Между днями ставится разделитель с датой («Сегодня»,
38
+ * «Вчера», иначе дата по языку через `Intl.DateTimeFormat`).
39
+ *
40
+ * Доступность: лента — `role="log"` с `aria-live="polite"`, поэтому новое сообщение читает
41
+ * скринридер (своё объявление для этого не нужно); «печатает…» объявляет отдельный
42
+ * `role="status"`; кнопка «Новые сообщения» называет и число новых, и у времени стоит
43
+ * `datetime`; аватары декоративны — имя автора уже есть в тексте. Лента прокручивается
44
+ * с клавиатуры (`tabindex="0"`).
45
+ *
46
+ * ```javascript
47
+ * const feed = new MessageBar({ own: 'Вы', height: '260px', messages: [] });
48
+ * const bar = new WriteBar({ placeholder: 'Напишите сообщение' });
49
+ * feed.linkWriteBar(bar); // отправленное из панели встаёт в ленту
50
+ * feed.setTyping('Иван'); // «Иван печатает…»
51
+ * feed.addMessage({ text: 'Смотри отчёт', author: 'Иван' });
52
+ * ```
53
+ *
54
+ * Опции: `id`, `messages`, `own`, `height`, `label`, `dateSeparators`, `avatars`, `typing`,
55
+ * `onMessageClick`.
56
+ * Сообщение: `{id, text, markdownText, author, avatar, time, own, status, attachments}`;
57
+ * фабрика — `MessageBar.Message(...)`, константа — `MessageBar.STATUS.{SENDING, SENT, READ, FAILED}`.
58
+ * API: `addMessage()`, `updateMessage(id, patch)`, `removeMessage(id)`, `setMessages()`,
59
+ * `getMessages()`, `clear()`, `setTyping(name)`, `isTyping()`, `scrollToBottom()`,
60
+ * `isNearBottom()`, `getUnread()`, `setHeight(value)`, `getHeight()`, `setLabel(text)`,
61
+ * `linkWriteBar(bar)`, `unlinkWriteBar()`, `getFeed()`, `set()`, `destroy()`.
62
+ */
63
+ class MessageBar {
64
+ static STATUS = {
65
+ SENDING: "sending",
66
+ SENT: "sent",
67
+ READ: "read",
68
+ FAILED: "failed",
69
+ };
70
+ static #counter = 0;
71
+
72
+ #id = undefined;
73
+ #root = document.createElement("chui_message_bar");
74
+ #view = document.createElement("message_bar_view");
75
+ #feed = document.createElement("message_bar_feed");
76
+ #content = document.createElement("message_bar_content");
77
+ #typing = document.createElement("message_bar_typing");
78
+ #empty = document.createElement("message_bar_empty");
79
+ #jump = document.createElement("button");
80
+ #jumpIcon = document.createElement("message_bar_jump_icon");
81
+ #jumpText = document.createElement("message_bar_jump_text");
82
+ #messages = [];
83
+ #height = DEFAULT_HEIGHT;
84
+ #own = undefined;
85
+ #label = undefined;
86
+ #dateSeparators = true;
87
+ #avatars = true;
88
+ #typingName = undefined;
89
+ #unread = 0;
90
+ #stuck = true;
91
+ #observer = undefined;
92
+ #onMessageClick = undefined;
93
+ #writeBar = undefined;
94
+ #sendListener = undefined;
95
+ // Текущий день и текущая группа — по ним решается, дописывать пузырь или начать новую;
96
+ // группа — это {node, bubbles, avatar, author, day}
97
+ #lastDate = undefined;
98
+ #lastGroup = undefined;
99
+
100
+ constructor(
101
+ options = {
102
+ id: String(),
103
+ messages: [],
104
+ own: String(),
105
+ height: String(),
106
+ label: String(),
107
+ dateSeparators: Boolean(),
108
+ avatars: Boolean(),
109
+ typing: String(),
110
+ onMessageClick: undefined,
111
+ }
112
+ ) {
113
+ setStyles(__dirname + "/styles.css", "chUiJS_MessageBar");
114
+ MessageBar.#counter += 1;
115
+ this.#id = options.id === undefined ? `chui_message_bar_${MessageBar.#counter}` : options.id;
116
+ this.#root.id = this.#id;
117
+ this.#own = options.own;
118
+ this.#label = options.label;
119
+ this.#onMessageClick = options.onMessageClick;
120
+ this.#dateSeparators = options.dateSeparators !== false;
121
+ this.#avatars = options.avatars !== false;
122
+ this.setHeight(options.height);
123
+
124
+ this.#feed.id = `${this.#id}_feed`;
125
+ this.#feed.setAttribute("role", "log");
126
+ this.#feed.setAttribute("aria-live", "polite");
127
+ this.#feed.setAttribute("aria-relevant", "additions");
128
+ this.#feed.setAttribute("aria-label", this.#labelText());
129
+ this.#feed.setAttribute("tabindex", "0");
130
+ this.#typing.setAttribute("role", "status");
131
+ this.#typing.hidden = true;
132
+ this.#empty.innerText = I18n.t("feedback.messageBarEmpty");
133
+
134
+ this.#jump.type = "button";
135
+ this.#jump.className = "message_bar_jump";
136
+ this.#jump.dataset.visible = "false";
137
+ this.#jumpIcon.innerHTML = new Icon(Icons.NAVIGATION.ARROW_DOWNWARD, "16px").getHTML();
138
+ this.#jumpText.innerText = I18n.t("feedback.messageBarNew");
139
+ this.#jump.setAttribute("aria-label", I18n.t("feedback.messageBarNew"));
140
+ this.#jump.append(this.#jumpIcon, this.#jumpText);
141
+ this.#jump.addEventListener("click", () => this.scrollToBottom());
142
+
143
+ this.#feed.appendChild(this.#content);
144
+ this.#view.append(this.#feed, this.#jump);
145
+ this.#root.append(this.#view, this.#typing);
146
+ this.#feed.addEventListener("scroll", this.#onScroll);
147
+ this.#feed.addEventListener("click", this.#onFeedClick);
148
+ // Лента держит нижний край и тогда, когда вёрстка меняется уже после показа: стили
149
+ // приходят отдельной таблицей, окно меняет ширину, картинки догружаются. Наблюдаем
150
+ // за содержимым, а не за самой лентой: её размер от наполнения не меняется, и рост
151
+ // сообщений был бы не виден — из-за этого лента открывалась не у нижнего края
152
+ this.#observer = new ResizeObserver(this.#onResize);
153
+ this.#observer.observe(this.#content);
154
+ if (options.typing !== undefined) this.setTyping(options.typing);
155
+ this.setMessages(options.messages === undefined ? [] : options.messages);
156
+ }
157
+
158
+ // GET
159
+ getId() {
160
+ return this.#id;
161
+ }
162
+ /** Копии сообщений: правка копии не меняет ленту */
163
+ getMessages() {
164
+ return this.#messages.map((message) => Object.assign({}, message));
165
+ }
166
+ getFeed() {
167
+ return this.#feed;
168
+ }
169
+ getHeight() {
170
+ return this.#height;
171
+ }
172
+ /** Сколько сообщений пришло, пока лента стояла выше нижнего края */
173
+ getUnread() {
174
+ return this.#unread;
175
+ }
176
+ isTyping() {
177
+ return this.#typingName !== undefined;
178
+ }
179
+ /** Лента стоит у нижнего края: новое сообщение там видно сразу */
180
+ isNearBottom() {
181
+ return this.#isNearBottom();
182
+ }
183
+
184
+ // SET
185
+ /** Высота ленты: своя переменная, поэтому строка «печатает» под лентой не уезжает */
186
+ setHeight(value = String()) {
187
+ this.#height = optionOr(value, undefined) === undefined ? DEFAULT_HEIGHT : String(value);
188
+ this.#root.style.setProperty("--message_bar_height", this.#height);
189
+ return this;
190
+ }
191
+ /** Имя ленты для скринридера: по умолчанию — общая подпись из словаря */
192
+ setLabel(text = String()) {
193
+ this.#label = text;
194
+ this.#feed.setAttribute("aria-label", this.#labelText());
195
+ return this;
196
+ }
197
+ set() {
198
+ return this.#root;
199
+ }
200
+
201
+ // СООБЩЕНИЯ
202
+ /** Полная замена списка: лента собирается заново, группы схлопываются по автору и дню */
203
+ setMessages(messages = []) {
204
+ this.#messages = [];
205
+ this.#lastDate = undefined;
206
+ this.#lastGroup = undefined;
207
+ this.#content.innerHTML = "";
208
+ this.#content.appendChild(this.#empty);
209
+ for (const message of messages) this.#appendMessage(Object.assign({}, message), false);
210
+ this.#empty.hidden = this.#messages.length > 0;
211
+ this.#unread = 0;
212
+ this.scrollToBottom(false);
213
+ this.#syncJump();
214
+ return this;
215
+ }
216
+ /**
217
+ * Новое сообщение. Лента ведёт себя как мессенджер: если она у нижнего края — сообщение
218
+ * сразу видно, а если человек читает историю выше — прокрутка не сдёргивается, зато
219
+ * растёт счётчик на кнопке «Новые сообщения».
220
+ */
221
+ addMessage(message = {}) {
222
+ const near = this.#isNearBottom();
223
+ const added = this.#appendMessage(Object.assign({}, message), near);
224
+ if (near === false) this.#unread += 1;
225
+ this.#syncJump();
226
+ return added;
227
+ }
228
+ /**
229
+ * Правка сообщения по `id`: текст, время, состояние (например, `sending` → `read`).
230
+ * Лента пересобирается, но прокрутка остаётся на месте — состояние доставки приходит
231
+ * и тогда, когда человек читает историю выше.
232
+ */
233
+ updateMessage(id = String(), patch = {}) {
234
+ const message = this.#messages.find((entry) => entry.id === id);
235
+ if (message === undefined) return this;
236
+ Object.assign(message, patch);
237
+ return this.#rebuild(true);
238
+ }
239
+ removeMessage(id = String()) {
240
+ const index = this.#messages.findIndex((entry) => entry.id === id);
241
+ if (index === -1) return this;
242
+ this.#messages.splice(index, 1);
243
+ return this.#rebuild(true);
244
+ }
245
+ clear() {
246
+ return this.setMessages([]);
247
+ }
248
+ /** «Иван печатает…»: имя автора; `undefined` убирает строку */
249
+ setTyping(name = undefined) {
250
+ this.#typingName = name;
251
+ this.#typing.innerText = name === undefined ? "" : I18n.t("feedback.chatTyping", { name: name });
252
+ this.#showTyping(name !== undefined);
253
+ return this;
254
+ }
255
+ /**
256
+ * Связь с панелью ввода: отправленное из панели встаёт в ленту своим сообщением
257
+ * со состоянием `sending` — приложение подтверждает доставку через `updateMessage()`.
258
+ * Повторный вызов меняет панель, прежняя связь снимается.
259
+ */
260
+ linkWriteBar(bar = undefined) {
261
+ this.unlinkWriteBar();
262
+ if (bar === undefined) return this;
263
+ this.#writeBar = bar;
264
+ this.#sendListener = (text) => {
265
+ this.addMessage({
266
+ text: text,
267
+ own: true,
268
+ time: Date.now(),
269
+ status: MessageBar.STATUS.SENDING,
270
+ });
271
+ };
272
+ if (typeof bar.addSendListener === "function") bar.addSendListener(this.#sendListener);
273
+ return this;
274
+ }
275
+ unlinkWriteBar() {
276
+ if (
277
+ this.#writeBar !== undefined &&
278
+ this.#sendListener !== undefined &&
279
+ typeof this.#writeBar.removeSendListener === "function"
280
+ )
281
+ this.#writeBar.removeSendListener(this.#sendListener);
282
+ this.#writeBar = undefined;
283
+ this.#sendListener = undefined;
284
+ return this;
285
+ }
286
+ /** Прокрутка к низу ленты: движение не запускается при «уменьшить движение» */
287
+ scrollToBottom(smooth = true) {
288
+ const behavior = smooth && this.#reducedMotion() === false ? "smooth" : "auto";
289
+ if (typeof this.#feed.scrollTo === "function")
290
+ this.#feed.scrollTo({ top: this.#feed.scrollHeight, behavior: behavior });
291
+ else this.#feed.scrollTop = this.#feed.scrollHeight;
292
+ this.#unread = 0;
293
+ this.#syncJump();
294
+ return this;
295
+ }
296
+
297
+ destroy() {
298
+ this.unlinkWriteBar();
299
+ this.#observer.disconnect();
300
+ this.#feed.removeEventListener("scroll", this.#onScroll);
301
+ this.#feed.removeEventListener("click", this.#onFeedClick);
302
+ this.#messages = [];
303
+ this.#root.remove();
304
+ }
305
+
306
+ // === ВНУТРЕННЕЕ ===
307
+ #labelText() {
308
+ return optionOr(this.#label, undefined) === undefined ? I18n.t("feedback.messageBar") : String(this.#label);
309
+ }
310
+ #reducedMotion() {
311
+ if (typeof window.matchMedia !== "function") return false;
312
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches === true;
313
+ }
314
+ #isNearBottom() {
315
+ return this.#feed.scrollHeight - this.#feed.scrollTop - this.#feed.clientHeight < NEAR_BOTTOM;
316
+ }
317
+ /** Пересборка на месте: прокрутку возвращаем на место — читающего историю не сдёргиваем */
318
+ #rebuild(keepScroll = false) {
319
+ const offset = this.#feed.scrollTop;
320
+ const messages = this.#messages.slice();
321
+ this.setMessages(messages);
322
+ if (keepScroll === true) this.#feed.scrollTop = offset;
323
+ this.#syncJump();
324
+ return this;
325
+ }
326
+ /**
327
+ * Кнопка над нижним краем: зовёт вниз, когда человек читает историю выше, и называет
328
+ * число новых сообщений. Вид задаётся атрибутом, а не `hidden`: показ и уборка идут
329
+ * переходом (прозрачность и сдвиг), поэтому кнопка не мигает.
330
+ */
331
+ #syncJump() {
332
+ const near = this.#isNearBottom();
333
+ this.#stuck = near;
334
+ if (near === true) this.#unread = 0;
335
+ const count = this.#unread;
336
+ const visible = count > 0 || near === false;
337
+ if (count > 1) {
338
+ this.#jumpText.innerText = `${I18n.t("feedback.messageBarNew")} · ${count}`;
339
+ this.#jump.setAttribute("aria-label", I18n.t("feedback.messageBarNewCount", { value: String(count) }));
340
+ } else if (count === 1) {
341
+ this.#jumpText.innerText = I18n.t("feedback.messageBarNew");
342
+ this.#jump.setAttribute("aria-label", I18n.t("feedback.messageBarNew"));
343
+ } else {
344
+ this.#jumpText.innerText = I18n.t("feedback.messageBarDown");
345
+ this.#jump.setAttribute("aria-label", I18n.t("feedback.messageBarDown"));
346
+ }
347
+ this.#jump.dataset.visible = visible === true ? "true" : "false";
348
+ }
349
+ /**
350
+ * Строка «печатает» появляется и уходит движением: `hidden` возвращается по концу кадра,
351
+ * поэтому строка не пропадает рывком. Открытие за время ухода отменяет снятие.
352
+ */
353
+ #showTyping(visible) {
354
+ if (visible === true) {
355
+ this.#typing.classList.remove("message_bar_typing_leaving");
356
+ this.#typing.hidden = false;
357
+ if (this.#isNearBottom()) this.scrollToBottom();
358
+ return;
359
+ }
360
+ if (this.#typing.hidden === true) return;
361
+ this.#typing.classList.add("message_bar_typing_leaving");
362
+ const done = (event) => {
363
+ if (event.target !== this.#typing) return;
364
+ this.#typing.removeEventListener("animationend", done);
365
+ if (this.#typing.classList.contains("message_bar_typing_leaving") === false) return;
366
+ this.#typing.classList.remove("message_bar_typing_leaving");
367
+ this.#typing.hidden = true;
368
+ };
369
+ this.#typing.addEventListener("animationend", done);
370
+ }
371
+ /**
372
+ * Добавить сообщение в ленту. Если автор и день совпадают с прошлой группой, пузырь
373
+ * дописывается в неё (один аватар и одна подпись автора), иначе создаётся новая.
374
+ */
375
+ #appendMessage(message = {}, live = true) {
376
+ const date = this.#toDate(message.time);
377
+ message.id = message.id === undefined ? require("randomstring").generate() : message.id;
378
+ message.time = date === undefined ? Date.now() : date.getTime();
379
+ this.#messages.push(message);
380
+ this.#empty.hidden = true;
381
+ this.#insertDateSeparator(date);
382
+ const group = this.#groupFor(message);
383
+ const first = group.bubbles.childElementCount === 0;
384
+ const bubble = this.#bubble(message, first);
385
+ // Хвостик группы: у последнего пузыря угол со стороны автора скруглён меньше —
386
+ // по нему видно, где кончается стопка
387
+ const previous = group.bubbles.lastElementChild;
388
+ if (previous !== null) previous.dataset.last = "false";
389
+ bubble.dataset.last = "true";
390
+ group.bubbles.appendChild(bubble);
391
+ if (live) this.scrollToBottom(true);
392
+ return message;
393
+ }
394
+ #toDate(time) {
395
+ if (time === undefined || time === null) return undefined;
396
+ const date = time instanceof Date ? time : new Date(time);
397
+ return isNaN(date.getTime()) ? undefined : date;
398
+ }
399
+ /** Разделитель дня: между сообщениями разных дней и в начале ленты */
400
+ #insertDateSeparator(date) {
401
+ if (date === undefined || this.#dateSeparators === false) return;
402
+ const key = this.#dayKey(date);
403
+ if (key === this.#lastDate) return;
404
+ this.#lastDate = key;
405
+ const separator = document.createElement("message_bar_day");
406
+ separator.dataset.day = key;
407
+ const label = document.createElement("message_bar_day_label");
408
+ label.innerText = this.#dayLabel(date);
409
+ separator.appendChild(label);
410
+ this.#content.appendChild(separator);
411
+ this.#lastGroup = undefined;
412
+ }
413
+ /** Группа для сообщения: та же, если автор и день совпадают, иначе новая */
414
+ #groupFor(message) {
415
+ const author = this.#authorOf(message);
416
+ if (
417
+ this.#lastGroup !== undefined &&
418
+ this.#lastGroup.author === author &&
419
+ this.#lastGroup.day === this.#lastDate
420
+ ) {
421
+ return this.#lastGroup;
422
+ }
423
+ const node = document.createElement("message_bar_group");
424
+ node.dataset.own = message.own === true ? "true" : "false";
425
+ const bubbles = document.createElement("message_bar_bubbles");
426
+ node.appendChild(bubbles);
427
+ // Аватар — только у чужих сообщений: свои и так справа
428
+ let avatar = undefined;
429
+ if (message.own !== true && this.#avatars === true) {
430
+ avatar = new Avatar({
431
+ name: author,
432
+ image: message.avatar === undefined ? undefined : { path: message.avatar },
433
+ size: Avatar.SIZE.SMALL,
434
+ decorative: true,
435
+ });
436
+ node.prepend(avatar.set());
437
+ }
438
+ this.#content.appendChild(node);
439
+ this.#lastGroup = { node: node, bubbles: bubbles, avatar: avatar, author: author, day: this.#lastDate };
440
+ return this.#lastGroup;
441
+ }
442
+ #bubble(message, first) {
443
+ const bubble = document.createElement("message_bar_bubble");
444
+ bubble.dataset.own = message.own === true ? "true" : "false";
445
+ bubble.dataset.id = message.id;
446
+ // Подпись автора — только у первого пузыря группы, дальше она уже прочитана
447
+ if (message.own !== true && first) {
448
+ const author = document.createElement("message_bar_author");
449
+ author.innerText = this.#authorOf(message);
450
+ bubble.appendChild(author);
451
+ }
452
+ if (message.markdownText !== undefined) {
453
+ const text = document.createElement("message_bar_text");
454
+ text.innerHTML = markdownToHtml(message.markdownText);
455
+ bubble.appendChild(text);
456
+ } else if (message.text !== undefined) {
457
+ const text = document.createElement("message_bar_text");
458
+ text.innerText = String(message.text);
459
+ bubble.appendChild(text);
460
+ }
461
+ for (const attachment of message.attachments === undefined ? [] : message.attachments) {
462
+ bubble.appendChild(this.#attachment(attachment));
463
+ }
464
+ bubble.appendChild(this.#meta(message));
465
+ return bubble;
466
+ }
467
+ #attachment(attachment = {}) {
468
+ const node = document.createElement("message_bar_attachment");
469
+ if (attachment.url === undefined) {
470
+ node.innerText = attachment.title === undefined ? "" : String(attachment.title);
471
+ return node;
472
+ }
473
+ const link = document.createElement("a");
474
+ link.href = attachment.url;
475
+ link.innerText = attachment.title === undefined ? attachment.url : String(attachment.title);
476
+ node.appendChild(link);
477
+ return node;
478
+ }
479
+ /** Время и состояние: у своих сообщений виден путь доставки */
480
+ #meta(message) {
481
+ const meta = document.createElement("message_bar_meta");
482
+ const date = this.#toDate(message.time);
483
+ if (date !== undefined) {
484
+ const time = document.createElement("message_bar_time");
485
+ time.innerText = this.#timeLabel(date);
486
+ time.setAttribute("datetime", date.toISOString());
487
+ meta.appendChild(time);
488
+ }
489
+ if (message.own === true && message.status !== undefined) {
490
+ const status = document.createElement("message_bar_status");
491
+ status.dataset.status = message.status;
492
+ status.setAttribute("aria-label", this.#statusLabel(message.status));
493
+ status.setAttribute("role", "img");
494
+ status.innerHTML = new Icon(
495
+ STATUS_ICONS[message.status] === undefined ? STATUS_ICONS.sent : STATUS_ICONS[message.status],
496
+ "13px"
497
+ ).getHTML();
498
+ meta.appendChild(status);
499
+ }
500
+ return meta;
501
+ }
502
+ #statusLabel(status) {
503
+ if (status === MessageBar.STATUS.READ) return I18n.t("feedback.chatRead");
504
+ if (status === MessageBar.STATUS.FAILED) return I18n.t("feedback.chatFailed");
505
+ if (status === MessageBar.STATUS.SENDING) return I18n.t("feedback.chatSending");
506
+ return I18n.t("feedback.chatSent");
507
+ }
508
+ #authorOf(message) {
509
+ if (message.author !== undefined) return String(message.author);
510
+ if (message.own !== true) return "";
511
+ // Своё имя — опция `own`: по нему же свои сообщения попадают в одну группу
512
+ return optionOr(this.#own, undefined) === undefined ? I18n.t("feedback.chatMe") : String(this.#own);
513
+ }
514
+ #dayKey(date) {
515
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
516
+ }
517
+ /** Дата дня: «Сегодня»/«Вчера» из словаря, иначе дата по текущему языку */
518
+ #dayLabel(date) {
519
+ const now = new Date();
520
+ if (this.#dayKey(date) === this.#dayKey(now)) return I18n.t("common.today");
521
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
522
+ if (this.#dayKey(date) === this.#dayKey(yesterday)) return I18n.t("common.yesterday");
523
+ return new Intl.DateTimeFormat(I18n.getLocale(), { day: "numeric", month: "long" }).format(date);
524
+ }
525
+ #timeLabel(date) {
526
+ return new Intl.DateTimeFormat(I18n.getLocale(), { hour: "2-digit", minute: "2-digit" }).format(date);
527
+ }
528
+
529
+ // === ОБРАБОТЧИКИ ===
530
+ #onScroll = () => {
531
+ this.#stuck = this.#isNearBottom();
532
+ this.#syncJump();
533
+ };
534
+ /** Содержимое выросло — если лента стояла у нижнего края, она там и остаётся */
535
+ #onResize = () => {
536
+ if (this.#stuck === false) return;
537
+ this.#feed.scrollTop = this.#feed.scrollHeight;
538
+ };
539
+ #onFeedClick = (event) => {
540
+ if (this.#onMessageClick === undefined) return;
541
+ const bubble = event.target instanceof Element ? event.target.closest("message_bar_bubble") : null;
542
+ if (bubble === null) return;
543
+ const message = this.#messages.find((entry) => entry.id === bubble.dataset.id);
544
+ if (message !== undefined) this.#onMessageClick(message);
545
+ };
546
+ }
547
+
548
+ exports.MessageBar = MessageBar;
549
+
550
+ // Описание сообщения собирает фабрика — как у Chat и ContextMenu
551
+ MessageBar.Message = (options = {}) => Object.assign({}, options);