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.
- package/CHANGELOG.md +597 -0
- package/README.md +43 -644
- package/framework/appLayout/default/chui_app_layout.js +602 -496
- package/framework/appLayout/default/global_style.css +288 -97
- package/framework/appLayout/default/main_theme_style.css +188 -101
- package/framework/components/chui_accordion/accordion.js +38 -35
- package/framework/components/chui_accordion/styles.css +13 -12
- package/framework/components/chui_action_sheet/action_sheet.js +93 -64
- package/framework/components/chui_action_sheet/styles.css +12 -5
- package/framework/components/chui_actions_bar/actions_bar.js +338 -0
- package/framework/components/chui_actions_bar/styles.css +139 -0
- package/framework/components/chui_affix/affix.js +16 -11
- package/framework/components/chui_affix/styles.css +5 -0
- package/framework/components/chui_alert/alert.js +34 -19
- package/framework/components/chui_alert/styles.css +7 -0
- package/framework/components/chui_avatar/avatar.js +36 -35
- package/framework/components/chui_avatar/styles.css +1 -1
- package/framework/components/chui_avatar_group/avatar_group.js +25 -23
- package/framework/components/chui_avatar_group/styles.css +1 -1
- package/framework/components/chui_backtop/backtop.js +60 -21
- package/framework/components/chui_backtop/styles.css +21 -0
- package/framework/components/chui_badge/badge.js +30 -28
- package/framework/components/chui_badge/styles.css +6 -6
- package/framework/components/chui_banner/banner.js +222 -0
- package/framework/components/chui_banner/styles.css +170 -0
- package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
- package/framework/components/chui_breadcrumb/styles.css +1 -1
- package/framework/components/chui_button/button.js +52 -36
- package/framework/components/chui_button/styles.css +7 -7
- package/framework/components/chui_calendar/calendar.js +18 -16
- package/framework/components/chui_card/card.js +52 -32
- package/framework/components/chui_card_scroll/card_scroll.js +169 -0
- package/framework/components/chui_card_scroll/styles.css +86 -0
- package/framework/components/chui_chat/chat.js +140 -79
- package/framework/components/chui_chat/styles.css +18 -7
- package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
- package/framework/components/chui_clipboard_history/styles.css +13 -9
- package/framework/components/chui_code/code.js +10 -10
- package/framework/components/chui_code/styles.css +4 -4
- package/framework/components/chui_code_editor/code_editor.js +73 -57
- package/framework/components/chui_code_editor/styles.css +6 -5
- package/framework/components/chui_command_palette/command_palette.js +87 -64
- package/framework/components/chui_command_palette/styles.css +5 -5
- package/framework/components/chui_console/console.js +13 -13
- package/framework/components/chui_console/styles.css +6 -3
- package/framework/components/chui_content_block/content_block.js +35 -21
- package/framework/components/chui_content_block/styles.css +2 -2
- package/framework/components/chui_context_menu/context_menu.js +171 -58
- package/framework/components/chui_context_menu/styles.css +82 -8
- package/framework/components/chui_custom_element/custom_element.js +25 -18
- package/framework/components/chui_data_grid/data_grid.js +633 -471
- package/framework/components/chui_data_grid/styles.css +24 -22
- package/framework/components/chui_descriptions/descriptions.js +34 -17
- package/framework/components/chui_descriptions/styles.css +1 -1
- package/framework/components/chui_details/details.js +42 -36
- package/framework/components/chui_details/styles.css +12 -11
- package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
- package/framework/components/chui_diff_viewer/styles.css +10 -10
- package/framework/components/chui_divider/divider.js +19 -17
- package/framework/components/chui_divider/styles.css +2 -2
- package/framework/components/chui_drawer/drawer.js +63 -38
- package/framework/components/chui_drawer/styles.css +18 -4
- package/framework/components/chui_drop_zone/drop_zone.js +37 -27
- package/framework/components/chui_drop_zone/styles.css +10 -6
- package/framework/components/chui_empty_state/empty_state.js +20 -15
- package/framework/components/chui_fieldset/fieldset.js +40 -23
- package/framework/components/chui_fieldset/styles.css +6 -6
- package/framework/components/chui_file_explorer/file_explorer.js +260 -189
- package/framework/components/chui_file_explorer/styles.css +8 -7
- package/framework/components/chui_form/form.js +40 -28
- package/framework/components/chui_form/styles.css +1 -1
- package/framework/components/chui_gantt/gantt.js +102 -78
- package/framework/components/chui_gantt/styles.css +5 -5
- package/framework/components/chui_graphs/graphs.js +376 -239
- package/framework/components/chui_graphs/styles.css +27 -6
- package/framework/components/chui_hero/hero.js +98 -37
- package/framework/components/chui_hero/styles.css +176 -7
- package/framework/components/chui_html_block/html_block.js +8 -8
- package/framework/components/chui_html_block/styles.css +3 -3
- package/framework/components/chui_hx/hx.js +3 -3
- package/framework/components/chui_hx/styles.css +13 -6
- package/framework/components/chui_icons/icons.js +2115 -2115
- package/framework/components/chui_icons/styles.css +4 -3
- package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
- package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
- package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
- package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
- package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
- package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
- package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
- package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
- package/framework/components/chui_inputs/chui_date/date.js +161 -127
- package/framework/components/chui_inputs/chui_date/styles.css +20 -19
- package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
- package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
- package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
- package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
- package/framework/components/chui_inputs/chui_email/email.js +48 -36
- package/framework/components/chui_inputs/chui_email/styles.css +11 -11
- package/framework/components/chui_inputs/chui_file/file.js +71 -44
- package/framework/components/chui_inputs/chui_file/styles.css +23 -19
- package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
- package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
- package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
- package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
- package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
- package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
- package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
- package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
- package/framework/components/chui_inputs/chui_number/number.js +84 -70
- package/framework/components/chui_inputs/chui_number/styles.css +13 -13
- package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
- package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
- package/framework/components/chui_inputs/chui_password/password.js +72 -56
- package/framework/components/chui_inputs/chui_password/styles.css +15 -15
- package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
- package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
- package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
- package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
- package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
- package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
- package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
- package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
- package/framework/components/chui_inputs/chui_text/styles.css +14 -14
- package/framework/components/chui_inputs/chui_text/text.js +69 -52
- package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
- package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
- package/framework/components/chui_inputs/chui_time/styles.css +18 -20
- package/framework/components/chui_inputs/chui_time/time.js +71 -49
- package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
- package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
- package/framework/components/chui_json_editor/json_editor.js +137 -127
- package/framework/components/chui_json_editor/styles.css +5 -5
- package/framework/components/chui_json_viewer/json_viewer.js +133 -114
- package/framework/components/chui_json_viewer/styles.css +9 -8
- package/framework/components/chui_kanban/kanban.js +78 -60
- package/framework/components/chui_kanban/styles.css +8 -4
- package/framework/components/chui_kbd/kbd.js +12 -10
- package/framework/components/chui_kbd/styles.css +1 -1
- package/framework/components/chui_label/label.js +31 -29
- package/framework/components/chui_label/styles.css +3 -3
- package/framework/components/chui_link/link.js +36 -29
- package/framework/components/chui_link/styles.css +11 -4
- package/framework/components/chui_list/list.js +42 -38
- package/framework/components/chui_list/styles.css +4 -3
- package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
- package/framework/components/chui_markdown_editor/styles.css +5 -5
- package/framework/components/chui_media/audio.js +457 -364
- package/framework/components/chui_media/audio_styles.css +74 -37
- package/framework/components/chui_media/image.js +99 -64
- package/framework/components/chui_media/image_compare.js +86 -55
- package/framework/components/chui_media/image_compare_styles.css +5 -5
- package/framework/components/chui_media/image_styles.css +4 -4
- package/framework/components/chui_media/video.js +264 -171
- package/framework/components/chui_media/video_styles.css +57 -24
- package/framework/components/chui_menu_bar/menu_bar.js +67 -65
- package/framework/components/chui_menu_bar/styles.css +14 -12
- package/framework/components/chui_message_bar/message_bar.js +551 -0
- package/framework/components/chui_message_bar/styles.css +248 -0
- package/framework/components/chui_modal/modal.js +81 -56
- package/framework/components/chui_modal/styles.css +3 -2
- package/framework/components/chui_notification/notification.js +107 -69
- package/framework/components/chui_notification/notification_download.js +28 -77
- package/framework/components/chui_notification/notification_download_card.js +115 -0
- package/framework/components/chui_notification/notification_download_progress.css +5 -4
- package/framework/components/chui_notification/notification_download_progress.js +73 -105
- package/framework/components/chui_notification/notification_update.js +44 -37
- package/framework/components/chui_notification/status_plate.css +3 -3
- package/framework/components/chui_notification/status_plate.js +2 -2
- package/framework/components/chui_notification/styles.css +6 -1
- package/framework/components/chui_notification/styles_download.css +2 -2
- package/framework/components/chui_notification/styles_update.css +3 -3
- package/framework/components/chui_org_chart/org_chart.js +101 -62
- package/framework/components/chui_org_chart/styles.css +2 -2
- package/framework/components/chui_pagination/pagination.js +65 -30
- package/framework/components/chui_pagination/styles.css +1 -1
- package/framework/components/chui_paragraph/paragraph.js +2 -2
- package/framework/components/chui_paragraph/styles.css +3 -3
- package/framework/components/chui_popover/popover.js +77 -50
- package/framework/components/chui_popover/styles.css +1 -1
- package/framework/components/chui_popups/popups.js +210 -181
- package/framework/components/chui_popups/styles.css +7 -7
- package/framework/components/chui_print_button/print_button.js +48 -33
- package/framework/components/chui_progress_bar/progress_bar.js +49 -19
- package/framework/components/chui_progress_bar/styles.css +6 -6
- package/framework/components/chui_progress_ring/progress_ring.js +32 -30
- package/framework/components/chui_progress_ring/styles.css +1 -1
- package/framework/components/chui_qrcode/qrcode.js +85 -41
- package/framework/components/chui_query_builder/query_builder.js +263 -170
- package/framework/components/chui_query_builder/styles.css +4 -4
- package/framework/components/chui_radio_group/radio_group.js +36 -26
- package/framework/components/chui_radio_group/styles.css +2 -2
- package/framework/components/chui_result/result.js +41 -31
- package/framework/components/chui_result/styles.css +3 -3
- package/framework/components/chui_scheduler/scheduler.js +126 -104
- package/framework/components/chui_scheduler/styles.css +11 -7
- package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
- package/framework/components/chui_screen_spinner/styles.css +49 -0
- package/framework/components/chui_segmented_control/segmented_control.js +90 -27
- package/framework/components/chui_segmented_control/styles.css +32 -6
- package/framework/components/chui_settings/settings.js +160 -87
- package/framework/components/chui_settings/styles.css +4 -4
- package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
- package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
- package/framework/components/chui_sidebar_nav/styles.css +9 -3
- package/framework/components/chui_skeleton/skeleton.js +29 -24
- package/framework/components/chui_slideshow/slideshow.js +122 -103
- package/framework/components/chui_slideshow/styles.css +22 -16
- package/framework/components/chui_spinner/spinner.js +17 -17
- package/framework/components/chui_spinner/styles.css +0 -1
- package/framework/components/chui_split_button/split_button.js +53 -30
- package/framework/components/chui_split_button/styles.css +22 -6
- package/framework/components/chui_splitter/splitter.js +46 -39
- package/framework/components/chui_splitter/styles.css +2 -2
- package/framework/components/chui_statistic/statistic.js +111 -84
- package/framework/components/chui_status_bar/status_bar.js +11 -9
- package/framework/components/chui_status_bar/styles.css +2 -2
- package/framework/components/chui_stepper/stepper.js +230 -54
- package/framework/components/chui_stepper/styles.css +79 -14
- package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
- package/framework/components/chui_sticky_bar/styles.css +11 -7
- package/framework/components/chui_tabs/chui_tabs.js +100 -46
- package/framework/components/chui_tabs/styles.css +43 -15
- package/framework/components/chui_text_editor/styles.css +24 -23
- package/framework/components/chui_text_editor/text_editor.js +103 -79
- package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
- package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
- package/framework/components/chui_timeline/styles.css +3 -1
- package/framework/components/chui_timeline/timeline.js +14 -12
- package/framework/components/chui_toast_queue/styles.css +11 -11
- package/framework/components/chui_toast_queue/toast_queue.js +82 -73
- package/framework/components/chui_toolbar/styles.css +2 -2
- package/framework/components/chui_toolbar/toolbar.js +20 -13
- package/framework/components/chui_tooltip/styles.css +2 -2
- package/framework/components/chui_tooltip/tooltip.js +40 -27
- package/framework/components/chui_tour/styles.css +12 -7
- package/framework/components/chui_tour/tour.js +132 -98
- package/framework/components/chui_transfer/styles.css +8 -7
- package/framework/components/chui_transfer/transfer.js +143 -123
- package/framework/components/chui_tree_table/styles.css +4 -4
- package/framework/components/chui_tree_table/tree_table.js +88 -74
- package/framework/components/chui_tree_view/styles.css +14 -10
- package/framework/components/chui_tree_view/tree_view.js +298 -253
- package/framework/components/chui_user_bar/styles.css +145 -0
- package/framework/components/chui_user_bar/user_bar.js +402 -0
- package/framework/components/chui_user_list_bar/styles.css +173 -0
- package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
- package/framework/components/chui_watermark/watermark.js +41 -33
- package/framework/components/chui_webview/styles.css +2 -2
- package/framework/components/chui_webview/webview.js +64 -52
- package/framework/components/chui_write_bar/styles.css +172 -0
- package/framework/components/chui_write_bar/write_bar.js +320 -0
- package/framework/main_window.js +1234 -0
- package/framework/modules/chui_a11y/chui_a11y.js +153 -123
- package/framework/modules/chui_animations/animations.js +105 -43
- package/framework/modules/chui_animations/motion.js +120 -0
- package/framework/modules/chui_animations/styles.css +37 -4
- package/framework/modules/chui_bridge/preload.js +36 -25
- package/framework/modules/chui_functions.js +69 -63
- package/framework/modules/chui_highlight/chui_highlight.js +25 -22
- package/framework/modules/chui_highlight/styles.css +13 -10
- package/framework/modules/chui_i18n/chui_i18n.js +12 -10
- package/framework/modules/chui_i18n/locales/a11y.js +5 -5
- package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
- package/framework/modules/chui_i18n/locales/calendar.js +3 -3
- package/framework/modules/chui_i18n/locales/common.js +3 -3
- package/framework/modules/chui_i18n/locales/data.js +36 -26
- package/framework/modules/chui_i18n/locales/editors.js +40 -13
- package/framework/modules/chui_i18n/locales/feedback.js +65 -3
- package/framework/modules/chui_i18n/locales/forms.js +8 -8
- package/framework/modules/chui_i18n/locales/graphics.js +8 -8
- package/framework/modules/chui_i18n/locales/media.js +4 -4
- package/framework/modules/chui_i18n/locales/navigation.js +12 -8
- package/framework/modules/chui_i18n/locales/validate.js +3 -3
- package/framework/modules/chui_json/chui_json.js +11 -11
- package/framework/modules/chui_layout/chui_layout.js +12 -10
- package/framework/modules/chui_logger/chui_logger.js +47 -28
- package/framework/modules/chui_page/page.js +39 -33
- package/framework/modules/chui_palette/palette.js +61 -0
- package/framework/modules/chui_qr/chui_qr.js +179 -88
- package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
- package/framework/modules/chui_scrollbar/styles.css +4 -2
- package/framework/modules/chui_services/chui_channels.js +27 -13
- package/framework/modules/chui_services/chui_services.js +47 -39
- package/framework/modules/chui_services/desktop.js +108 -101
- package/framework/modules/chui_sortable/chui_sortable.js +33 -28
- package/framework/modules/chui_validate/chui_validate.js +10 -7
- package/framework/tray_bar/chui_menu_items.js +136 -46
- package/index.d.ts +5046 -0
- package/index.js +199 -1254
- package/package.json +81 -68
- package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const {Icon, Icons} = require("../chui_icons/icons");
|
|
2
|
-
const {makeKeyboardButton} = require("../../modules/chui_a11y/chui_a11y");
|
|
3
|
-
const {I18n} = require("../../modules/chui_i18n/chui_i18n");
|
|
1
|
+
const { Icon, Icons } = require("../chui_icons/icons");
|
|
2
|
+
const { makeKeyboardButton } = require("../../modules/chui_a11y/chui_a11y");
|
|
3
|
+
const { I18n } = require("../../modules/chui_i18n/chui_i18n");
|
|
4
4
|
|
|
5
5
|
/** Идентификаторы панелей дерева для aria-controls */
|
|
6
|
-
let
|
|
6
|
+
let treePanelCounter = 0;
|
|
7
7
|
|
|
8
8
|
/** Классы состояния узлов дерева */
|
|
9
9
|
const SELECTED_CLASS = "tree_view_button_selected";
|
|
@@ -11,7 +11,7 @@ const DRAGGING_CLASS = "tree_view_button_dragging";
|
|
|
11
11
|
const DROP_CLASSES = {
|
|
12
12
|
before: "tree_view_button_drop_before",
|
|
13
13
|
after: "tree_view_button_drop_after",
|
|
14
|
-
inside: "tree_view_button_drop_inside"
|
|
14
|
+
inside: "tree_view_button_drop_inside",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -58,28 +58,28 @@ class TreeView {
|
|
|
58
58
|
static SELECTION = {
|
|
59
59
|
NONE: "none",
|
|
60
60
|
SINGLE: "single",
|
|
61
|
-
MULTIPLE: "multiple"
|
|
61
|
+
MULTIPLE: "multiple",
|
|
62
62
|
};
|
|
63
63
|
static DROP = {
|
|
64
64
|
BEFORE: "before",
|
|
65
65
|
AFTER: "after",
|
|
66
|
-
INSIDE: "inside"
|
|
66
|
+
INSIDE: "inside",
|
|
67
67
|
};
|
|
68
68
|
// Узел → описание из фабрики: коллбэки и getSelected() отдают то же, что вернул TreeView.Button
|
|
69
69
|
static #registry = new WeakMap();
|
|
70
70
|
static #HIDDEN_CLASS = "tree_view_search_hidden";
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
72
|
+
#chuiTreeView = document.createElement(`chui_tree_view`);
|
|
73
|
+
#mainTest = undefined;
|
|
74
|
+
#selectionMode = TreeView.SELECTION.NONE;
|
|
75
75
|
#selected = new Set();
|
|
76
76
|
#anchor = undefined;
|
|
77
77
|
#draggable = false;
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
78
|
+
#moveOnDrop = false;
|
|
79
|
+
#onSelect = undefined;
|
|
80
|
+
#onDrop = undefined;
|
|
81
|
+
#dragSource = undefined;
|
|
82
|
+
#dropTarget = undefined;
|
|
83
83
|
// Поиск: запрос, совпадения и скрытый статус для скринридера
|
|
84
84
|
#query = "";
|
|
85
85
|
#matches = new Set();
|
|
@@ -87,120 +87,130 @@ class TreeView {
|
|
|
87
87
|
// Виртуализация: структурная модель (все узлы) + плоский список видимых строк
|
|
88
88
|
#virtual = undefined;
|
|
89
89
|
// Переменная высота строк: из данных (`rowHeight`-функция) или из DOM (`variableHeight`)
|
|
90
|
-
#
|
|
91
|
-
#
|
|
90
|
+
#variableHeight = false;
|
|
91
|
+
#measureDom = false;
|
|
92
92
|
#heights = new Map();
|
|
93
|
-
#
|
|
93
|
+
#rowHeights = [];
|
|
94
94
|
#offsets = [0];
|
|
95
|
-
#
|
|
95
|
+
#subtreeHeights = new Map();
|
|
96
96
|
#nodes = [];
|
|
97
|
-
#
|
|
97
|
+
#nodeIndex = new Map();
|
|
98
98
|
#roots = [];
|
|
99
99
|
#rows = [];
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
100
|
+
#childRows = new Map();
|
|
101
|
+
#rootRows = [];
|
|
102
|
+
#rowSizes = new Map();
|
|
103
|
+
#rowPositions = new Map();
|
|
104
|
+
#rowsDirty = true;
|
|
105
105
|
// Счётчик изменений структуры: окно пересобирается, когда он разошёлся с отрисованным
|
|
106
106
|
#stamp = 0;
|
|
107
|
-
#
|
|
108
|
-
#
|
|
107
|
+
#winStamp = -1;
|
|
108
|
+
#winRows = -1;
|
|
109
109
|
#win = undefined;
|
|
110
|
-
#
|
|
111
|
-
#
|
|
110
|
+
#measuredRowHeight = undefined;
|
|
111
|
+
#scrollBound = false;
|
|
112
112
|
#raf = null;
|
|
113
113
|
#destroyed = false;
|
|
114
114
|
|
|
115
|
-
constructor(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
constructor(
|
|
116
|
+
options = {
|
|
117
|
+
width: String(),
|
|
118
|
+
components: [],
|
|
119
|
+
selection: String(),
|
|
120
|
+
draggable: Boolean(),
|
|
121
|
+
moveOnDrop: Boolean(),
|
|
122
|
+
label: String(),
|
|
123
|
+
onSelect: undefined,
|
|
124
|
+
onDrop: undefined,
|
|
125
|
+
deferRender: Boolean(),
|
|
126
|
+
virtual: undefined,
|
|
127
|
+
variableHeight: Boolean(),
|
|
128
|
+
estimatedRowHeight: undefined,
|
|
129
|
+
height: String(),
|
|
130
|
+
}
|
|
131
|
+
) {
|
|
132
|
+
require("../../modules/chui_functions").setStyles(__dirname + "/styles.css", "chUiJS_TreeView");
|
|
133
|
+
const treeNode = document.createElement(`chui_tree_view_main_test`);
|
|
134
|
+
treeNode.setAttribute("role", "tree");
|
|
135
|
+
this.#mainTest = treeNode;
|
|
134
136
|
// Виртуализация включается явно: `virtual: true` или `{rowHeight, buffer}`
|
|
135
|
-
this.#virtual =
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
this.#virtual =
|
|
138
|
+
options.virtual === undefined || options.virtual === false
|
|
139
|
+
? undefined
|
|
140
|
+
: options.virtual === true
|
|
141
|
+
? {}
|
|
142
|
+
: { ...options.virtual };
|
|
138
143
|
// Переменная высота: функция `rowHeight(record, index)` — высоты из данных,
|
|
139
144
|
// `variableHeight: true` — высоты измеряются по отрисованным строкам
|
|
140
145
|
if (this.#virtual !== undefined) {
|
|
141
146
|
if (options.variableHeight === true) this.#virtual.variableHeight = true;
|
|
142
|
-
this.#
|
|
143
|
-
this.#
|
|
144
|
-
if (this.#
|
|
147
|
+
this.#variableHeight = typeof this.#virtual.rowHeight === "function";
|
|
148
|
+
this.#measureDom = this.#virtual.variableHeight === true;
|
|
149
|
+
if (this.#measureDom && this.#virtual.estimatedRowHeight === undefined)
|
|
150
|
+
this.#virtual.estimatedRowHeight = 32;
|
|
145
151
|
}
|
|
146
|
-
this.#
|
|
152
|
+
this.#selectionMode = options.selection === undefined ? TreeView.SELECTION.NONE : options.selection;
|
|
147
153
|
this.#draggable = options.draggable === true;
|
|
148
|
-
this.#
|
|
149
|
-
this.#
|
|
150
|
-
this.#
|
|
151
|
-
if (options.label !== undefined)
|
|
154
|
+
this.#moveOnDrop = options.moveOnDrop === true;
|
|
155
|
+
this.#onSelect = options.onSelect;
|
|
156
|
+
this.#onDrop = options.onDrop;
|
|
157
|
+
if (options.label !== undefined) treeNode.setAttribute("aria-label", options.label);
|
|
152
158
|
// Отложенная отрисовка строк: узлы в DOM, но далёкие от окна не раскладываются
|
|
153
|
-
if (options.deferRender === true)
|
|
154
|
-
if (this.#
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
if (options.deferRender === true) treeNode.classList.add("tree_view_defer");
|
|
160
|
+
if (this.#selectionMode !== TreeView.SELECTION.NONE) {
|
|
161
|
+
treeNode.setAttribute(
|
|
162
|
+
"aria-multiselectable",
|
|
163
|
+
this.#selectionMode === TreeView.SELECTION.MULTIPLE ? "true" : "false"
|
|
164
|
+
);
|
|
165
|
+
treeNode.addEventListener("click", this.#onClick);
|
|
157
166
|
// Space приходит и от makeKeyboardButton (кликом), поэтому перехватываем его
|
|
158
167
|
// с модификатором раньше — в фазе перехвата
|
|
159
|
-
|
|
168
|
+
treeNode.addEventListener("keydown", this.#onSelectionKeyDown, true);
|
|
160
169
|
}
|
|
161
170
|
// Стрелки по дереву: ↑/↓ по видимым пунктам, → раскрыть, ← свернуть, Home/End — края
|
|
162
|
-
|
|
171
|
+
treeNode.addEventListener("keydown", this.#onNavigationKeyDown);
|
|
163
172
|
if (this.#draggable) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
treeNode.addEventListener("dragstart", this.#onDragStart);
|
|
174
|
+
treeNode.addEventListener("dragover", this.#onDragOver);
|
|
175
|
+
treeNode.addEventListener("dragleave", this.#onDragLeave);
|
|
176
|
+
treeNode.addEventListener("drop", this.#onDropEvent);
|
|
177
|
+
treeNode.addEventListener("dragend", this.#onDragEnd);
|
|
169
178
|
}
|
|
170
179
|
if (options.width !== undefined) {
|
|
171
|
-
this.#
|
|
180
|
+
this.#chuiTreeView.style.width = options.width;
|
|
172
181
|
} else {
|
|
173
|
-
this.#
|
|
182
|
+
this.#chuiTreeView.style.width = "max-content";
|
|
174
183
|
}
|
|
175
184
|
// Панели действий приходят опцией: частичный объект опций без них — дерево без панелей
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
const toolbarButtons = Array.isArray(options.components) ? options.components : [];
|
|
186
|
+
toolbarButtons.forEach((buttons) => {
|
|
187
|
+
treeNode.appendChild(buttons.button);
|
|
179
188
|
if (buttons.panel !== undefined) {
|
|
180
|
-
|
|
189
|
+
treeNode.appendChild(buttons.panel);
|
|
181
190
|
}
|
|
182
|
-
})
|
|
191
|
+
});
|
|
183
192
|
// Структурная модель строится один раз по полному DOM: дальше окно собирается из неё,
|
|
184
193
|
// поэтому отрисовка перестаёт зависеть от того, что сейчас лежит в документе
|
|
185
194
|
this.#buildStructure();
|
|
186
195
|
if (this.#virtual !== undefined) {
|
|
187
|
-
this.#
|
|
196
|
+
this.#chuiTreeView.style.height = options.height === undefined ? "400px" : options.height;
|
|
188
197
|
// Высота строки одна для вёрстки и для арифметики распорок (в переменном режиме её нет)
|
|
189
|
-
if (!this.#isVariable())
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
if (!this.#isVariable())
|
|
199
|
+
this.#chuiTreeView.style.setProperty("--tree_view_row_height", `${this.#rowHeight()}px`);
|
|
200
|
+
this.#chuiTreeView.dataset.virtual = "true";
|
|
201
|
+
treeNode.classList.add("tree_view_virtual");
|
|
192
202
|
this.#bindScroll();
|
|
193
203
|
// Клик по раскрытию меняет не только классы, но и состав видимых строк
|
|
194
|
-
|
|
204
|
+
treeNode.addEventListener("click", this.#onStructureClick);
|
|
195
205
|
}
|
|
196
206
|
this.#applySelection(false);
|
|
197
207
|
this.#syncDraggable();
|
|
198
|
-
this.#
|
|
208
|
+
this.#chuiTreeView.appendChild(treeNode);
|
|
199
209
|
if (this.#virtual !== undefined) this.#renderVirtual();
|
|
200
210
|
}
|
|
201
211
|
|
|
202
212
|
set() {
|
|
203
|
-
return this.#
|
|
213
|
+
return this.#chuiTreeView;
|
|
204
214
|
}
|
|
205
215
|
/**
|
|
206
216
|
* Снять подписки (прокрутка, клавиатура, клики, drag) и отпустить модель строк: плоский список
|
|
@@ -212,28 +222,28 @@ class TreeView {
|
|
|
212
222
|
this.#destroyed = true;
|
|
213
223
|
if (this.#raf !== null) cancelAnimationFrame(this.#raf);
|
|
214
224
|
this.#raf = null;
|
|
215
|
-
this.#
|
|
216
|
-
this.#
|
|
217
|
-
this.#
|
|
218
|
-
this.#
|
|
219
|
-
this.#
|
|
220
|
-
this.#
|
|
221
|
-
this.#
|
|
222
|
-
this.#
|
|
223
|
-
this.#
|
|
224
|
-
this.#
|
|
225
|
-
this.#
|
|
225
|
+
this.#chuiTreeView.removeEventListener("scroll", this.#onScroll);
|
|
226
|
+
this.#scrollBound = false;
|
|
227
|
+
this.#mainTest.removeEventListener("click", this.#onClick);
|
|
228
|
+
this.#mainTest.removeEventListener("keydown", this.#onSelectionKeyDown, true);
|
|
229
|
+
this.#mainTest.removeEventListener("keydown", this.#onNavigationKeyDown);
|
|
230
|
+
this.#mainTest.removeEventListener("click", this.#onStructureClick);
|
|
231
|
+
this.#mainTest.removeEventListener("dragstart", this.#onDragStart);
|
|
232
|
+
this.#mainTest.removeEventListener("dragover", this.#onDragOver);
|
|
233
|
+
this.#mainTest.removeEventListener("dragleave", this.#onDragLeave);
|
|
234
|
+
this.#mainTest.removeEventListener("drop", this.#onDropEvent);
|
|
235
|
+
this.#mainTest.removeEventListener("dragend", this.#onDragEnd);
|
|
226
236
|
this.#nodes = [];
|
|
227
237
|
this.#rows = [];
|
|
228
238
|
this.#roots = [];
|
|
229
|
-
this.#
|
|
230
|
-
this.#
|
|
239
|
+
this.#rootRows = [];
|
|
240
|
+
this.#rowHeights = [];
|
|
231
241
|
this.#offsets = [0];
|
|
232
|
-
this.#
|
|
233
|
-
this.#
|
|
234
|
-
this.#
|
|
235
|
-
this.#
|
|
236
|
-
this.#
|
|
242
|
+
this.#nodeIndex.clear();
|
|
243
|
+
this.#childRows.clear();
|
|
244
|
+
this.#rowSizes.clear();
|
|
245
|
+
this.#rowPositions.clear();
|
|
246
|
+
this.#subtreeHeights.clear();
|
|
237
247
|
this.#heights.clear();
|
|
238
248
|
this.#selected.clear();
|
|
239
249
|
return this;
|
|
@@ -241,7 +251,7 @@ class TreeView {
|
|
|
241
251
|
|
|
242
252
|
// === ВЫБОР ===
|
|
243
253
|
getSelectionMode() {
|
|
244
|
-
return this.#
|
|
254
|
+
return this.#selectionMode;
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
/** Описания выбранных узлов — те же объекты, что вернули фабрики */
|
|
@@ -285,7 +295,9 @@ class TreeView {
|
|
|
285
295
|
// строки могут быть вне документа, но остаются видимыми по смыслу
|
|
286
296
|
#items() {
|
|
287
297
|
if (this.#virtual === undefined) {
|
|
288
|
-
return [...this.#
|
|
298
|
+
return [...this.#mainTest.querySelectorAll("tree_view_button")].filter((button) =>
|
|
299
|
+
this.#isVisibleRow(button)
|
|
300
|
+
);
|
|
289
301
|
}
|
|
290
302
|
return this.#project().map((row) => this.#nodes[row.index].button);
|
|
291
303
|
}
|
|
@@ -308,7 +320,7 @@ class TreeView {
|
|
|
308
320
|
|
|
309
321
|
/** Все кнопки, к которым применим выбор: в виртуальном режиме — по модели, а не по документу */
|
|
310
322
|
#selectionTargets() {
|
|
311
|
-
if (this.#virtual === undefined) return [...this.#
|
|
323
|
+
if (this.#virtual === undefined) return [...this.#mainTest.querySelectorAll("tree_view_button")];
|
|
312
324
|
return this.#project().map((row) => this.#nodes[row.index].button);
|
|
313
325
|
}
|
|
314
326
|
|
|
@@ -320,7 +332,7 @@ class TreeView {
|
|
|
320
332
|
const node = entry instanceof Element ? entry : entry.button;
|
|
321
333
|
if (node === undefined || !(node instanceof Element)) continue;
|
|
322
334
|
// В виртуальном режиме строка может быть вне документа — она всё равно есть в модели
|
|
323
|
-
if (this.#
|
|
335
|
+
if (this.#mainTest.contains(node) || this.#nodeIndex.has(node)) nodes.push(node);
|
|
324
336
|
}
|
|
325
337
|
return nodes;
|
|
326
338
|
}
|
|
@@ -337,22 +349,25 @@ class TreeView {
|
|
|
337
349
|
for (const node of this.#selectionTargets()) {
|
|
338
350
|
const selected = this.#selected.has(node);
|
|
339
351
|
node.classList.toggle(SELECTED_CLASS, selected);
|
|
340
|
-
if (this.#
|
|
352
|
+
if (this.#selectionMode !== TreeView.SELECTION.NONE)
|
|
353
|
+
node.setAttribute("aria-selected", selected ? "true" : "false");
|
|
341
354
|
}
|
|
342
|
-
if (!announce || this.#
|
|
355
|
+
if (!announce || this.#onSelect === undefined) return;
|
|
343
356
|
const items = this.getSelected();
|
|
344
|
-
this.#
|
|
357
|
+
this.#onSelect(items, items.length === 0 ? undefined : items[items.length - 1]);
|
|
345
358
|
}
|
|
346
359
|
|
|
347
360
|
#toggleNode(node) {
|
|
348
|
-
const nodes = this.#selected.has(node)
|
|
361
|
+
const nodes = this.#selected.has(node)
|
|
362
|
+
? [...this.#selected].filter((entry) => entry !== node)
|
|
363
|
+
: [...this.#selected, node];
|
|
349
364
|
this.#anchor = node;
|
|
350
365
|
this.#commitSelection(nodes);
|
|
351
366
|
}
|
|
352
367
|
|
|
353
368
|
#selectRange(node) {
|
|
354
369
|
const items = this.#items();
|
|
355
|
-
const anchor = this.#anchor === undefined || !this.#
|
|
370
|
+
const anchor = this.#anchor === undefined || !this.#mainTest.contains(this.#anchor) ? node : this.#anchor;
|
|
356
371
|
const from = items.indexOf(anchor);
|
|
357
372
|
const to = items.indexOf(node);
|
|
358
373
|
if (from === -1 || to === -1) return;
|
|
@@ -386,9 +401,9 @@ class TreeView {
|
|
|
386
401
|
#inOwnSubtree(source, target) {
|
|
387
402
|
if (source === target || source.contains(target)) return true;
|
|
388
403
|
// Модель знает родство и для строк вне окна: DOM-проверка видит только отрисованное
|
|
389
|
-
const sourceIndex = this.#
|
|
404
|
+
const sourceIndex = this.#nodeIndex.get(source);
|
|
390
405
|
if (sourceIndex !== undefined) {
|
|
391
|
-
let current = this.#
|
|
406
|
+
let current = this.#nodeIndex.get(target);
|
|
392
407
|
while (current !== undefined && current !== -1) {
|
|
393
408
|
if (current === sourceIndex) return true;
|
|
394
409
|
current = this.#nodes[current].parent;
|
|
@@ -410,13 +425,13 @@ class TreeView {
|
|
|
410
425
|
}
|
|
411
426
|
|
|
412
427
|
#applyDropTarget(target, position) {
|
|
413
|
-
if (this.#
|
|
414
|
-
this.#
|
|
415
|
-
this.#
|
|
428
|
+
if (this.#dropTarget !== undefined) {
|
|
429
|
+
this.#dropTarget.node.classList.remove(DROP_CLASSES[this.#dropTarget.position]);
|
|
430
|
+
this.#dropTarget = undefined;
|
|
416
431
|
}
|
|
417
432
|
if (target !== undefined && position !== undefined) {
|
|
418
433
|
target.classList.add(DROP_CLASSES[position]);
|
|
419
|
-
this.#
|
|
434
|
+
this.#dropTarget = { node: target, position: position };
|
|
420
435
|
}
|
|
421
436
|
}
|
|
422
437
|
|
|
@@ -428,7 +443,10 @@ class TreeView {
|
|
|
428
443
|
let reference;
|
|
429
444
|
if (position === TreeView.DROP.INSIDE) {
|
|
430
445
|
const panel = target.nextElementSibling;
|
|
431
|
-
const html =
|
|
446
|
+
const html =
|
|
447
|
+
panel !== null && panel.tagName.toLowerCase() === "tree_view_panel"
|
|
448
|
+
? panel.querySelector("tree_view_html")
|
|
449
|
+
: null;
|
|
432
450
|
if (html === null) return false;
|
|
433
451
|
container = html;
|
|
434
452
|
reference = null;
|
|
@@ -444,14 +462,14 @@ class TreeView {
|
|
|
444
462
|
}
|
|
445
463
|
|
|
446
464
|
#finishDrag() {
|
|
447
|
-
if (this.#
|
|
448
|
-
this.#
|
|
465
|
+
if (this.#dragSource !== undefined) this.#dragSource.classList.remove(DRAGGING_CLASS);
|
|
466
|
+
this.#dragSource = undefined;
|
|
449
467
|
}
|
|
450
468
|
|
|
451
469
|
#descriptor(node) {
|
|
452
470
|
const item = TreeView.#registry.get(node);
|
|
453
471
|
if (item !== undefined) return item;
|
|
454
|
-
return {title: this.#title(node), button: node};
|
|
472
|
+
return { title: this.#title(node), button: node };
|
|
455
473
|
}
|
|
456
474
|
|
|
457
475
|
#title(node) {
|
|
@@ -466,9 +484,9 @@ class TreeView {
|
|
|
466
484
|
// Строится один раз по полному DOM, ленивые поддеревья и переносы достраивают её точечно.
|
|
467
485
|
#buildStructure() {
|
|
468
486
|
this.#nodes = [];
|
|
469
|
-
this.#
|
|
487
|
+
this.#nodeIndex = new Map();
|
|
470
488
|
this.#roots = [];
|
|
471
|
-
for (const child of [...this.#
|
|
489
|
+
for (const child of [...this.#mainTest.children]) {
|
|
472
490
|
if (child.tagName.toLowerCase() !== "tree_view_button") continue;
|
|
473
491
|
this.#register(child, -1);
|
|
474
492
|
}
|
|
@@ -477,7 +495,7 @@ class TreeView {
|
|
|
477
495
|
|
|
478
496
|
/** Структура изменилась: плоский список и окно надо пересобрать */
|
|
479
497
|
#touch() {
|
|
480
|
-
this.#
|
|
498
|
+
this.#rowsDirty = true;
|
|
481
499
|
this.#stamp += 1;
|
|
482
500
|
}
|
|
483
501
|
|
|
@@ -495,10 +513,10 @@ class TreeView {
|
|
|
495
513
|
parent: parent,
|
|
496
514
|
children: [],
|
|
497
515
|
title: this.#title(button),
|
|
498
|
-
hidden: false
|
|
516
|
+
hidden: false,
|
|
499
517
|
};
|
|
500
518
|
this.#nodes.push(record);
|
|
501
|
-
this.#
|
|
519
|
+
this.#nodeIndex.set(button, index);
|
|
502
520
|
if (parent === -1) this.#roots.push(index);
|
|
503
521
|
else this.#nodes[parent].children.push(index);
|
|
504
522
|
if (html !== null) {
|
|
@@ -517,7 +535,7 @@ class TreeView {
|
|
|
517
535
|
* можно собирать из него, не оглядываясь на документ.
|
|
518
536
|
*/
|
|
519
537
|
#project() {
|
|
520
|
-
if (!this.#
|
|
538
|
+
if (!this.#rowsDirty) return this.#rows;
|
|
521
539
|
const rows = [];
|
|
522
540
|
const positions = new Map();
|
|
523
541
|
const childrenOf = new Map();
|
|
@@ -529,7 +547,7 @@ class TreeView {
|
|
|
529
547
|
const record = this.#nodes[index];
|
|
530
548
|
if (record === undefined || record.hidden) return 0;
|
|
531
549
|
positions.set(index, rows.length);
|
|
532
|
-
rows.push({index: index, depth: depth});
|
|
550
|
+
rows.push({ index: index, depth: depth });
|
|
533
551
|
const own = variable ? this.#heightOf(index) : this.#rowHeight();
|
|
534
552
|
heights.push(own);
|
|
535
553
|
let size = 1;
|
|
@@ -553,17 +571,17 @@ class TreeView {
|
|
|
553
571
|
const roots = [];
|
|
554
572
|
for (const root of this.#roots) if (walk(root, 0) > 0) roots.push(root);
|
|
555
573
|
this.#rows = rows;
|
|
556
|
-
this.#
|
|
557
|
-
this.#
|
|
558
|
-
this.#
|
|
559
|
-
this.#
|
|
560
|
-
this.#
|
|
561
|
-
this.#
|
|
574
|
+
this.#childRows = childrenOf;
|
|
575
|
+
this.#rootRows = roots;
|
|
576
|
+
this.#rowSizes = sizes;
|
|
577
|
+
this.#subtreeHeights = subtree;
|
|
578
|
+
this.#rowHeights = heights;
|
|
579
|
+
this.#rowPositions = positions;
|
|
562
580
|
// Верхняя граница каждой позиции: для переменной высоты окно и прокрутка считаются по ней
|
|
563
581
|
const offsets = [0];
|
|
564
582
|
for (const value of heights) offsets.push(offsets[offsets.length - 1] + value);
|
|
565
583
|
this.#offsets = offsets;
|
|
566
|
-
this.#
|
|
584
|
+
this.#rowsDirty = false;
|
|
567
585
|
return rows;
|
|
568
586
|
}
|
|
569
587
|
|
|
@@ -575,19 +593,19 @@ class TreeView {
|
|
|
575
593
|
|
|
576
594
|
/** Переменная высота строк: высоты из данных (функция) или измеренные по DOM */
|
|
577
595
|
#isVariable() {
|
|
578
|
-
return this.#
|
|
596
|
+
return this.#variableHeight || this.#measureDom;
|
|
579
597
|
}
|
|
580
598
|
|
|
581
599
|
/** Высота строки с индексом модели: из данных, из кеша замеров или оценка */
|
|
582
600
|
#heightOf(index) {
|
|
583
|
-
if (this.#
|
|
601
|
+
if (this.#variableHeight) {
|
|
584
602
|
if (!this.#heights.has(index)) {
|
|
585
603
|
const value = Number(this.#virtual.rowHeight(this.#nodes[index], index));
|
|
586
604
|
this.#heights.set(index, Number.isFinite(value) && value > 0 ? value : 26);
|
|
587
605
|
}
|
|
588
606
|
return this.#heights.get(index);
|
|
589
607
|
}
|
|
590
|
-
if (this.#
|
|
608
|
+
if (this.#measureDom) {
|
|
591
609
|
if (this.#heights.has(index)) return this.#heights.get(index);
|
|
592
610
|
return this.#virtual.estimatedRowHeight;
|
|
593
611
|
}
|
|
@@ -601,13 +619,13 @@ class TreeView {
|
|
|
601
619
|
* тела `DataGrid`).
|
|
602
620
|
*/
|
|
603
621
|
#measureWindow() {
|
|
604
|
-
for (const button of this.#
|
|
605
|
-
const index = this.#
|
|
622
|
+
for (const button of this.#mainTest.querySelectorAll("tree_view_button")) {
|
|
623
|
+
const index = this.#nodeIndex.get(button);
|
|
606
624
|
if (index === undefined) continue;
|
|
607
625
|
const measured = Math.round(button.getBoundingClientRect().height);
|
|
608
626
|
if (measured > 0 && this.#heights.get(index) !== measured) {
|
|
609
627
|
this.#heights.set(index, measured);
|
|
610
|
-
this.#
|
|
628
|
+
this.#rowsDirty = true;
|
|
611
629
|
}
|
|
612
630
|
}
|
|
613
631
|
}
|
|
@@ -616,12 +634,12 @@ class TreeView {
|
|
|
616
634
|
#rowHeight() {
|
|
617
635
|
if (this.#virtual === undefined) return 26;
|
|
618
636
|
if (this.#virtual.rowHeight !== undefined) return this.#virtual.rowHeight;
|
|
619
|
-
return this.#
|
|
637
|
+
return this.#measuredRowHeight === undefined ? 26 : this.#measuredRowHeight;
|
|
620
638
|
}
|
|
621
639
|
|
|
622
640
|
#measureRow() {
|
|
623
641
|
if (this.#virtual === undefined || this.#isVariable() || this.#virtual.rowHeight !== undefined) return 0;
|
|
624
|
-
const sample = this.#
|
|
642
|
+
const sample = this.#mainTest.querySelector("tree_view_button");
|
|
625
643
|
return sample === null ? 0 : Math.round(sample.getBoundingClientRect().height);
|
|
626
644
|
}
|
|
627
645
|
|
|
@@ -630,9 +648,9 @@ class TreeView {
|
|
|
630
648
|
}
|
|
631
649
|
|
|
632
650
|
#bindScroll() {
|
|
633
|
-
if (this.#
|
|
634
|
-
this.#
|
|
635
|
-
this.#
|
|
651
|
+
if (this.#scrollBound) return;
|
|
652
|
+
this.#scrollBound = true;
|
|
653
|
+
this.#chuiTreeView.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
636
654
|
}
|
|
637
655
|
|
|
638
656
|
#onScroll = () => {
|
|
@@ -661,15 +679,15 @@ class TreeView {
|
|
|
661
679
|
if (remeasure) {
|
|
662
680
|
// Переменный режим по DOM: сначала измеряем уже отрисованные строки — их настоящие
|
|
663
681
|
// высоты уточняют смещения, и только потом считается окно
|
|
664
|
-
if (this.#
|
|
682
|
+
if (this.#measureDom) this.#measureWindow();
|
|
665
683
|
// Настоящая высота строки может отличаться от умолчания: распорки и окно считаются
|
|
666
684
|
// по ней, поэтому замер идёт до расчёта окна
|
|
667
685
|
const measured = this.#measureRow();
|
|
668
|
-
if (measured > 0 && measured !== this.#
|
|
669
|
-
this.#
|
|
670
|
-
this.#
|
|
686
|
+
if (measured > 0 && measured !== this.#measuredRowHeight) {
|
|
687
|
+
this.#measuredRowHeight = measured;
|
|
688
|
+
this.#chuiTreeView.style.setProperty("--tree_view_row_height", `${measured}px`);
|
|
671
689
|
// Смещения в проекции считались по прежней высоте — пересобрать
|
|
672
|
-
this.#
|
|
690
|
+
this.#rowsDirty = true;
|
|
673
691
|
force = true;
|
|
674
692
|
}
|
|
675
693
|
}
|
|
@@ -677,7 +695,7 @@ class TreeView {
|
|
|
677
695
|
const variable = this.#isVariable();
|
|
678
696
|
const height = this.#rowHeight();
|
|
679
697
|
const buffer = this.#buffer();
|
|
680
|
-
const viewport = Math.max(1, this.#
|
|
698
|
+
const viewport = Math.max(1, this.#chuiTreeView.clientHeight);
|
|
681
699
|
// Позиция строки по верхней границе прокрутки (двоичный поиск по смещениям)
|
|
682
700
|
const positionFor = (top) => {
|
|
683
701
|
let low = 0;
|
|
@@ -695,13 +713,14 @@ class TreeView {
|
|
|
695
713
|
// нет паддинга, рамки и зазоров между отрисованными строками, поэтому «модельный» низ чуть
|
|
696
714
|
// выше настоящего — только по нему последняя строка (End, стрелка вниз) осталась бы обрезанной
|
|
697
715
|
const modelBottom = Math.max(0, total - viewport);
|
|
698
|
-
const realBottom = Math.max(0, this.#
|
|
699
|
-
let scrollTop = Math.min(Math.max(0, this.#
|
|
716
|
+
const realBottom = Math.max(0, this.#chuiTreeView.scrollHeight - viewport);
|
|
717
|
+
let scrollTop = Math.min(Math.max(0, this.#chuiTreeView.scrollTop), Math.max(modelBottom, realBottom));
|
|
700
718
|
// Низ окна для переменной высоты — позиция строки под нижней кромкой плюс одна строка,
|
|
701
719
|
// иначе последняя видимая строка выпадала бы из окна
|
|
702
|
-
const windowTo = (top) =>
|
|
703
|
-
|
|
704
|
-
|
|
720
|
+
const windowTo = (top) =>
|
|
721
|
+
variable
|
|
722
|
+
? Math.min(rows.length, positionFor(top + viewport) + 1 + buffer)
|
|
723
|
+
: Math.min(rows.length, Math.ceil((top + viewport) / height) + buffer);
|
|
705
724
|
let from = Math.max(0, positionFor(scrollTop) - buffer);
|
|
706
725
|
let to = windowTo(scrollTop);
|
|
707
726
|
// Список сжался (поиск, сворачивание) — позиция ещё старая и указывает за конец: прижимаем
|
|
@@ -711,14 +730,21 @@ class TreeView {
|
|
|
711
730
|
from = Math.max(0, positionFor(scrollTop) - buffer);
|
|
712
731
|
to = windowTo(scrollTop);
|
|
713
732
|
}
|
|
714
|
-
if (scrollTop !== this.#
|
|
733
|
+
if (scrollTop !== this.#chuiTreeView.scrollTop) this.#chuiTreeView.scrollTop = scrollTop;
|
|
715
734
|
// Ни окно, ни структура не изменились (прокрутка в пределах строки) — пересобирать
|
|
716
735
|
// нечего: пересборка на каждом кадре только дёргала бы DOM и фокус
|
|
717
|
-
if (
|
|
718
|
-
|
|
736
|
+
if (
|
|
737
|
+
!force &&
|
|
738
|
+
this.#win !== undefined &&
|
|
739
|
+
from === this.#win.from &&
|
|
740
|
+
to === this.#win.to &&
|
|
741
|
+
this.#winRows === rows.length &&
|
|
742
|
+
this.#winStamp === this.#stamp
|
|
743
|
+
)
|
|
744
|
+
return;
|
|
719
745
|
// Кто держит фокус: снимок до мутации — после неё `document.activeElement` уже `body`
|
|
720
746
|
const active = document.activeElement;
|
|
721
|
-
const activeIndex = active instanceof Element ? this.#
|
|
747
|
+
const activeIndex = active instanceof Element ? this.#nodeIndex.get(active) : undefined;
|
|
722
748
|
const keepIndex = activeIndex === undefined ? undefined : this.#pinRow(rows, activeIndex);
|
|
723
749
|
const visible = new Set();
|
|
724
750
|
for (let position = from; position < to; position++) {
|
|
@@ -737,7 +763,7 @@ class TreeView {
|
|
|
737
763
|
current = this.#nodes[current].parent;
|
|
738
764
|
}
|
|
739
765
|
}
|
|
740
|
-
const sizeOf = (index) => this.#
|
|
766
|
+
const sizeOf = (index) => this.#rowSizes.get(index) || 1;
|
|
741
767
|
const spacer = (px) => {
|
|
742
768
|
const node = document.createElement("tree_view_spacer");
|
|
743
769
|
node.setAttribute("aria-hidden", "true");
|
|
@@ -745,10 +771,10 @@ class TreeView {
|
|
|
745
771
|
return node;
|
|
746
772
|
};
|
|
747
773
|
// Сколько занимает пропущенная ветка: в переменном режиме — суммарная высота её поддерева
|
|
748
|
-
const subtreePx = (index) => variable ?
|
|
774
|
+
const subtreePx = (index) => (variable ? this.#subtreeHeights.get(index) || 0 : sizeOf(index) * height);
|
|
749
775
|
const fill = (container, parentIndex) => {
|
|
750
776
|
// Дети берутся из проекции: скрытое поиском и свёрнутое в неё уже не попадает
|
|
751
|
-
const children = parentIndex === -1 ? this.#
|
|
777
|
+
const children = parentIndex === -1 ? this.#rootRows : this.#childRows.get(parentIndex) || [];
|
|
752
778
|
const parts = [];
|
|
753
779
|
let gap = 0;
|
|
754
780
|
for (const index of children) {
|
|
@@ -763,7 +789,7 @@ class TreeView {
|
|
|
763
789
|
}
|
|
764
790
|
const record = this.#nodes[index];
|
|
765
791
|
// Высота из данных задаётся строке инлайном — иначе DOM разошёлся бы с моделью
|
|
766
|
-
if (this.#
|
|
792
|
+
if (this.#variableHeight) {
|
|
767
793
|
const rowHeightPx = this.#heightOf(index);
|
|
768
794
|
record.button.style.minHeight = `${rowHeightPx}px`;
|
|
769
795
|
record.button.style.height = `${rowHeightPx}px`;
|
|
@@ -783,14 +809,14 @@ class TreeView {
|
|
|
783
809
|
else record.html.replaceChildren();
|
|
784
810
|
}
|
|
785
811
|
};
|
|
786
|
-
fill(this.#
|
|
787
|
-
if (this.#status !== undefined) this.#
|
|
812
|
+
fill(this.#mainTest, -1);
|
|
813
|
+
if (this.#status !== undefined) this.#mainTest.appendChild(this.#status);
|
|
788
814
|
this.#restoreFocus(keepIndex);
|
|
789
815
|
this.#applySelection(false);
|
|
790
816
|
this.#syncDraggable();
|
|
791
|
-
this.#win = {from: from, to: to};
|
|
792
|
-
this.#
|
|
793
|
-
this.#
|
|
817
|
+
this.#win = { from: from, to: to };
|
|
818
|
+
this.#winStamp = this.#stamp;
|
|
819
|
+
this.#winRows = rows.length;
|
|
794
820
|
}
|
|
795
821
|
|
|
796
822
|
/**
|
|
@@ -799,7 +825,7 @@ class TreeView {
|
|
|
799
825
|
* видимая: фокус остаётся в дереве, а не уходит в `body`.
|
|
800
826
|
*/
|
|
801
827
|
#pinRow(rows, index) {
|
|
802
|
-
const position = this.#
|
|
828
|
+
const position = this.#rowPositions.get(index);
|
|
803
829
|
if (position !== undefined) return rows[position].index;
|
|
804
830
|
let nearest = undefined;
|
|
805
831
|
for (const row of rows) {
|
|
@@ -818,14 +844,18 @@ class TreeView {
|
|
|
818
844
|
if (index === undefined) return;
|
|
819
845
|
const record = this.#nodes[index];
|
|
820
846
|
if (record === undefined || document.activeElement === record.button) return;
|
|
821
|
-
record.button.focus({preventScroll: true});
|
|
847
|
+
record.button.focus({ preventScroll: true });
|
|
822
848
|
}
|
|
823
849
|
|
|
824
850
|
/** Что сейчас в окне: {from, to} по плоскому списку (для стендов и приложений) */
|
|
825
|
-
getVirtualWindow() {
|
|
851
|
+
getVirtualWindow() {
|
|
852
|
+
return this.#win;
|
|
853
|
+
}
|
|
826
854
|
|
|
827
855
|
/** Переменная высота строк включена (`rowHeight`-функция или `variableHeight`) */
|
|
828
|
-
isVariableHeight() {
|
|
856
|
+
isVariableHeight() {
|
|
857
|
+
return this.#isVariable();
|
|
858
|
+
}
|
|
829
859
|
|
|
830
860
|
/** Высота строки с индексом модели: из данных, из замеров или заданная */
|
|
831
861
|
getRowHeightAt(index = Number()) {
|
|
@@ -851,17 +881,17 @@ class TreeView {
|
|
|
851
881
|
return;
|
|
852
882
|
}
|
|
853
883
|
// Позиция строки в плоском списке: карта проекции, чтобы не перебирать все строки
|
|
854
|
-
const modelIndex = this.#
|
|
855
|
-
const place = modelIndex === undefined ? undefined : this.#
|
|
884
|
+
const modelIndex = this.#nodeIndex.get(button);
|
|
885
|
+
const place = modelIndex === undefined ? undefined : this.#rowPositions.get(modelIndex);
|
|
856
886
|
if (place === undefined) return;
|
|
857
887
|
// В переменном режиме верх и высота строки берутся из смещений проекции
|
|
858
888
|
const variable = this.#isVariable();
|
|
859
|
-
const height = variable ?
|
|
860
|
-
const viewport = Math.max(1, this.#
|
|
889
|
+
const height = variable ? this.#rowHeights[place] || this.#rowHeight() : this.#rowHeight();
|
|
890
|
+
const viewport = Math.max(1, this.#chuiTreeView.clientHeight);
|
|
861
891
|
const top = variable ? this.#offsets[place] : place * height;
|
|
862
|
-
const scrollTop = this.#
|
|
892
|
+
const scrollTop = this.#chuiTreeView.scrollTop;
|
|
863
893
|
if (top < scrollTop || top + height > scrollTop + viewport) {
|
|
864
|
-
this.#
|
|
894
|
+
this.#chuiTreeView.scrollTop = Math.max(0, top - Math.floor(viewport / 2));
|
|
865
895
|
this.#renderVirtual();
|
|
866
896
|
}
|
|
867
897
|
button.focus();
|
|
@@ -871,7 +901,7 @@ class TreeView {
|
|
|
871
901
|
#onStructureClick = (event) => {
|
|
872
902
|
const button = event.target instanceof Element ? event.target.closest("tree_view_button[aria-expanded]") : null;
|
|
873
903
|
if (button === null) return;
|
|
874
|
-
const index = this.#
|
|
904
|
+
const index = this.#nodeIndex.get(button);
|
|
875
905
|
if (index !== undefined) this.#syncSubtree(index);
|
|
876
906
|
const panel = button.nextElementSibling;
|
|
877
907
|
if (panel !== null && panel.tagName.toLowerCase() === "tree_view_panel") {
|
|
@@ -887,7 +917,7 @@ class TreeView {
|
|
|
887
917
|
if (record === undefined || record.html === null) return;
|
|
888
918
|
for (const child of [...record.html.children]) {
|
|
889
919
|
if (child.tagName.toLowerCase() !== "tree_view_button") continue;
|
|
890
|
-
if (this.#
|
|
920
|
+
if (this.#nodeIndex.has(child)) continue;
|
|
891
921
|
this.#register(child, index);
|
|
892
922
|
}
|
|
893
923
|
}
|
|
@@ -907,8 +937,8 @@ class TreeView {
|
|
|
907
937
|
}
|
|
908
938
|
|
|
909
939
|
#syncMove(source, target, position) {
|
|
910
|
-
const index = this.#
|
|
911
|
-
const targetIndex = this.#
|
|
940
|
+
const index = this.#nodeIndex.get(source);
|
|
941
|
+
const targetIndex = this.#nodeIndex.get(target);
|
|
912
942
|
if (index === undefined || targetIndex === undefined) return;
|
|
913
943
|
if (position === TreeView.DROP.INSIDE) this.#relink(index, targetIndex);
|
|
914
944
|
else {
|
|
@@ -921,17 +951,23 @@ class TreeView {
|
|
|
921
951
|
}
|
|
922
952
|
|
|
923
953
|
// === ПОИСК ===
|
|
924
|
-
getSearchQuery() {
|
|
925
|
-
|
|
954
|
+
getSearchQuery() {
|
|
955
|
+
return this.#query;
|
|
956
|
+
}
|
|
957
|
+
isSearching() {
|
|
958
|
+
return this.#query !== "";
|
|
959
|
+
}
|
|
926
960
|
/** Совпадения поиска (описания из фабрик), в порядке показа */
|
|
927
|
-
getMatches() {
|
|
961
|
+
getMatches() {
|
|
962
|
+
return [...this.#matches].map((node) => this.#descriptor(node));
|
|
963
|
+
}
|
|
928
964
|
|
|
929
965
|
/**
|
|
930
966
|
* Показать только подходящие узлы и цепочку их предков; остальное убирается (`display: none`),
|
|
931
967
|
* поэтому навигация и выбор дальше работают по видимому. `autoExpand` раскрывает предков
|
|
932
968
|
* (без него совпадения внутри свёрнутых веток остаются скрытыми), `highlight` подсвечивает совпадения.
|
|
933
969
|
*/
|
|
934
|
-
search(query = String(), options = {autoExpand: true, highlight: true}) {
|
|
970
|
+
search(query = String(), options = { autoExpand: true, highlight: true }) {
|
|
935
971
|
this.clearSearch();
|
|
936
972
|
const text = String(query).trim().toLocaleLowerCase();
|
|
937
973
|
if (text === "") return this;
|
|
@@ -939,10 +975,10 @@ class TreeView {
|
|
|
939
975
|
// В виртуальном режиме в документе только окно — ищем и скрываем по модели
|
|
940
976
|
if (this.#virtual !== undefined) {
|
|
941
977
|
this.#searchModel(text, options);
|
|
942
|
-
this.#announce(I18n.t("treeView.found", {count: this.#matches.size}));
|
|
978
|
+
this.#announce(I18n.t("treeView.found", { count: this.#matches.size }));
|
|
943
979
|
return this;
|
|
944
980
|
}
|
|
945
|
-
const buttons = [...this.#
|
|
981
|
+
const buttons = [...this.#mainTest.querySelectorAll("tree_view_button")];
|
|
946
982
|
const visible = new Set();
|
|
947
983
|
for (const button of buttons) {
|
|
948
984
|
if (!this.#title(button).toLocaleLowerCase().includes(text)) continue;
|
|
@@ -964,7 +1000,7 @@ class TreeView {
|
|
|
964
1000
|
}
|
|
965
1001
|
}
|
|
966
1002
|
for (const button of buttons) if (!visible.has(button)) button.classList.add(TreeView.#HIDDEN_CLASS);
|
|
967
|
-
for (const panel of this.#
|
|
1003
|
+
for (const panel of this.#mainTest.querySelectorAll("tree_view_panel")) {
|
|
968
1004
|
if (!visible.has(panel)) panel.classList.add(TreeView.#HIDDEN_CLASS);
|
|
969
1005
|
// Раскрытая панель должна быть видна целиком: max-height после раскрытия пересчитываем
|
|
970
1006
|
else if (panel.style.maxHeight !== "") panel.style.maxHeight = "max-content";
|
|
@@ -972,7 +1008,7 @@ class TreeView {
|
|
|
972
1008
|
if (options.highlight !== false) for (const node of this.#matches) node.classList.add("tree_view_button_match");
|
|
973
1009
|
this.#touch();
|
|
974
1010
|
if (this.#virtual !== undefined) this.#renderVirtual();
|
|
975
|
-
this.#announce(I18n.t("treeView.found", {count: this.#matches.size}));
|
|
1011
|
+
this.#announce(I18n.t("treeView.found", { count: this.#matches.size }));
|
|
976
1012
|
return this;
|
|
977
1013
|
}
|
|
978
1014
|
|
|
@@ -1000,7 +1036,9 @@ class TreeView {
|
|
|
1000
1036
|
return this;
|
|
1001
1037
|
}
|
|
1002
1038
|
|
|
1003
|
-
collapse(target) {
|
|
1039
|
+
collapse(target) {
|
|
1040
|
+
return this.expand(target, false);
|
|
1041
|
+
}
|
|
1004
1042
|
|
|
1005
1043
|
/** Раскрыть всё: в виртуальном режиме — единственный способ, строки вне окна не кликнуть */
|
|
1006
1044
|
expandAll() {
|
|
@@ -1014,7 +1052,8 @@ class TreeView {
|
|
|
1014
1052
|
}
|
|
1015
1053
|
|
|
1016
1054
|
getExpanded() {
|
|
1017
|
-
return this.#nodes
|
|
1055
|
+
return this.#nodes
|
|
1056
|
+
.filter((record) => record.button.getAttribute("aria-expanded") === "true")
|
|
1018
1057
|
.map((record) => this.#descriptor(record.button));
|
|
1019
1058
|
}
|
|
1020
1059
|
|
|
@@ -1022,7 +1061,7 @@ class TreeView {
|
|
|
1022
1061
|
if (target === undefined || target === null) return undefined;
|
|
1023
1062
|
const node = target instanceof Element ? target : target.button;
|
|
1024
1063
|
if (!(node instanceof Element)) return undefined;
|
|
1025
|
-
const index = this.#
|
|
1064
|
+
const index = this.#nodeIndex.get(node);
|
|
1026
1065
|
return index === undefined ? undefined : this.#nodes[index];
|
|
1027
1066
|
}
|
|
1028
1067
|
|
|
@@ -1045,14 +1084,16 @@ class TreeView {
|
|
|
1045
1084
|
}
|
|
1046
1085
|
}
|
|
1047
1086
|
for (const record of this.#nodes) record.hidden = !visible.has(record.index);
|
|
1048
|
-
if (options.highlight !== false)
|
|
1087
|
+
if (options.highlight !== false)
|
|
1088
|
+
for (const button of this.#matches) button.classList.add("tree_view_button_match");
|
|
1049
1089
|
this.#touch();
|
|
1050
1090
|
this.#renderVirtual();
|
|
1051
1091
|
}
|
|
1052
1092
|
|
|
1053
1093
|
/** Снять фильтр поиска: возвращает все узлы и снимает подсветку */
|
|
1054
1094
|
clearSearch() {
|
|
1055
|
-
for (const node of this.#
|
|
1095
|
+
for (const node of this.#mainTest.querySelectorAll("." + TreeView.#HIDDEN_CLASS))
|
|
1096
|
+
node.classList.remove(TreeView.#HIDDEN_CLASS);
|
|
1056
1097
|
for (const node of this.#matches) node.classList.remove("tree_view_button_match");
|
|
1057
1098
|
for (const record of this.#nodes) record.hidden = false;
|
|
1058
1099
|
const wasSearching = this.#query !== "";
|
|
@@ -1067,10 +1108,12 @@ class TreeView {
|
|
|
1067
1108
|
// Статус поиска читает скринридер (узла нет, пока поиск не использовался)
|
|
1068
1109
|
#announce(text = String()) {
|
|
1069
1110
|
if (this.#status === undefined) {
|
|
1070
|
-
this.#status = Object.assign(document.createElement("chui_visually_hidden"), {
|
|
1111
|
+
this.#status = Object.assign(document.createElement("chui_visually_hidden"), {
|
|
1112
|
+
className: "chui_visually_hidden",
|
|
1113
|
+
});
|
|
1071
1114
|
this.#status.setAttribute("role", "status");
|
|
1072
1115
|
this.#status.setAttribute("aria-live", "polite");
|
|
1073
|
-
this.#
|
|
1116
|
+
this.#mainTest.appendChild(this.#status);
|
|
1074
1117
|
}
|
|
1075
1118
|
this.#status.innerText = text;
|
|
1076
1119
|
}
|
|
@@ -1078,8 +1121,8 @@ class TreeView {
|
|
|
1078
1121
|
// === ОБРАБОТЧИКИ ===
|
|
1079
1122
|
#onClick = (event) => {
|
|
1080
1123
|
const node = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1081
|
-
if (node === null || !this.#
|
|
1082
|
-
const multiple = this.#
|
|
1124
|
+
if (node === null || !this.#mainTest.contains(node)) return;
|
|
1125
|
+
const multiple = this.#selectionMode === TreeView.SELECTION.MULTIPLE;
|
|
1083
1126
|
if (multiple && (event.ctrlKey || event.metaKey)) this.#toggleNode(node);
|
|
1084
1127
|
else if (multiple && event.shiftKey) this.#selectRange(node);
|
|
1085
1128
|
else {
|
|
@@ -1097,16 +1140,16 @@ class TreeView {
|
|
|
1097
1140
|
if (!event.ctrlKey && !event.metaKey && !event.shiftKey) return;
|
|
1098
1141
|
const target = event.target instanceof Element ? event.target : document.activeElement;
|
|
1099
1142
|
const node = target instanceof Element ? target.closest("tree_view_button") : null;
|
|
1100
|
-
if (node === null || !this.#
|
|
1143
|
+
if (node === null || !this.#mainTest.contains(node)) return;
|
|
1101
1144
|
event.preventDefault();
|
|
1102
1145
|
event.stopPropagation();
|
|
1103
|
-
if (event.shiftKey && this.#
|
|
1146
|
+
if (event.shiftKey && this.#selectionMode === TreeView.SELECTION.MULTIPLE) this.#selectRange(node);
|
|
1104
1147
|
else this.#toggleNode(node);
|
|
1105
1148
|
};
|
|
1106
1149
|
|
|
1107
1150
|
#onNavigationKeyDown = (event) => {
|
|
1108
1151
|
const items = this.#items();
|
|
1109
|
-
const current = items.findIndex(item => item === document.activeElement);
|
|
1152
|
+
const current = items.findIndex((item) => item === document.activeElement);
|
|
1110
1153
|
if (current === -1) return;
|
|
1111
1154
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1112
1155
|
if (event.altKey && this.#draggable) {
|
|
@@ -1126,7 +1169,7 @@ class TreeView {
|
|
|
1126
1169
|
document.activeElement.click();
|
|
1127
1170
|
}
|
|
1128
1171
|
} else if (event.key === "ArrowLeft") {
|
|
1129
|
-
const parent = document.activeElement.parentElement.closest(
|
|
1172
|
+
const parent = document.activeElement.parentElement.closest("tree_view_button[aria-expanded]");
|
|
1130
1173
|
if (document.activeElement.getAttribute("aria-expanded") === "true") {
|
|
1131
1174
|
event.preventDefault();
|
|
1132
1175
|
document.activeElement.click();
|
|
@@ -1140,7 +1183,7 @@ class TreeView {
|
|
|
1140
1183
|
// Клавиатурная альтернатива перетаскиванию: Alt+↑/↓ переставляет узел среди соседей.
|
|
1141
1184
|
// Соседей берём из модели: в виртуальном режиме в DOM только окно
|
|
1142
1185
|
#moveByKeyboard(node, step) {
|
|
1143
|
-
const index = this.#
|
|
1186
|
+
const index = this.#nodeIndex.get(node);
|
|
1144
1187
|
if (index === undefined) return;
|
|
1145
1188
|
const parent = this.#nodes[index].parent;
|
|
1146
1189
|
const siblings = parent === -1 ? this.#roots : this.#nodes[parent].children;
|
|
@@ -1149,14 +1192,14 @@ class TreeView {
|
|
|
1149
1192
|
const target = this.#nodes[targetIndex].button;
|
|
1150
1193
|
const position = step < 0 ? TreeView.DROP.BEFORE : TreeView.DROP.AFTER;
|
|
1151
1194
|
if (!this.#moveNodes(node, target, position)) return;
|
|
1152
|
-
if (this.#
|
|
1195
|
+
if (this.#onDrop !== undefined) this.#onDrop(this.#descriptor(node), this.#descriptor(target), position);
|
|
1153
1196
|
node.focus();
|
|
1154
1197
|
}
|
|
1155
1198
|
|
|
1156
1199
|
#onDragStart = (event) => {
|
|
1157
1200
|
const node = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1158
1201
|
if (node === null) return;
|
|
1159
|
-
this.#
|
|
1202
|
+
this.#dragSource = node;
|
|
1160
1203
|
node.classList.add(DRAGGING_CLASS);
|
|
1161
1204
|
if (event.dataTransfer === null) return;
|
|
1162
1205
|
event.dataTransfer.effectAllowed = "move";
|
|
@@ -1164,10 +1207,10 @@ class TreeView {
|
|
|
1164
1207
|
};
|
|
1165
1208
|
|
|
1166
1209
|
#onDragOver = (event) => {
|
|
1167
|
-
if (this.#
|
|
1210
|
+
if (this.#dragSource === undefined) return;
|
|
1168
1211
|
const target = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1169
1212
|
// В себя и в собственное поддерево бросать нельзя: узел потерял бы ветку
|
|
1170
|
-
if (target === null || this.#inOwnSubtree(this.#
|
|
1213
|
+
if (target === null || this.#inOwnSubtree(this.#dragSource, target)) {
|
|
1171
1214
|
this.#applyDropTarget(undefined, undefined);
|
|
1172
1215
|
if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "none";
|
|
1173
1216
|
return;
|
|
@@ -1178,20 +1221,20 @@ class TreeView {
|
|
|
1178
1221
|
};
|
|
1179
1222
|
|
|
1180
1223
|
#onDragLeave = (event) => {
|
|
1181
|
-
if (event.relatedTarget instanceof Element && this.#
|
|
1224
|
+
if (event.relatedTarget instanceof Element && this.#mainTest.contains(event.relatedTarget)) return;
|
|
1182
1225
|
this.#applyDropTarget(undefined, undefined);
|
|
1183
1226
|
};
|
|
1184
1227
|
|
|
1185
1228
|
#onDropEvent = (event) => {
|
|
1186
|
-
if (this.#
|
|
1229
|
+
if (this.#dragSource === undefined || this.#dropTarget === undefined) return;
|
|
1187
1230
|
event.preventDefault();
|
|
1188
|
-
const source = this.#
|
|
1189
|
-
const target = this.#
|
|
1190
|
-
const position = this.#
|
|
1231
|
+
const source = this.#dragSource;
|
|
1232
|
+
const target = this.#dropTarget.node;
|
|
1233
|
+
const position = this.#dropTarget.position;
|
|
1191
1234
|
this.#applyDropTarget(undefined, undefined);
|
|
1192
1235
|
this.#finishDrag();
|
|
1193
|
-
if (this.#
|
|
1194
|
-
if (this.#
|
|
1236
|
+
if (this.#moveOnDrop) this.#moveNodes(source, target, position);
|
|
1237
|
+
if (this.#onDrop !== undefined) this.#onDrop(this.#descriptor(source), this.#descriptor(target), position);
|
|
1195
1238
|
};
|
|
1196
1239
|
|
|
1197
1240
|
#onDragEnd = () => {
|
|
@@ -1200,88 +1243,90 @@ class TreeView {
|
|
|
1200
1243
|
};
|
|
1201
1244
|
|
|
1202
1245
|
static Button(options = { title: String(), listener: () => {} }) {
|
|
1203
|
-
|
|
1246
|
+
const button = document.createElement("tree_view_button");
|
|
1204
1247
|
if (options.listener !== undefined) button.addEventListener("click", options.listener);
|
|
1205
1248
|
makeKeyboardButton(button, "treeitem");
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1249
|
+
const buttonText = document.createElement("tree_view_button_text");
|
|
1250
|
+
// Заголовок — текст, а не разметка: в `title` приходят имена файлов и папок, а тот же
|
|
1251
|
+
// узел читается как `textContent` (`#title`), и поиск сравнивал бы текст с тегами
|
|
1252
|
+
buttonText.textContent = options.title;
|
|
1253
|
+
button.appendChild(buttonText);
|
|
1254
|
+
const item = { title: options.title, button: button };
|
|
1210
1255
|
TreeView.#registry.set(button, item);
|
|
1211
1256
|
return item;
|
|
1212
1257
|
}
|
|
1213
1258
|
|
|
1214
1259
|
static ExpandButton(options = { title: String(), subButtons: [], components: [], lazySubButtons: undefined }) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1260
|
+
const button = document.createElement("tree_view_button");
|
|
1261
|
+
const panel = document.createElement("tree_view_panel");
|
|
1217
1262
|
// Раскрытая панель переходит с фиксированной высоты на max-content: иначе высота
|
|
1218
1263
|
// остаётся «замороженной» после первого раскрытия
|
|
1219
1264
|
const transitionEndEvent = (event) => {
|
|
1220
1265
|
const target = event.currentTarget;
|
|
1221
1266
|
if (target.style.maxHeight && target.style.maxHeight !== "0px") target.style.maxHeight = "max-content";
|
|
1222
1267
|
};
|
|
1223
|
-
panel.id = "chui_tree_panel_" + (
|
|
1268
|
+
panel.id = "chui_tree_panel_" + (treePanelCounter += 1);
|
|
1224
1269
|
button.setAttribute("aria-controls", panel.id);
|
|
1225
1270
|
button.setAttribute("aria-expanded", "false");
|
|
1226
1271
|
makeKeyboardButton(button, "treeitem");
|
|
1227
|
-
button.addEventListener(
|
|
1272
|
+
button.addEventListener("click", () => {
|
|
1228
1273
|
buildSubs();
|
|
1229
|
-
const open = !button.classList.contains(
|
|
1230
|
-
button.classList.toggle(
|
|
1274
|
+
const open = !button.classList.contains("tree_view_button_active");
|
|
1275
|
+
button.classList.toggle("tree_view_button_active", open);
|
|
1231
1276
|
// Стрелка поворачивается по aria-expanded (styles.css) — состояние одно
|
|
1232
1277
|
button.setAttribute("aria-expanded", open ? "true" : "false");
|
|
1233
1278
|
if (open) {
|
|
1234
1279
|
panel.style.maxHeight = panel.scrollHeight + "px";
|
|
1235
|
-
panel.addEventListener(
|
|
1280
|
+
panel.addEventListener("transitionend", transitionEndEvent);
|
|
1236
1281
|
} else {
|
|
1237
1282
|
// Сначала фиксируем текущую высоту, иначе сжатие не анимируется
|
|
1238
1283
|
panel.style.maxHeight = panel.scrollHeight + "px";
|
|
1239
1284
|
void panel.offsetHeight;
|
|
1240
1285
|
panel.style.maxHeight = "0px";
|
|
1241
1286
|
}
|
|
1242
|
-
})
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
button.appendChild(
|
|
1246
|
-
|
|
1287
|
+
});
|
|
1288
|
+
const buttonText = document.createElement("tree_view_button_text");
|
|
1289
|
+
buttonText.textContent = options.title;
|
|
1290
|
+
button.appendChild(buttonText);
|
|
1291
|
+
const barrow = document.createElement("tree_view_button_arrow");
|
|
1247
1292
|
barrow.innerHTML = new Icon(Icons.HARDWARE.KEYBOARD_ARROW_DOWN).getHTML();
|
|
1248
|
-
button.appendChild(barrow)
|
|
1249
|
-
|
|
1293
|
+
button.appendChild(barrow);
|
|
1294
|
+
const html = document.createElement("tree_view_html");
|
|
1250
1295
|
// Ленивое поддерево: содержимое строится один раз — при первом раскрытии,
|
|
1251
1296
|
// поэтому большое дерево не занимает DOM заранее (lazySubButtons: () => [...])
|
|
1252
|
-
let
|
|
1297
|
+
let subButtonsBuilt = false;
|
|
1253
1298
|
const buildSubs = () => {
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1299
|
+
if (subButtonsBuilt) return;
|
|
1300
|
+
subButtonsBuilt = true;
|
|
1256
1301
|
if (options.subButtons !== undefined) {
|
|
1257
|
-
options.subButtons.forEach(sub => {
|
|
1258
|
-
html.appendChild(sub.button)
|
|
1302
|
+
options.subButtons.forEach((sub) => {
|
|
1303
|
+
html.appendChild(sub.button);
|
|
1259
1304
|
if (sub.panel !== undefined) {
|
|
1260
|
-
html.appendChild(sub.panel)
|
|
1305
|
+
html.appendChild(sub.panel);
|
|
1261
1306
|
}
|
|
1262
|
-
})
|
|
1307
|
+
});
|
|
1263
1308
|
}
|
|
1264
1309
|
if (options.components !== undefined) {
|
|
1265
|
-
options.components.forEach(sub => {
|
|
1266
|
-
html.appendChild(sub.set())
|
|
1267
|
-
})
|
|
1310
|
+
options.components.forEach((sub) => {
|
|
1311
|
+
html.appendChild(sub.set());
|
|
1312
|
+
});
|
|
1268
1313
|
}
|
|
1269
1314
|
if (options.lazySubButtons !== undefined) {
|
|
1270
|
-
options.lazySubButtons().forEach(sub => {
|
|
1271
|
-
html.appendChild(sub.button)
|
|
1315
|
+
options.lazySubButtons().forEach((sub) => {
|
|
1316
|
+
html.appendChild(sub.button);
|
|
1272
1317
|
if (sub.panel !== undefined) {
|
|
1273
|
-
html.appendChild(sub.panel)
|
|
1318
|
+
html.appendChild(sub.panel);
|
|
1274
1319
|
}
|
|
1275
|
-
})
|
|
1320
|
+
});
|
|
1276
1321
|
}
|
|
1277
1322
|
};
|
|
1278
1323
|
// Без ленивой фабрики поведение прежнее — строим сразу
|
|
1279
1324
|
if (options.lazySubButtons === undefined) buildSubs();
|
|
1280
|
-
panel.appendChild(html)
|
|
1281
|
-
const item = {title: options.title, button: button, panel: panel};
|
|
1325
|
+
panel.appendChild(html);
|
|
1326
|
+
const item = { title: options.title, button: button, panel: panel };
|
|
1282
1327
|
TreeView.#registry.set(button, item);
|
|
1283
1328
|
return item;
|
|
1284
1329
|
}
|
|
1285
1330
|
}
|
|
1286
1331
|
|
|
1287
|
-
exports.TreeView = TreeView
|
|
1332
|
+
exports.TreeView = TreeView;
|