chuijs 4.0.3 → 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 (258) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/framework/appLayout/default/chui_app_layout.js +361 -303
  3. package/framework/appLayout/default/global_style.css +243 -89
  4. package/framework/appLayout/default/main_theme_style.css +188 -101
  5. package/framework/components/chui_accordion/accordion.js +17 -17
  6. package/framework/components/chui_accordion/styles.css +7 -7
  7. package/framework/components/chui_action_sheet/action_sheet.js +35 -32
  8. package/framework/components/chui_action_sheet/styles.css +10 -4
  9. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  10. package/framework/components/chui_actions_bar/styles.css +139 -0
  11. package/framework/components/chui_affix/affix.js +4 -4
  12. package/framework/components/chui_affix/styles.css +5 -0
  13. package/framework/components/chui_alert/alert.js +11 -5
  14. package/framework/components/chui_alert/styles.css +7 -0
  15. package/framework/components/chui_avatar/avatar.js +13 -13
  16. package/framework/components/chui_avatar/styles.css +1 -1
  17. package/framework/components/chui_avatar_group/avatar_group.js +4 -4
  18. package/framework/components/chui_avatar_group/styles.css +1 -1
  19. package/framework/components/chui_backtop/backtop.js +25 -2
  20. package/framework/components/chui_backtop/styles.css +21 -0
  21. package/framework/components/chui_badge/badge.js +15 -15
  22. package/framework/components/chui_badge/styles.css +6 -6
  23. package/framework/components/chui_banner/banner.js +222 -0
  24. package/framework/components/chui_banner/styles.css +170 -0
  25. package/framework/components/chui_breadcrumb/styles.css +1 -1
  26. package/framework/components/chui_button/button.js +24 -28
  27. package/framework/components/chui_button/styles.css +5 -5
  28. package/framework/components/chui_calendar/calendar.js +6 -6
  29. package/framework/components/chui_card/card.js +14 -14
  30. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  31. package/framework/components/chui_card_scroll/styles.css +86 -0
  32. package/framework/components/chui_chat/chat.js +73 -51
  33. package/framework/components/chui_chat/styles.css +18 -7
  34. package/framework/components/chui_clipboard_history/clipboard_history.js +25 -20
  35. package/framework/components/chui_clipboard_history/styles.css +10 -8
  36. package/framework/components/chui_code/code.js +8 -8
  37. package/framework/components/chui_code/styles.css +2 -2
  38. package/framework/components/chui_code_editor/code_editor.js +26 -26
  39. package/framework/components/chui_code_editor/styles.css +3 -3
  40. package/framework/components/chui_command_palette/command_palette.js +29 -29
  41. package/framework/components/chui_command_palette/styles.css +5 -5
  42. package/framework/components/chui_console/console.js +9 -9
  43. package/framework/components/chui_console/styles.css +5 -2
  44. package/framework/components/chui_content_block/content_block.js +2 -2
  45. package/framework/components/chui_context_menu/context_menu.js +162 -54
  46. package/framework/components/chui_context_menu/styles.css +82 -8
  47. package/framework/components/chui_custom_element/custom_element.js +11 -11
  48. package/framework/components/chui_data_grid/data_grid.js +268 -268
  49. package/framework/components/chui_data_grid/styles.css +14 -14
  50. package/framework/components/chui_descriptions/styles.css +1 -1
  51. package/framework/components/chui_details/details.js +12 -12
  52. package/framework/components/chui_details/styles.css +7 -7
  53. package/framework/components/chui_diff_viewer/diff_viewer.js +88 -88
  54. package/framework/components/chui_diff_viewer/styles.css +9 -9
  55. package/framework/components/chui_divider/divider.js +6 -6
  56. package/framework/components/chui_divider/styles.css +2 -2
  57. package/framework/components/chui_drawer/drawer.js +37 -14
  58. package/framework/components/chui_drawer/styles.css +18 -4
  59. package/framework/components/chui_drop_zone/drop_zone.js +6 -6
  60. package/framework/components/chui_drop_zone/styles.css +10 -6
  61. package/framework/components/chui_fieldset/fieldset.js +6 -6
  62. package/framework/components/chui_fieldset/styles.css +5 -5
  63. package/framework/components/chui_file_explorer/file_explorer.js +115 -115
  64. package/framework/components/chui_file_explorer/styles.css +6 -6
  65. package/framework/components/chui_form/form.js +11 -11
  66. package/framework/components/chui_gantt/gantt.js +41 -41
  67. package/framework/components/chui_gantt/styles.css +5 -5
  68. package/framework/components/chui_graphs/graphs.js +82 -46
  69. package/framework/components/chui_graphs/styles.css +26 -5
  70. package/framework/components/chui_hero/hero.js +63 -20
  71. package/framework/components/chui_hero/styles.css +176 -7
  72. package/framework/components/chui_html_block/html_block.js +5 -5
  73. package/framework/components/chui_html_block/styles.css +1 -1
  74. package/framework/components/chui_hx/styles.css +2 -2
  75. package/framework/components/chui_icons/icons.js +7 -7
  76. package/framework/components/chui_icons/styles.css +2 -1
  77. package/framework/components/chui_inputs/chui_check_box/styles.css +12 -3
  78. package/framework/components/chui_inputs/chui_chips/chips.js +92 -31
  79. package/framework/components/chui_inputs/chui_chips/styles.css +28 -5
  80. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +20 -15
  81. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  82. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +58 -58
  83. package/framework/components/chui_inputs/chui_combo_box/styles.css +9 -9
  84. package/framework/components/chui_inputs/chui_date/date.js +93 -95
  85. package/framework/components/chui_inputs/chui_date/styles.css +8 -8
  86. package/framework/components/chui_inputs/chui_date_range/date_range.js +22 -22
  87. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  88. package/framework/components/chui_inputs/chui_datetime/datetime.js +12 -12
  89. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  90. package/framework/components/chui_inputs/chui_email/email.js +10 -10
  91. package/framework/components/chui_inputs/chui_email/styles.css +6 -6
  92. package/framework/components/chui_inputs/chui_file/file.js +8 -8
  93. package/framework/components/chui_inputs/chui_file/styles.css +12 -9
  94. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +39 -39
  95. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  96. package/framework/components/chui_inputs/chui_masked/masked.js +44 -44
  97. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  98. package/framework/components/chui_inputs/chui_mention/mention.js +13 -13
  99. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  100. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +147 -137
  101. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +14 -8
  102. package/framework/components/chui_inputs/chui_number/number.js +42 -45
  103. package/framework/components/chui_inputs/chui_number/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_otp/otp.js +32 -32
  105. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_password/password.js +30 -36
  107. package/framework/components/chui_inputs/chui_password/styles.css +7 -7
  108. package/framework/components/chui_inputs/chui_radio_button/styles.css +2 -2
  109. package/framework/components/chui_inputs/chui_rating/rating.js +28 -28
  110. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  111. package/framework/components/chui_inputs/chui_select_box/select_box.js +54 -54
  112. package/framework/components/chui_inputs/chui_select_box/styles.css +10 -10
  113. package/framework/components/chui_inputs/chui_slider/slider.js +14 -14
  114. package/framework/components/chui_inputs/chui_slider/styles.css +5 -4
  115. package/framework/components/chui_inputs/chui_text/styles.css +7 -7
  116. package/framework/components/chui_inputs/chui_text/text.js +13 -17
  117. package/framework/components/chui_inputs/chui_text_area/styles.css +8 -8
  118. package/framework/components/chui_inputs/chui_text_area/text_area.js +23 -23
  119. package/framework/components/chui_inputs/chui_time/styles.css +13 -17
  120. package/framework/components/chui_inputs/chui_time/time.js +35 -24
  121. package/framework/components/chui_inputs/chui_toggle/styles.css +10 -3
  122. package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
  123. package/framework/components/chui_json_editor/json_editor.js +87 -87
  124. package/framework/components/chui_json_editor/styles.css +5 -5
  125. package/framework/components/chui_json_viewer/json_viewer.js +80 -80
  126. package/framework/components/chui_json_viewer/styles.css +6 -6
  127. package/framework/components/chui_kanban/kanban.js +27 -27
  128. package/framework/components/chui_kanban/styles.css +8 -4
  129. package/framework/components/chui_kbd/styles.css +1 -1
  130. package/framework/components/chui_label/label.js +17 -17
  131. package/framework/components/chui_label/styles.css +1 -1
  132. package/framework/components/chui_link/link.js +9 -9
  133. package/framework/components/chui_link/styles.css +11 -4
  134. package/framework/components/chui_list/list.js +11 -11
  135. package/framework/components/chui_list/styles.css +2 -2
  136. package/framework/components/chui_markdown_editor/markdown_editor.js +66 -66
  137. package/framework/components/chui_markdown_editor/styles.css +5 -5
  138. package/framework/components/chui_media/audio.js +240 -246
  139. package/framework/components/chui_media/audio_styles.css +41 -23
  140. package/framework/components/chui_media/image.js +49 -46
  141. package/framework/components/chui_media/image_compare.js +35 -35
  142. package/framework/components/chui_media/image_compare_styles.css +5 -5
  143. package/framework/components/chui_media/image_styles.css +1 -1
  144. package/framework/components/chui_media/video.js +148 -148
  145. package/framework/components/chui_media/video_styles.css +27 -11
  146. package/framework/components/chui_menu_bar/menu_bar.js +54 -54
  147. package/framework/components/chui_menu_bar/styles.css +10 -10
  148. package/framework/components/chui_message_bar/message_bar.js +551 -0
  149. package/framework/components/chui_message_bar/styles.css +248 -0
  150. package/framework/components/chui_modal/modal.js +37 -13
  151. package/framework/components/chui_notification/notification.js +76 -54
  152. package/framework/components/chui_notification/notification_download.js +26 -81
  153. package/framework/components/chui_notification/notification_download_card.js +115 -0
  154. package/framework/components/chui_notification/notification_download_progress.css +2 -2
  155. package/framework/components/chui_notification/notification_download_progress.js +70 -108
  156. package/framework/components/chui_notification/notification_update.js +31 -31
  157. package/framework/components/chui_notification/status_plate.css +3 -3
  158. package/framework/components/chui_notification/styles.css +6 -1
  159. package/framework/components/chui_notification/styles_download.css +1 -1
  160. package/framework/components/chui_notification/styles_update.css +2 -2
  161. package/framework/components/chui_org_chart/org_chart.js +29 -29
  162. package/framework/components/chui_org_chart/styles.css +2 -2
  163. package/framework/components/chui_pagination/pagination.js +3 -3
  164. package/framework/components/chui_pagination/styles.css +1 -1
  165. package/framework/components/chui_paragraph/styles.css +2 -2
  166. package/framework/components/chui_popover/popover.js +29 -23
  167. package/framework/components/chui_popover/styles.css +1 -1
  168. package/framework/components/chui_popups/popups.js +124 -124
  169. package/framework/components/chui_popups/styles.css +5 -5
  170. package/framework/components/chui_print_button/print_button.js +15 -10
  171. package/framework/components/chui_progress_bar/progress_bar.js +34 -12
  172. package/framework/components/chui_progress_bar/styles.css +4 -4
  173. package/framework/components/chui_progress_ring/progress_ring.js +15 -15
  174. package/framework/components/chui_progress_ring/styles.css +1 -1
  175. package/framework/components/chui_qrcode/qrcode.js +12 -12
  176. package/framework/components/chui_query_builder/query_builder.js +46 -46
  177. package/framework/components/chui_query_builder/styles.css +4 -4
  178. package/framework/components/chui_radio_group/styles.css +1 -1
  179. package/framework/components/chui_result/result.js +15 -15
  180. package/framework/components/chui_result/styles.css +3 -3
  181. package/framework/components/chui_scheduler/scheduler.js +74 -74
  182. package/framework/components/chui_scheduler/styles.css +5 -5
  183. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  184. package/framework/components/chui_screen_spinner/styles.css +49 -0
  185. package/framework/components/chui_segmented_control/segmented_control.js +63 -6
  186. package/framework/components/chui_segmented_control/styles.css +32 -6
  187. package/framework/components/chui_settings/settings.js +45 -46
  188. package/framework/components/chui_settings/styles.css +4 -4
  189. package/framework/components/chui_shortcut_help/shortcut_help.js +32 -34
  190. package/framework/components/chui_sidebar_nav/sidebar_nav.js +98 -29
  191. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  192. package/framework/components/chui_slideshow/slideshow.js +85 -85
  193. package/framework/components/chui_slideshow/styles.css +6 -6
  194. package/framework/components/chui_spinner/spinner.js +10 -10
  195. package/framework/components/chui_split_button/split_button.js +30 -12
  196. package/framework/components/chui_split_button/styles.css +21 -5
  197. package/framework/components/chui_splitter/splitter.js +11 -11
  198. package/framework/components/chui_splitter/styles.css +2 -2
  199. package/framework/components/chui_statistic/statistic.js +47 -47
  200. package/framework/components/chui_status_bar/styles.css +2 -2
  201. package/framework/components/chui_stepper/stepper.js +202 -31
  202. package/framework/components/chui_stepper/styles.css +79 -14
  203. package/framework/components/chui_sticky_bar/sticky_bar.js +44 -13
  204. package/framework/components/chui_sticky_bar/styles.css +10 -8
  205. package/framework/components/chui_tabs/chui_tabs.js +74 -26
  206. package/framework/components/chui_tabs/styles.css +40 -12
  207. package/framework/components/chui_text_editor/styles.css +7 -7
  208. package/framework/components/chui_text_editor/text_editor.js +56 -56
  209. package/framework/components/chui_text_editor/text_editor_panel.js +173 -174
  210. package/framework/components/chui_theme_toggle/theme_toggle.js +21 -5
  211. package/framework/components/chui_timeline/styles.css +3 -1
  212. package/framework/components/chui_toast_queue/styles.css +11 -11
  213. package/framework/components/chui_toast_queue/toast_queue.js +30 -27
  214. package/framework/components/chui_toolbar/styles.css +2 -2
  215. package/framework/components/chui_tooltip/styles.css +2 -2
  216. package/framework/components/chui_tooltip/tooltip.js +3 -3
  217. package/framework/components/chui_tour/styles.css +8 -8
  218. package/framework/components/chui_tour/tour.js +72 -72
  219. package/framework/components/chui_transfer/styles.css +6 -6
  220. package/framework/components/chui_transfer/transfer.js +93 -106
  221. package/framework/components/chui_tree_table/styles.css +4 -4
  222. package/framework/components/chui_tree_table/tree_table.js +45 -45
  223. package/framework/components/chui_tree_view/styles.css +10 -8
  224. package/framework/components/chui_tree_view/tree_view.js +185 -183
  225. package/framework/components/chui_user_bar/styles.css +145 -0
  226. package/framework/components/chui_user_bar/user_bar.js +402 -0
  227. package/framework/components/chui_user_list_bar/styles.css +173 -0
  228. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  229. package/framework/components/chui_watermark/watermark.js +17 -17
  230. package/framework/components/chui_webview/webview.js +9 -9
  231. package/framework/components/chui_write_bar/styles.css +172 -0
  232. package/framework/components/chui_write_bar/write_bar.js +320 -0
  233. package/framework/main_window.js +1234 -0
  234. package/framework/modules/chui_a11y/chui_a11y.js +78 -79
  235. package/framework/modules/chui_animations/animations.js +77 -15
  236. package/framework/modules/chui_animations/motion.js +120 -0
  237. package/framework/modules/chui_animations/styles.css +36 -3
  238. package/framework/modules/chui_functions.js +31 -27
  239. package/framework/modules/chui_highlight/styles.css +13 -10
  240. package/framework/modules/chui_i18n/chui_i18n.js +3 -3
  241. package/framework/modules/chui_i18n/locales/feedback.js +62 -0
  242. package/framework/modules/chui_i18n/locales/navigation.js +4 -0
  243. package/framework/modules/chui_json/chui_json.js +2 -2
  244. package/framework/modules/chui_logger/chui_logger.js +23 -8
  245. package/framework/modules/chui_page/page.js +10 -10
  246. package/framework/modules/chui_palette/palette.js +61 -0
  247. package/framework/modules/chui_qr/chui_qr.js +23 -23
  248. package/framework/modules/chui_scrollbar/chui_scrollbar.js +45 -45
  249. package/framework/modules/chui_scrollbar/styles.css +3 -3
  250. package/framework/modules/chui_services/chui_services.js +8 -8
  251. package/framework/modules/chui_services/desktop.js +77 -71
  252. package/framework/modules/chui_sortable/chui_sortable.js +17 -17
  253. package/framework/modules/chui_validate/chui_validate.js +3 -3
  254. package/framework/tray_bar/chui_menu_items.js +2 -2
  255. package/index.d.ts +266 -34
  256. package/index.js +20 -1168
  257. package/package.json +6 -4
  258. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -191
@@ -23,7 +23,7 @@ class ContentBlock {
23
23
  if (this.#disableMarginChild) this.disableMarginChild();
24
24
  }
25
25
  remove(...components) {
26
- for (let component of components) new Animation(component.set()).fadeOutAndRemove();
26
+ for (const component of components) new Animation(component.set()).fadeOutAndRemove();
27
27
  }
28
28
  // FUNCTIONS
29
29
  disableMarginChild() {
@@ -32,7 +32,7 @@ class ContentBlock {
32
32
  this.#body.classList.add("contentblock_no_margin");
33
33
  }
34
34
  add(...components) {
35
- for (let component of components) {
35
+ for (const component of components) {
36
36
  new Animation(component.set()).fadeIn();
37
37
  this.#body.appendChild(component.set());
38
38
  }
@@ -4,18 +4,27 @@ const { MenuKeyboard, A11Y_FOCUSABLE_SELECTOR } = require("../../modules/chui_a1
4
4
 
5
5
  class ContextMenu {
6
6
  #menu = document.createElement("chui_context_menu");
7
+ // Слой пунктов: кадр появления и ухода играет он, а поверхность меню с блюром стоит
8
+ // на месте. Элемент с `transform` или `opacity` становится «backdrop root», и его
9
+ // `backdrop-filter` перестаёт видеть страницу — прежний кадр на самом меню из-за этого
10
+ // шёл без блюра, и блюр появлялся только после конца кадра
11
+ #panel = document.createElement("context_menu_panel");
7
12
  #targets = [];
8
13
  #visible = false;
9
14
  #hideHandler = undefined;
10
15
  #keyboard = undefined;
16
+ #label = undefined;
11
17
  // Цель `attach()`, к которой возвращается фокус после закрытия меню
12
- #return_focus = undefined;
18
+ #returnFocus = undefined;
13
19
  // Где фокус был до открытия меню: цель может быть нефокусируемой обёрткой (chui_button)
14
- #previous_focus = undefined;
20
+ #previousFocus = undefined;
15
21
 
16
22
  constructor(options = { items: [] }) {
17
23
  setStyles(__dirname + "/styles.css", "chUiJS_ContextMenu");
18
- this.#renderItems(this.#menu, options.items);
24
+ // Роль меню лежит на самом слое: `MenuKeyboard` и отбирает пункты по ближайшему меню,
25
+ // и ставит её сам, а слой пунктов нужен только для кадра
26
+ this.#menu.appendChild(this.#panel);
27
+ this.#renderItems(this.#panel, options.items);
19
28
  // Меню — фокусируемый слой: открытие мышью фокуса в нём не оставляет, но клавиши
20
29
  // (стрелки, набор букв) должны попадать в список, а не элементу под курсором
21
30
  this.#menu.setAttribute("tabindex", "-1");
@@ -32,7 +41,14 @@ class ContextMenu {
32
41
 
33
42
  // === СТАТИЧЕСКИЕ ОПИСАНИЯ ПУНКТОВ ===
34
43
  static Item(
35
- options = { title: String(), icon: String(), shortcut: String(), disabled: Boolean(), clickEvent: () => {} }
44
+ options = {
45
+ title: String(),
46
+ icon: String(),
47
+ shortcut: String(),
48
+ disabled: Boolean(),
49
+ danger: Boolean(),
50
+ clickEvent: () => {},
51
+ }
36
52
  ) {
37
53
  return { type: "item", ...options };
38
54
  }
@@ -46,45 +62,50 @@ class ContextMenu {
46
62
  // === ПОСТРОЕНИЕ ===
47
63
  #renderItems(container, items) {
48
64
  if (items === undefined) return;
49
- for (let item of items) {
65
+ for (const item of items) {
50
66
  if (item.type === "separator") {
51
67
  // Разделитель — не пункт меню: роль `separator` говорит об этом и скринридеру
52
- let separator = document.createElement("context_menu_separator");
68
+ const separator = document.createElement("context_menu_separator");
53
69
  separator.setAttribute("role", "separator");
54
70
  container.appendChild(separator);
55
71
  continue;
56
72
  }
57
- let row = document.createElement("context_menu_item");
73
+ const row = document.createElement("context_menu_item");
74
+ row.setAttribute("role", "menuitem");
58
75
  if (item.icon) {
59
- let iconWrap = document.createElement("context_menu_item_icon");
76
+ const iconWrap = document.createElement("context_menu_item_icon");
60
77
  iconWrap.innerHTML = new Icon(item.icon).getHTML();
61
78
  // Значок — украшение: его лигатура не должна попадать ни в имя пункта, ни в набор
62
79
  iconWrap.setAttribute("aria-hidden", "true");
63
80
  row.appendChild(iconWrap);
64
81
  }
65
- let title = document.createElement("context_menu_item_title");
82
+ const title = document.createElement("context_menu_item_title");
66
83
  if (item.title !== undefined) title.innerText = item.title;
67
84
  row.appendChild(title);
68
85
 
69
86
  if (item.type === "submenu") {
87
+ row.setAttribute("aria-haspopup", "menu");
70
88
  if (item.shortcut) {
71
- let shortcut = document.createElement("context_menu_item_shortcut");
89
+ const shortcut = document.createElement("context_menu_item_shortcut");
72
90
  shortcut.innerText = item.shortcut;
73
91
  row.appendChild(shortcut);
74
92
  }
75
- let arrow = document.createElement("context_menu_item_arrow");
93
+ const arrow = document.createElement("context_menu_item_arrow");
76
94
  arrow.innerHTML = new Icon(Icons.NAVIGATION.CHEVRON_RIGHT).getHTML();
77
95
  arrow.setAttribute("aria-hidden", "true");
78
96
  if (!item.shortcut) arrow.classList.add("context_menu_item_arrow_end");
79
97
  row.appendChild(arrow);
80
- let submenu = document.createElement("context_menu_submenu");
81
- this.#renderItems(submenu, item.items);
98
+ const submenu = document.createElement("context_menu_submenu");
99
+ const subpanel = document.createElement("context_menu_panel");
100
+ submenu.setAttribute("role", "menu");
101
+ submenu.appendChild(subpanel);
102
+ this.#renderItems(subpanel, item.items);
82
103
  row.appendChild(submenu);
83
104
  // Подменю — свой уровень меню: Escape/ArrowLeft закрывают его и возвращают фокус на строку
84
105
  new MenuKeyboard(submenu, {
85
106
  itemSelector: "context_menu_item",
86
107
  submenuSelector: "context_menu_submenu",
87
- onSubmenuOpen: (inner_row) => this.#openSubMenu(inner_row),
108
+ onSubmenuOpen: (innerRow) => this.#openSubMenu(innerRow),
88
109
  onEscape: () => this.#closeSubMenu(row),
89
110
  submenu: true,
90
111
  trigger: row,
@@ -100,8 +121,10 @@ class ContextMenu {
100
121
  this.hide();
101
122
  });
102
123
  }
124
+ // Опасный пункт (удалить, очистить) — подписью красного цвета
125
+ if (item.danger === true) row.dataset.danger = "true";
103
126
  if (item.shortcut) {
104
- let shortcut = document.createElement("context_menu_item_shortcut");
127
+ const shortcut = document.createElement("context_menu_item_shortcut");
105
128
  shortcut.innerText = item.shortcut;
106
129
  row.appendChild(shortcut);
107
130
  }
@@ -112,19 +135,19 @@ class ContextMenu {
112
135
 
113
136
  // === ПОДМЕНЮ ===
114
137
  #openSubMenu(row) {
115
- let submenu = row.querySelector("context_menu_submenu");
138
+ const submenu = row.querySelector("context_menu_submenu");
116
139
  if (submenu === null) return;
117
140
  // Закрыть соседние подменю того же уровня
118
- let parent = row.parentElement;
119
- for (let child of parent.children) {
141
+ const parent = row.parentElement;
142
+ for (const child of parent.children) {
120
143
  if (child === row) continue;
121
- let sibling = child.querySelector("context_menu_submenu");
144
+ const sibling = child.querySelector("context_menu_submenu");
122
145
  if (sibling !== null) sibling.style.display = "none";
123
146
  }
124
147
  submenu.style.display = "flex";
125
148
  submenu.style.left = "100%";
126
149
  submenu.style.top = "0px";
127
- let rowRect = row.getBoundingClientRect();
150
+ const rowRect = row.getBoundingClientRect();
128
151
  let subRect = submenu.getBoundingClientRect();
129
152
  // Флип влево у правого края окна
130
153
  if (subRect.right > window.innerWidth - 8) {
@@ -132,41 +155,97 @@ class ContextMenu {
132
155
  subRect = submenu.getBoundingClientRect();
133
156
  }
134
157
  // Флип вверх у нижнего края окна
135
- let overflow = rowRect.top + subRect.height - (window.innerHeight - 8);
158
+ const overflow = rowRect.top + subRect.height - (window.innerHeight - 8);
136
159
  if (overflow > 0) {
137
- let shift = Math.min(overflow, rowRect.top - 8);
160
+ const shift = Math.min(overflow, rowRect.top - 8);
138
161
  submenu.style.top = -shift + "px";
139
162
  }
163
+ // Кадр — после расстановки: он начинается с уменьшенного состояния, а по нему положение
164
+ // и высоту уже не измерить. Перезапускать явно не нужно: панель до этого была скрыта
165
+ // (`display: none`), поэтому при показе кадр начинается заново. Кадр играет слой пунктов,
166
+ // поверхность подменю стоит на месте — так блюр видно с первого кадра
167
+ const panel = submenu.querySelector("context_menu_panel");
168
+ if (panel !== null) this.#play(panel, "context-scale-in", "enter");
169
+ this.#plate(submenu, "enter");
170
+ }
171
+
172
+ /**
173
+ * Снять вид движения по концу кадра появления.
174
+ *
175
+ * Вид держит конечное состояние (`animation-fill-mode: both`), поэтому на слое остаётся
176
+ * вычисленный `transform` — пусть и единичный. Элемент с `transform` становится собственным
177
+ * «backdrop root», и размытие подложки перестаёт видеть содержимое под слоем: меню шло
178
+ * прозрачной подложкой без блюра. После кадра вид снимается, а `animation-name` остаётся:
179
+ * по нему видно, что узел уже открыт.
180
+ */
181
+ #stripMotion(element = new HTMLElement()) {
182
+ const done = (event) => {
183
+ if (event.target !== element) return;
184
+ element.removeEventListener("animationend", done);
185
+ element.removeAttribute("data-motion");
186
+ };
187
+ element.addEventListener("animationend", done);
140
188
  }
141
189
  #closeSubMenu(row) {
142
- let submenu = row.querySelector("context_menu_submenu");
190
+ const submenu = row.querySelector("context_menu_submenu");
143
191
  if (submenu !== null) submenu.style.display = "none";
144
192
  }
145
193
 
194
+ /**
195
+ * Замена пунктов: меню собирается заново. Нужно там, где меню строят по тому, на чём его
196
+ * открыли: список разговоров собирает пункты по выбранной строке («Отметить прочитанным»
197
+ * есть только у строки с непрочитанными) — для этого подходит и функция-строитель
198
+ */
199
+ setItems(items = []) {
200
+ this.#panel.innerHTML = "";
201
+ this.#renderItems(this.#panel, items);
202
+ this.#keyboard.refresh();
203
+ return this;
204
+ }
205
+ /** Имя меню для скринридера: оно нужно слою с ролью меню, то есть самому меню */
206
+ setLabel(text = String()) {
207
+ this.#label = String(text);
208
+ if (this.#label === "") this.#menu.removeAttribute("aria-label");
209
+ else this.#menu.setAttribute("aria-label", this.#label);
210
+ return this;
211
+ }
212
+ getLabel() {
213
+ return this.#label === undefined ? "" : this.#label;
214
+ }
215
+ /**
216
+ * Слой пунктов. Пункты лежат в нём, а не прямо в меню, только ради кадра: кадр появления
217
+ * с прозрачностью и усадкой должен играть внутри, а поверхность меню с блюром — стоять
218
+ * недвижимо. Роли на нём нет: меню для скринридера — сам слой (`role="menu"` ставит
219
+ * `MenuKeyboard`), и по нему же клавиатура отбирает свои пункты
220
+ */
221
+ getPanel() {
222
+ return this.#panel;
223
+ }
224
+
146
225
  // === ПОКАЗ / СКРЫТИЕ ===
147
226
  attach(target) {
148
- let element = typeof target.set === "function" ? target.set() : target;
149
- let listener = (event) => {
227
+ const element = typeof target.set === "function" ? target.set() : target;
228
+ const listener = (event) => {
150
229
  event.preventDefault();
151
- this.#return_focus = event.currentTarget === null ? element : event.currentTarget;
230
+ this.#returnFocus = event.currentTarget === null ? element : event.currentTarget;
152
231
  this.show(event.clientX, event.clientY);
153
232
  };
154
233
  // Клавиатура: клавиша ContextMenu или Shift+F10 открывают меню у элемента
155
- let key_listener = (event) => {
234
+ const keyListener = (event) => {
156
235
  if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10")) return;
157
236
  event.preventDefault();
158
- this.#return_focus = element;
159
- let rect = element.getBoundingClientRect();
237
+ this.#returnFocus = element;
238
+ const rect = element.getBoundingClientRect();
160
239
  this.show(rect.left + 8, rect.top + rect.height / 2);
161
240
  this.#keyboard.focusFirst();
162
241
  };
163
242
  element.addEventListener("contextmenu", listener);
164
- element.addEventListener("keydown", key_listener);
165
- this.#targets.push({ element: element, listener: listener, key_listener: key_listener });
243
+ element.addEventListener("keydown", keyListener);
244
+ this.#targets.push({ element: element, listener: listener, key_listener: keyListener });
166
245
  return this;
167
246
  }
168
247
  detach(target) {
169
- let element = typeof target.set === "function" ? target.set() : target;
248
+ const element = typeof target.set === "function" ? target.set() : target;
170
249
  for (let i = 0; i < this.#targets.length; i++) {
171
250
  if (this.#targets[i].element === element) {
172
251
  element.removeEventListener("contextmenu", this.#targets[i].listener);
@@ -179,7 +258,7 @@ class ContextMenu {
179
258
  show(x = 0, y = 0) {
180
259
  this.#cancelPendingHide();
181
260
  // Сбросить геометрию подменю от прошлого показа
182
- for (let submenu of this.#menu.querySelectorAll("context_menu_submenu")) {
261
+ for (const submenu of this.#menu.querySelectorAll("context_menu_submenu")) {
183
262
  submenu.style.display = "none";
184
263
  submenu.style.left = "100%";
185
264
  submenu.style.top = "0px";
@@ -200,22 +279,24 @@ class ContextMenu {
200
279
  this.#menu.style.left = "0px";
201
280
  this.#menu.style.top = "0px";
202
281
  // Позиция с учётом краёв окна
203
- let left = Math.max(8, Math.min(x, window.innerWidth - this.#menu.offsetWidth - 8));
204
- let top = Math.max(8, Math.min(y, window.innerHeight - this.#menu.offsetHeight - 8));
282
+ const left = Math.max(8, Math.min(x, window.innerWidth - this.#menu.offsetWidth - 8));
283
+ const top = Math.max(8, Math.min(y, window.innerHeight - this.#menu.offsetHeight - 8));
205
284
  this.#menu.style.left = left + "px";
206
285
  this.#menu.style.top = top + "px";
207
286
  // Зафиксировать финальную геометрию до показа
208
287
  void this.#menu.offsetWidth;
209
- // Показ и перезапуск анимации уже в финальной позиции
288
+ // Показ и перезапуск анимации уже в финальной позиции: кадр играет слой пунктов,
289
+ // а поверхность растворяется своим кадром по псевдоэлементу — ни `transform`, ни
290
+ // смена размера у неё не появляются, поэтому блюр подложки виден от начала и до конца,
291
+ // а рамка ни на миг не оказывается уже пунктов
210
292
  this.#menu.style.visibility = "visible";
211
- this.#menu.style.animationName = "none";
212
- void this.#menu.offsetWidth;
213
- this.#menu.style.animationName = "context-scale-in";
293
+ this.#plate(this.#menu, "enter");
294
+ this.#play(this.#panel, "context-scale-in", "enter");
214
295
  // Забрать фокус меню: без этого после правого клика стрелки и набор букв уходили бы
215
296
  // элементу под курсором. Пункт при этом не подсвечивается — набор начнётся с найденного.
216
297
  if (!this.#menu.contains(document.activeElement)) {
217
298
  // Где фокус был до открытия: по нему меню вернёт его, если сама цель нефокусируема
218
- this.#previous_focus = document.activeElement;
299
+ this.#previousFocus = document.activeElement;
219
300
  this.#menu.focus();
220
301
  }
221
302
  this.#keyboard.refresh();
@@ -230,18 +311,44 @@ class ContextMenu {
230
311
  window.removeEventListener("keydown", this.#onWindowKeyDown);
231
312
  window.removeEventListener("resize", this.#onWindowResize);
232
313
  window.removeEventListener("blur", this.#onWindowBlur);
233
- // Анимация скрытия
234
- this.#menu.style.animationName = "none";
235
- void this.#menu.offsetWidth;
236
- this.#menu.style.animationName = "context-scale-out";
314
+ // Анимация скрытия: уход играет слой пунктов, поверхность убирается тем же кругом
315
+ this.#plate(this.#menu, "exit");
316
+ this.#play(this.#panel, "context-scale-out", "exit");
237
317
  this.#hideHandler = (event) => {
238
- if (event.target !== this.#menu) return;
239
- this.#menu.removeEventListener("animationend", this.#hideHandler);
318
+ if (event.target !== this.#panel) return;
319
+ this.#panel.removeEventListener("animationend", this.#hideHandler);
320
+ this.#stop(this.#menu);
321
+ this.#stop(this.#panel);
240
322
  this.#menu.style.display = "none";
241
- this.#menu.style.removeProperty("animation-name");
242
323
  this.#hideHandler = undefined;
243
324
  };
244
- this.#menu.addEventListener("animationend", this.#hideHandler);
325
+ this.#panel.addEventListener("animationend", this.#hideHandler);
326
+ }
327
+ /** Пустить кадр по слою: имя, вид движения и снятие вида по концу кадра */
328
+ #play(element, frame, motion) {
329
+ element.style.animationName = "none";
330
+ void element.offsetWidth;
331
+ element.style.animationName = frame;
332
+ element.dataset.motion = motion;
333
+ this.#stripMotion(element);
334
+ }
335
+ /**
336
+ * Кадр поверхности. На самом слое его нет намеренно: с `transform` слой становится
337
+ * «backdrop root» и перестаёт видеть страницу — блюр подложки пропадал бы до конца кадра.
338
+ * Поэтому кадр идёт по псевдоэлементу поверхности (`::before`) и только по прозрачности:
339
+ * он ничего не двигает и не меняет размер, так что рамка ни на миг не оказывается уже
340
+ * пунктов, а блюр виден всю дорогу. Вид движения на слое открывает кадру его время и кривую
341
+ */
342
+ #plate(element, motion) {
343
+ element.removeAttribute("data-motion");
344
+ void element.offsetWidth;
345
+ element.dataset.motion = motion;
346
+ this.#stripMotion(element);
347
+ }
348
+ /** Снять следы кадра: имя и вид движения с инлайновых стилей и атрибутов */
349
+ #stop(element) {
350
+ element.style.removeProperty("animation-name");
351
+ element.removeAttribute("data-motion");
245
352
  }
246
353
  /**
247
354
  * Вернуть фокус после закрытия. Сначала — на цель `attach()`; сама цель бывает
@@ -249,7 +356,7 @@ class ContextMenu {
249
356
  * где он был до открытия меню (правый клик фокусирует внутреннюю кнопку сам).
250
357
  */
251
358
  #restoreFocus() {
252
- const target = this.#return_focus;
359
+ const target = this.#returnFocus;
253
360
  if (
254
361
  target !== undefined &&
255
362
  target !== null &&
@@ -260,23 +367,24 @@ class ContextMenu {
260
367
  target.focus();
261
368
  return;
262
369
  }
263
- const previous = this.#previous_focus;
264
- this.#previous_focus = undefined;
370
+ const previous = this.#previousFocus;
371
+ this.#previousFocus = undefined;
265
372
  if (previous === undefined || previous === null || previous === document.body) return;
266
373
  if (this.#menu.contains(previous) || !document.body.contains(previous)) return;
267
374
  if (typeof previous.focus === "function") previous.focus();
268
375
  }
269
376
  #cancelPendingHide() {
270
377
  if (this.#hideHandler !== undefined) {
271
- this.#menu.removeEventListener("animationend", this.#hideHandler);
272
- this.#menu.style.removeProperty("animation-name");
378
+ this.#panel.removeEventListener("animationend", this.#hideHandler);
379
+ this.#stop(this.#menu);
380
+ this.#stop(this.#panel);
273
381
  this.#hideHandler = undefined;
274
382
  }
275
383
  }
276
384
  destroy() {
277
385
  this.hide();
278
386
  this.#keyboard.destroy();
279
- for (let target of this.#targets) {
387
+ for (const target of this.#targets) {
280
388
  target.element.removeEventListener("contextmenu", target.listener);
281
389
  target.element.removeEventListener("keydown", target.key_listener);
282
390
  }
@@ -1,8 +1,10 @@
1
1
  /* ── ContextMenu — контекстное меню ───────────────────────── */
2
+ /* Кадры слоя пунктов: прозрачность и усадка. Слой пунктов не носит блюра, поэтому его
3
+ `opacity` и `transform` блюру поверхности не мешают */
2
4
  @keyframes context-scale-in {
3
5
  from {
4
6
  opacity: 0;
5
- transform: scale(0.92);
7
+ transform: scale(0.96);
6
8
  }
7
9
  to {
8
10
  opacity: 1;
@@ -16,7 +18,22 @@
16
18
  }
17
19
  to {
18
20
  opacity: 0;
19
- transform: scale(0.92);
21
+ transform: scale(0.96);
22
+ }
23
+ }
24
+ /* Кадры поверхности: растворение на месте. Размер она не меняет намеренно: с ростом по `inset`
25
+ рамка оказывалась меньше пунктов — на уходе это видно как вылезающая за кромку подпись
26
+ (замер: на 60-й мс ухода поверхность была на 7px меньше пунктов, а последний пункт ещё
27
+ не погас). Кадр идёт по псевдоэлементу и только по прозрачности: `transform` на нём или
28
+ на его предке сделал бы слой «backdrop root», и блюр подложки пропал бы до конца кадра */
29
+ @keyframes context-plate-in {
30
+ from {
31
+ opacity: 0;
32
+ }
33
+ }
34
+ @keyframes context-plate-out {
35
+ to {
36
+ opacity: 0;
20
37
  }
21
38
  }
22
39
 
@@ -32,13 +49,56 @@ context_menu_submenu {
32
49
  width: max-content;
33
50
  min-width: 180px;
34
51
  height: max-content;
35
- padding: var(--margin);
52
+ padding: var(--block_margin);
36
53
  background-color: transparent;
37
54
  color: var(--text_color);
38
55
  border: none;
39
56
  font-size: var(--font_default_size);
40
57
  }
41
58
 
59
+ /* Слой пунктов: в нём живут пункты меню и подменю, и он же — само меню для скринридера.
60
+ Кадр появления играет он, поэтому поверхность меню остаётся неанимированной */
61
+ context_menu_panel {
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: stretch;
65
+ outline: none;
66
+ }
67
+
68
+ /* Вид движения ставит context_menu.js: время и кривая — те же, что у остальных всплывающих
69
+ слоёв, а уход короче входа. Кадр играет слой пунктов: он не носит блюра, и его прозрачность
70
+ с усадкой поверхности меню не мешают */
71
+ context_menu_panel[data-motion="enter"] {
72
+ animation-duration: var(--motion_base);
73
+ animation-timing-function: var(--motion_ease);
74
+ animation-fill-mode: both;
75
+ }
76
+
77
+ context_menu_panel[data-motion="exit"] {
78
+ animation-duration: var(--motion_fast);
79
+ animation-timing-function: var(--motion_ease);
80
+ animation-fill-mode: both;
81
+ }
82
+
83
+ /* Поверхность растворяется своим кадром: он идёт по псевдоэлементу и только по прозрачности,
84
+ поэтому сама поверхность не двигается и не меняет размер — блюр подложки виден всю дорогу,
85
+ а рамка ни на миг не оказывается уже пунктов */
86
+ chui_context_menu[data-motion="enter"]::before,
87
+ context_menu_submenu[data-motion="enter"]::before {
88
+ animation-name: context-plate-in;
89
+ animation-duration: var(--motion_base);
90
+ animation-timing-function: var(--motion_ease);
91
+ animation-fill-mode: both;
92
+ }
93
+
94
+ chui_context_menu[data-motion="exit"]::before,
95
+ context_menu_submenu[data-motion="exit"]::before {
96
+ animation-name: context-plate-out;
97
+ animation-duration: var(--motion_fast);
98
+ animation-timing-function: var(--motion_ease);
99
+ animation-fill-mode: both;
100
+ }
101
+
42
102
  chui_context_menu::before,
43
103
  context_menu_submenu::before {
44
104
  content: "";
@@ -47,7 +107,7 @@ context_menu_submenu::before {
47
107
  z-index: -1;
48
108
  background-color: var(--header_background);
49
109
  border: 1px solid var(--border_color);
50
- border-radius: var(--border_radius);
110
+ border-radius: var(--radius_sm);
51
111
  /* Меню — всплывающий слой: тень отрыва (как у меню SplitButton и палитры),
52
112
  а не контактная тень карточки */
53
113
  box-shadow: var(--box_shadow_drop);
@@ -56,7 +116,8 @@ context_menu_submenu::before {
56
116
  pointer-events: none;
57
117
  }
58
118
 
59
- chui_context_menu {
119
+ chui_context_menu,
120
+ context_menu_submenu {
60
121
  position: fixed;
61
122
  z-index: 5000;
62
123
  transform-origin: top left;
@@ -87,8 +148,8 @@ context_menu_item {
87
148
  white-space: nowrap;
88
149
  cursor: pointer;
89
150
  /* Компактный шаг контрола: 5px по вертикали дают те же 29px, что у пунктов меню */
90
- padding: var(--main_padding_sm);
91
- border-radius: var(--border_radius);
151
+ padding: var(--block_padding_sm);
152
+ border-radius: var(--radius_sm);
92
153
  color: var(--text_color);
93
154
  gap: 6px;
94
155
  box-sizing: border-box;
@@ -160,7 +221,7 @@ context_menu_item_shortcut {
160
221
  }
161
222
 
162
223
  context_menu_item:hover context_menu_item_shortcut {
163
- color: var(--text_color_hover_disable);
224
+ color: var(--text_color_hover_disabled);
164
225
  }
165
226
 
166
227
  context_menu_separator {
@@ -169,6 +230,19 @@ context_menu_separator {
169
230
  background: var(--border_color);
170
231
  }
171
232
 
233
+ /* Опасный пункт (удалить, очистить) — подписью и значком красного цвета: значок
234
+ часть подписи, и красный текст с обычным значком читались бы нарочно недоведёнными */
235
+ context_menu_item[data-danger="true"] context_menu_item_title,
236
+ context_menu_item[data-danger="true"] context_menu_item_icon chui_icon {
237
+ color: var(--red_prime_background);
238
+ }
239
+
240
+ /* Наведение не должно гасить красноту: правило `:hover chui_icon` ниже по файлу
241
+ иначе перекрашивало значок опасного пункта в обычный цвет */
242
+ context_menu_item[data-danger="true"]:hover chui_icon {
243
+ color: var(--red_prime_background);
244
+ }
245
+
172
246
  .context_menu_item_disabled {
173
247
  cursor: default;
174
248
  opacity: 0.5;
@@ -1,5 +1,5 @@
1
1
  class CustomElement {
2
- #chui_custom_element = undefined;
2
+ #chuiCustomElement = undefined;
3
3
  constructor(
4
4
  options = {
5
5
  tag: String(),
@@ -12,27 +12,27 @@ class CustomElement {
12
12
  // Пустая заготовка опции равносильна отсутствующей: `createElement("")` падал TypeError
13
13
  const tag = optionOr(options.tag, undefined);
14
14
  if (tag === undefined) throw new Error("Не установлен тэг элемента");
15
- this.#chui_custom_element = document.createElement(tag);
15
+ this.#chuiCustomElement = document.createElement(tag);
16
16
  const id = optionOr(options.id, undefined);
17
17
  if (id === undefined) throw new Error("Не установлен идентификатор элемента");
18
18
  // Файл стилей — необязательная опция: пустой путь читался как файл и падал ENOENT
19
- const path_to_css = optionOr(options.pathToCSS, undefined);
20
- if (path_to_css !== undefined) setStyles(path_to_css, `chUiJS_CustomElement_${id}`);
21
- this.#chui_custom_element.id = id;
22
- const class_name = optionOr(options.className, undefined);
23
- if (class_name !== undefined) this.#chui_custom_element.className = class_name;
19
+ const pathToCss = optionOr(options.pathToCSS, undefined);
20
+ if (pathToCss !== undefined) setStyles(pathToCss, `chUiJS_CustomElement_${id}`);
21
+ this.#chuiCustomElement.id = id;
22
+ const className = optionOr(options.className, undefined);
23
+ if (className !== undefined) this.#chuiCustomElement.className = className;
24
24
  }
25
25
  addEventListener(type = String(), listener = () => {}) {
26
- this.#chui_custom_element.addEventListener(type, listener);
26
+ this.#chuiCustomElement.addEventListener(type, listener);
27
27
  }
28
28
  innerText(innerText = String()) {
29
- this.#chui_custom_element.innerText = innerText;
29
+ this.#chuiCustomElement.innerText = innerText;
30
30
  }
31
31
  innerHTML(innerHTML = String()) {
32
- this.#chui_custom_element.innerHTML = innerHTML;
32
+ this.#chuiCustomElement.innerHTML = innerHTML;
33
33
  }
34
34
  set() {
35
- return this.#chui_custom_element;
35
+ return this.#chuiCustomElement;
36
36
  }
37
37
  }
38
38