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.
- package/CHANGELOG.md +536 -0
- package/framework/appLayout/default/chui_app_layout.js +361 -303
- package/framework/appLayout/default/global_style.css +243 -89
- package/framework/appLayout/default/main_theme_style.css +188 -101
- package/framework/components/chui_accordion/accordion.js +17 -17
- package/framework/components/chui_accordion/styles.css +7 -7
- package/framework/components/chui_action_sheet/action_sheet.js +35 -32
- package/framework/components/chui_action_sheet/styles.css +10 -4
- 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 +4 -4
- package/framework/components/chui_affix/styles.css +5 -0
- package/framework/components/chui_alert/alert.js +11 -5
- package/framework/components/chui_alert/styles.css +7 -0
- package/framework/components/chui_avatar/avatar.js +13 -13
- package/framework/components/chui_avatar/styles.css +1 -1
- package/framework/components/chui_avatar_group/avatar_group.js +4 -4
- package/framework/components/chui_avatar_group/styles.css +1 -1
- package/framework/components/chui_backtop/backtop.js +25 -2
- package/framework/components/chui_backtop/styles.css +21 -0
- package/framework/components/chui_badge/badge.js +15 -15
- 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/styles.css +1 -1
- package/framework/components/chui_button/button.js +24 -28
- package/framework/components/chui_button/styles.css +5 -5
- package/framework/components/chui_calendar/calendar.js +6 -6
- package/framework/components/chui_card/card.js +14 -14
- 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 +73 -51
- package/framework/components/chui_chat/styles.css +18 -7
- package/framework/components/chui_clipboard_history/clipboard_history.js +25 -20
- package/framework/components/chui_clipboard_history/styles.css +10 -8
- package/framework/components/chui_code/code.js +8 -8
- package/framework/components/chui_code/styles.css +2 -2
- package/framework/components/chui_code_editor/code_editor.js +26 -26
- package/framework/components/chui_code_editor/styles.css +3 -3
- package/framework/components/chui_command_palette/command_palette.js +29 -29
- package/framework/components/chui_command_palette/styles.css +5 -5
- package/framework/components/chui_console/console.js +9 -9
- package/framework/components/chui_console/styles.css +5 -2
- package/framework/components/chui_content_block/content_block.js +2 -2
- package/framework/components/chui_context_menu/context_menu.js +162 -54
- package/framework/components/chui_context_menu/styles.css +82 -8
- package/framework/components/chui_custom_element/custom_element.js +11 -11
- package/framework/components/chui_data_grid/data_grid.js +268 -268
- package/framework/components/chui_data_grid/styles.css +14 -14
- package/framework/components/chui_descriptions/styles.css +1 -1
- package/framework/components/chui_details/details.js +12 -12
- package/framework/components/chui_details/styles.css +7 -7
- package/framework/components/chui_diff_viewer/diff_viewer.js +88 -88
- package/framework/components/chui_diff_viewer/styles.css +9 -9
- package/framework/components/chui_divider/divider.js +6 -6
- package/framework/components/chui_divider/styles.css +2 -2
- package/framework/components/chui_drawer/drawer.js +37 -14
- package/framework/components/chui_drawer/styles.css +18 -4
- package/framework/components/chui_drop_zone/drop_zone.js +6 -6
- package/framework/components/chui_drop_zone/styles.css +10 -6
- package/framework/components/chui_fieldset/fieldset.js +6 -6
- package/framework/components/chui_fieldset/styles.css +5 -5
- package/framework/components/chui_file_explorer/file_explorer.js +115 -115
- package/framework/components/chui_file_explorer/styles.css +6 -6
- package/framework/components/chui_form/form.js +11 -11
- package/framework/components/chui_gantt/gantt.js +41 -41
- package/framework/components/chui_gantt/styles.css +5 -5
- package/framework/components/chui_graphs/graphs.js +82 -46
- package/framework/components/chui_graphs/styles.css +26 -5
- package/framework/components/chui_hero/hero.js +63 -20
- package/framework/components/chui_hero/styles.css +176 -7
- package/framework/components/chui_html_block/html_block.js +5 -5
- package/framework/components/chui_html_block/styles.css +1 -1
- package/framework/components/chui_hx/styles.css +2 -2
- package/framework/components/chui_icons/icons.js +7 -7
- package/framework/components/chui_icons/styles.css +2 -1
- package/framework/components/chui_inputs/chui_check_box/styles.css +12 -3
- package/framework/components/chui_inputs/chui_chips/chips.js +92 -31
- package/framework/components/chui_inputs/chui_chips/styles.css +28 -5
- package/framework/components/chui_inputs/chui_color_picker/color_picker.js +20 -15
- package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
- package/framework/components/chui_inputs/chui_combo_box/combo_box.js +58 -58
- package/framework/components/chui_inputs/chui_combo_box/styles.css +9 -9
- package/framework/components/chui_inputs/chui_date/date.js +93 -95
- package/framework/components/chui_inputs/chui_date/styles.css +8 -8
- package/framework/components/chui_inputs/chui_date_range/date_range.js +22 -22
- package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
- package/framework/components/chui_inputs/chui_datetime/datetime.js +12 -12
- package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
- package/framework/components/chui_inputs/chui_email/email.js +10 -10
- package/framework/components/chui_inputs/chui_email/styles.css +6 -6
- package/framework/components/chui_inputs/chui_file/file.js +8 -8
- package/framework/components/chui_inputs/chui_file/styles.css +12 -9
- package/framework/components/chui_inputs/chui_hotkey/hotkey.js +39 -39
- package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
- package/framework/components/chui_inputs/chui_masked/masked.js +44 -44
- package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
- package/framework/components/chui_inputs/chui_mention/mention.js +13 -13
- package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
- package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +147 -137
- package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +14 -8
- package/framework/components/chui_inputs/chui_number/number.js +42 -45
- package/framework/components/chui_inputs/chui_number/styles.css +6 -6
- package/framework/components/chui_inputs/chui_otp/otp.js +32 -32
- package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
- package/framework/components/chui_inputs/chui_password/password.js +30 -36
- package/framework/components/chui_inputs/chui_password/styles.css +7 -7
- package/framework/components/chui_inputs/chui_radio_button/styles.css +2 -2
- package/framework/components/chui_inputs/chui_rating/rating.js +28 -28
- package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
- package/framework/components/chui_inputs/chui_select_box/select_box.js +54 -54
- package/framework/components/chui_inputs/chui_select_box/styles.css +10 -10
- package/framework/components/chui_inputs/chui_slider/slider.js +14 -14
- package/framework/components/chui_inputs/chui_slider/styles.css +5 -4
- package/framework/components/chui_inputs/chui_text/styles.css +7 -7
- package/framework/components/chui_inputs/chui_text/text.js +13 -17
- package/framework/components/chui_inputs/chui_text_area/styles.css +8 -8
- package/framework/components/chui_inputs/chui_text_area/text_area.js +23 -23
- package/framework/components/chui_inputs/chui_time/styles.css +13 -17
- package/framework/components/chui_inputs/chui_time/time.js +35 -24
- package/framework/components/chui_inputs/chui_toggle/styles.css +10 -3
- package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
- package/framework/components/chui_json_editor/json_editor.js +87 -87
- package/framework/components/chui_json_editor/styles.css +5 -5
- package/framework/components/chui_json_viewer/json_viewer.js +80 -80
- package/framework/components/chui_json_viewer/styles.css +6 -6
- package/framework/components/chui_kanban/kanban.js +27 -27
- package/framework/components/chui_kanban/styles.css +8 -4
- package/framework/components/chui_kbd/styles.css +1 -1
- package/framework/components/chui_label/label.js +17 -17
- package/framework/components/chui_label/styles.css +1 -1
- package/framework/components/chui_link/link.js +9 -9
- package/framework/components/chui_link/styles.css +11 -4
- package/framework/components/chui_list/list.js +11 -11
- package/framework/components/chui_list/styles.css +2 -2
- package/framework/components/chui_markdown_editor/markdown_editor.js +66 -66
- package/framework/components/chui_markdown_editor/styles.css +5 -5
- package/framework/components/chui_media/audio.js +240 -246
- package/framework/components/chui_media/audio_styles.css +41 -23
- package/framework/components/chui_media/image.js +49 -46
- package/framework/components/chui_media/image_compare.js +35 -35
- package/framework/components/chui_media/image_compare_styles.css +5 -5
- package/framework/components/chui_media/image_styles.css +1 -1
- package/framework/components/chui_media/video.js +148 -148
- package/framework/components/chui_media/video_styles.css +27 -11
- package/framework/components/chui_menu_bar/menu_bar.js +54 -54
- package/framework/components/chui_menu_bar/styles.css +10 -10
- 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 +37 -13
- package/framework/components/chui_notification/notification.js +76 -54
- package/framework/components/chui_notification/notification_download.js +26 -81
- package/framework/components/chui_notification/notification_download_card.js +115 -0
- package/framework/components/chui_notification/notification_download_progress.css +2 -2
- package/framework/components/chui_notification/notification_download_progress.js +70 -108
- package/framework/components/chui_notification/notification_update.js +31 -31
- package/framework/components/chui_notification/status_plate.css +3 -3
- package/framework/components/chui_notification/styles.css +6 -1
- package/framework/components/chui_notification/styles_download.css +1 -1
- package/framework/components/chui_notification/styles_update.css +2 -2
- package/framework/components/chui_org_chart/org_chart.js +29 -29
- package/framework/components/chui_org_chart/styles.css +2 -2
- package/framework/components/chui_pagination/pagination.js +3 -3
- package/framework/components/chui_pagination/styles.css +1 -1
- package/framework/components/chui_paragraph/styles.css +2 -2
- package/framework/components/chui_popover/popover.js +29 -23
- package/framework/components/chui_popover/styles.css +1 -1
- package/framework/components/chui_popups/popups.js +124 -124
- package/framework/components/chui_popups/styles.css +5 -5
- package/framework/components/chui_print_button/print_button.js +15 -10
- package/framework/components/chui_progress_bar/progress_bar.js +34 -12
- package/framework/components/chui_progress_bar/styles.css +4 -4
- package/framework/components/chui_progress_ring/progress_ring.js +15 -15
- package/framework/components/chui_progress_ring/styles.css +1 -1
- package/framework/components/chui_qrcode/qrcode.js +12 -12
- package/framework/components/chui_query_builder/query_builder.js +46 -46
- package/framework/components/chui_query_builder/styles.css +4 -4
- package/framework/components/chui_radio_group/styles.css +1 -1
- package/framework/components/chui_result/result.js +15 -15
- package/framework/components/chui_result/styles.css +3 -3
- package/framework/components/chui_scheduler/scheduler.js +74 -74
- package/framework/components/chui_scheduler/styles.css +5 -5
- 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 +63 -6
- package/framework/components/chui_segmented_control/styles.css +32 -6
- package/framework/components/chui_settings/settings.js +45 -46
- package/framework/components/chui_settings/styles.css +4 -4
- package/framework/components/chui_shortcut_help/shortcut_help.js +32 -34
- package/framework/components/chui_sidebar_nav/sidebar_nav.js +98 -29
- package/framework/components/chui_sidebar_nav/styles.css +9 -3
- package/framework/components/chui_slideshow/slideshow.js +85 -85
- package/framework/components/chui_slideshow/styles.css +6 -6
- package/framework/components/chui_spinner/spinner.js +10 -10
- package/framework/components/chui_split_button/split_button.js +30 -12
- package/framework/components/chui_split_button/styles.css +21 -5
- package/framework/components/chui_splitter/splitter.js +11 -11
- package/framework/components/chui_splitter/styles.css +2 -2
- package/framework/components/chui_statistic/statistic.js +47 -47
- package/framework/components/chui_status_bar/styles.css +2 -2
- package/framework/components/chui_stepper/stepper.js +202 -31
- package/framework/components/chui_stepper/styles.css +79 -14
- package/framework/components/chui_sticky_bar/sticky_bar.js +44 -13
- package/framework/components/chui_sticky_bar/styles.css +10 -8
- package/framework/components/chui_tabs/chui_tabs.js +74 -26
- package/framework/components/chui_tabs/styles.css +40 -12
- package/framework/components/chui_text_editor/styles.css +7 -7
- package/framework/components/chui_text_editor/text_editor.js +56 -56
- package/framework/components/chui_text_editor/text_editor_panel.js +173 -174
- package/framework/components/chui_theme_toggle/theme_toggle.js +21 -5
- package/framework/components/chui_timeline/styles.css +3 -1
- package/framework/components/chui_toast_queue/styles.css +11 -11
- package/framework/components/chui_toast_queue/toast_queue.js +30 -27
- package/framework/components/chui_toolbar/styles.css +2 -2
- package/framework/components/chui_tooltip/styles.css +2 -2
- package/framework/components/chui_tooltip/tooltip.js +3 -3
- package/framework/components/chui_tour/styles.css +8 -8
- package/framework/components/chui_tour/tour.js +72 -72
- package/framework/components/chui_transfer/styles.css +6 -6
- package/framework/components/chui_transfer/transfer.js +93 -106
- package/framework/components/chui_tree_table/styles.css +4 -4
- package/framework/components/chui_tree_table/tree_table.js +45 -45
- package/framework/components/chui_tree_view/styles.css +10 -8
- package/framework/components/chui_tree_view/tree_view.js +185 -183
- 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 +17 -17
- package/framework/components/chui_webview/webview.js +9 -9
- 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 +78 -79
- package/framework/modules/chui_animations/animations.js +77 -15
- package/framework/modules/chui_animations/motion.js +120 -0
- package/framework/modules/chui_animations/styles.css +36 -3
- package/framework/modules/chui_functions.js +31 -27
- package/framework/modules/chui_highlight/styles.css +13 -10
- package/framework/modules/chui_i18n/chui_i18n.js +3 -3
- package/framework/modules/chui_i18n/locales/feedback.js +62 -0
- package/framework/modules/chui_i18n/locales/navigation.js +4 -0
- package/framework/modules/chui_json/chui_json.js +2 -2
- package/framework/modules/chui_logger/chui_logger.js +23 -8
- package/framework/modules/chui_page/page.js +10 -10
- package/framework/modules/chui_palette/palette.js +61 -0
- package/framework/modules/chui_qr/chui_qr.js +23 -23
- package/framework/modules/chui_scrollbar/chui_scrollbar.js +45 -45
- package/framework/modules/chui_scrollbar/styles.css +3 -3
- package/framework/modules/chui_services/chui_services.js +8 -8
- package/framework/modules/chui_services/desktop.js +77 -71
- package/framework/modules/chui_sortable/chui_sortable.js +17 -17
- package/framework/modules/chui_validate/chui_validate.js +3 -3
- package/framework/tray_bar/chui_menu_items.js +2 -2
- package/index.d.ts +266 -34
- package/index.js +20 -1168
- package/package.json +6 -4
- package/framework/components/telegram_bot/chui_telegram_bot.js +0 -191
|
@@ -3,7 +3,7 @@ const { makeKeyboardButton } = require("../../modules/chui_a11y/chui_a11y");
|
|
|
3
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";
|
|
@@ -69,17 +69,17 @@ class TreeView {
|
|
|
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,28 +87,28 @@ 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
|
|
|
@@ -130,9 +130,9 @@ class TreeView {
|
|
|
130
130
|
}
|
|
131
131
|
) {
|
|
132
132
|
require("../../modules/chui_functions").setStyles(__dirname + "/styles.css", "chUiJS_TreeView");
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.#
|
|
133
|
+
const treeNode = document.createElement(`chui_tree_view_main_test`);
|
|
134
|
+
treeNode.setAttribute("role", "tree");
|
|
135
|
+
this.#mainTest = treeNode;
|
|
136
136
|
// Виртуализация включается явно: `virtual: true` или `{rowHeight, buffer}`
|
|
137
137
|
this.#virtual =
|
|
138
138
|
options.virtual === undefined || options.virtual === false
|
|
@@ -144,73 +144,73 @@ class TreeView {
|
|
|
144
144
|
// `variableHeight: true` — высоты измеряются по отрисованным строкам
|
|
145
145
|
if (this.#virtual !== undefined) {
|
|
146
146
|
if (options.variableHeight === true) this.#virtual.variableHeight = true;
|
|
147
|
-
this.#
|
|
148
|
-
this.#
|
|
149
|
-
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
150
|
this.#virtual.estimatedRowHeight = 32;
|
|
151
151
|
}
|
|
152
|
-
this.#
|
|
152
|
+
this.#selectionMode = options.selection === undefined ? TreeView.SELECTION.NONE : options.selection;
|
|
153
153
|
this.#draggable = options.draggable === true;
|
|
154
|
-
this.#
|
|
155
|
-
this.#
|
|
156
|
-
this.#
|
|
157
|
-
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);
|
|
158
158
|
// Отложенная отрисовка строк: узлы в DOM, но далёкие от окна не раскладываются
|
|
159
|
-
if (options.deferRender === true)
|
|
160
|
-
if (this.#
|
|
161
|
-
|
|
159
|
+
if (options.deferRender === true) treeNode.classList.add("tree_view_defer");
|
|
160
|
+
if (this.#selectionMode !== TreeView.SELECTION.NONE) {
|
|
161
|
+
treeNode.setAttribute(
|
|
162
162
|
"aria-multiselectable",
|
|
163
|
-
this.#
|
|
163
|
+
this.#selectionMode === TreeView.SELECTION.MULTIPLE ? "true" : "false"
|
|
164
164
|
);
|
|
165
|
-
|
|
165
|
+
treeNode.addEventListener("click", this.#onClick);
|
|
166
166
|
// Space приходит и от makeKeyboardButton (кликом), поэтому перехватываем его
|
|
167
167
|
// с модификатором раньше — в фазе перехвата
|
|
168
|
-
|
|
168
|
+
treeNode.addEventListener("keydown", this.#onSelectionKeyDown, true);
|
|
169
169
|
}
|
|
170
170
|
// Стрелки по дереву: ↑/↓ по видимым пунктам, → раскрыть, ← свернуть, Home/End — края
|
|
171
|
-
|
|
171
|
+
treeNode.addEventListener("keydown", this.#onNavigationKeyDown);
|
|
172
172
|
if (this.#draggable) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
178
|
}
|
|
179
179
|
if (options.width !== undefined) {
|
|
180
|
-
this.#
|
|
180
|
+
this.#chuiTreeView.style.width = options.width;
|
|
181
181
|
} else {
|
|
182
|
-
this.#
|
|
182
|
+
this.#chuiTreeView.style.width = "max-content";
|
|
183
183
|
}
|
|
184
184
|
// Панели действий приходят опцией: частичный объект опций без них — дерево без панелей
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
const toolbarButtons = Array.isArray(options.components) ? options.components : [];
|
|
186
|
+
toolbarButtons.forEach((buttons) => {
|
|
187
|
+
treeNode.appendChild(buttons.button);
|
|
188
188
|
if (buttons.panel !== undefined) {
|
|
189
|
-
|
|
189
|
+
treeNode.appendChild(buttons.panel);
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
192
|
// Структурная модель строится один раз по полному DOM: дальше окно собирается из неё,
|
|
193
193
|
// поэтому отрисовка перестаёт зависеть от того, что сейчас лежит в документе
|
|
194
194
|
this.#buildStructure();
|
|
195
195
|
if (this.#virtual !== undefined) {
|
|
196
|
-
this.#
|
|
196
|
+
this.#chuiTreeView.style.height = options.height === undefined ? "400px" : options.height;
|
|
197
197
|
// Высота строки одна для вёрстки и для арифметики распорок (в переменном режиме её нет)
|
|
198
198
|
if (!this.#isVariable())
|
|
199
|
-
this.#
|
|
200
|
-
this.#
|
|
201
|
-
|
|
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");
|
|
202
202
|
this.#bindScroll();
|
|
203
203
|
// Клик по раскрытию меняет не только классы, но и состав видимых строк
|
|
204
|
-
|
|
204
|
+
treeNode.addEventListener("click", this.#onStructureClick);
|
|
205
205
|
}
|
|
206
206
|
this.#applySelection(false);
|
|
207
207
|
this.#syncDraggable();
|
|
208
|
-
this.#
|
|
208
|
+
this.#chuiTreeView.appendChild(treeNode);
|
|
209
209
|
if (this.#virtual !== undefined) this.#renderVirtual();
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
set() {
|
|
213
|
-
return this.#
|
|
213
|
+
return this.#chuiTreeView;
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
216
|
* Снять подписки (прокрутка, клавиатура, клики, drag) и отпустить модель строк: плоский список
|
|
@@ -222,28 +222,28 @@ class TreeView {
|
|
|
222
222
|
this.#destroyed = true;
|
|
223
223
|
if (this.#raf !== null) cancelAnimationFrame(this.#raf);
|
|
224
224
|
this.#raf = null;
|
|
225
|
-
this.#
|
|
226
|
-
this.#
|
|
227
|
-
this.#
|
|
228
|
-
this.#
|
|
229
|
-
this.#
|
|
230
|
-
this.#
|
|
231
|
-
this.#
|
|
232
|
-
this.#
|
|
233
|
-
this.#
|
|
234
|
-
this.#
|
|
235
|
-
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);
|
|
236
236
|
this.#nodes = [];
|
|
237
237
|
this.#rows = [];
|
|
238
238
|
this.#roots = [];
|
|
239
|
-
this.#
|
|
240
|
-
this.#
|
|
239
|
+
this.#rootRows = [];
|
|
240
|
+
this.#rowHeights = [];
|
|
241
241
|
this.#offsets = [0];
|
|
242
|
-
this.#
|
|
243
|
-
this.#
|
|
244
|
-
this.#
|
|
245
|
-
this.#
|
|
246
|
-
this.#
|
|
242
|
+
this.#nodeIndex.clear();
|
|
243
|
+
this.#childRows.clear();
|
|
244
|
+
this.#rowSizes.clear();
|
|
245
|
+
this.#rowPositions.clear();
|
|
246
|
+
this.#subtreeHeights.clear();
|
|
247
247
|
this.#heights.clear();
|
|
248
248
|
this.#selected.clear();
|
|
249
249
|
return this;
|
|
@@ -251,7 +251,7 @@ class TreeView {
|
|
|
251
251
|
|
|
252
252
|
// === ВЫБОР ===
|
|
253
253
|
getSelectionMode() {
|
|
254
|
-
return this.#
|
|
254
|
+
return this.#selectionMode;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/** Описания выбранных узлов — те же объекты, что вернули фабрики */
|
|
@@ -295,7 +295,7 @@ class TreeView {
|
|
|
295
295
|
// строки могут быть вне документа, но остаются видимыми по смыслу
|
|
296
296
|
#items() {
|
|
297
297
|
if (this.#virtual === undefined) {
|
|
298
|
-
return [...this.#
|
|
298
|
+
return [...this.#mainTest.querySelectorAll("tree_view_button")].filter((button) =>
|
|
299
299
|
this.#isVisibleRow(button)
|
|
300
300
|
);
|
|
301
301
|
}
|
|
@@ -320,7 +320,7 @@ class TreeView {
|
|
|
320
320
|
|
|
321
321
|
/** Все кнопки, к которым применим выбор: в виртуальном режиме — по модели, а не по документу */
|
|
322
322
|
#selectionTargets() {
|
|
323
|
-
if (this.#virtual === undefined) return [...this.#
|
|
323
|
+
if (this.#virtual === undefined) return [...this.#mainTest.querySelectorAll("tree_view_button")];
|
|
324
324
|
return this.#project().map((row) => this.#nodes[row.index].button);
|
|
325
325
|
}
|
|
326
326
|
|
|
@@ -332,7 +332,7 @@ class TreeView {
|
|
|
332
332
|
const node = entry instanceof Element ? entry : entry.button;
|
|
333
333
|
if (node === undefined || !(node instanceof Element)) continue;
|
|
334
334
|
// В виртуальном режиме строка может быть вне документа — она всё равно есть в модели
|
|
335
|
-
if (this.#
|
|
335
|
+
if (this.#mainTest.contains(node) || this.#nodeIndex.has(node)) nodes.push(node);
|
|
336
336
|
}
|
|
337
337
|
return nodes;
|
|
338
338
|
}
|
|
@@ -349,12 +349,12 @@ class TreeView {
|
|
|
349
349
|
for (const node of this.#selectionTargets()) {
|
|
350
350
|
const selected = this.#selected.has(node);
|
|
351
351
|
node.classList.toggle(SELECTED_CLASS, selected);
|
|
352
|
-
if (this.#
|
|
352
|
+
if (this.#selectionMode !== TreeView.SELECTION.NONE)
|
|
353
353
|
node.setAttribute("aria-selected", selected ? "true" : "false");
|
|
354
354
|
}
|
|
355
|
-
if (!announce || this.#
|
|
355
|
+
if (!announce || this.#onSelect === undefined) return;
|
|
356
356
|
const items = this.getSelected();
|
|
357
|
-
this.#
|
|
357
|
+
this.#onSelect(items, items.length === 0 ? undefined : items[items.length - 1]);
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
#toggleNode(node) {
|
|
@@ -367,7 +367,7 @@ class TreeView {
|
|
|
367
367
|
|
|
368
368
|
#selectRange(node) {
|
|
369
369
|
const items = this.#items();
|
|
370
|
-
const anchor = this.#anchor === undefined || !this.#
|
|
370
|
+
const anchor = this.#anchor === undefined || !this.#mainTest.contains(this.#anchor) ? node : this.#anchor;
|
|
371
371
|
const from = items.indexOf(anchor);
|
|
372
372
|
const to = items.indexOf(node);
|
|
373
373
|
if (from === -1 || to === -1) return;
|
|
@@ -401,9 +401,9 @@ class TreeView {
|
|
|
401
401
|
#inOwnSubtree(source, target) {
|
|
402
402
|
if (source === target || source.contains(target)) return true;
|
|
403
403
|
// Модель знает родство и для строк вне окна: DOM-проверка видит только отрисованное
|
|
404
|
-
const sourceIndex = this.#
|
|
404
|
+
const sourceIndex = this.#nodeIndex.get(source);
|
|
405
405
|
if (sourceIndex !== undefined) {
|
|
406
|
-
let current = this.#
|
|
406
|
+
let current = this.#nodeIndex.get(target);
|
|
407
407
|
while (current !== undefined && current !== -1) {
|
|
408
408
|
if (current === sourceIndex) return true;
|
|
409
409
|
current = this.#nodes[current].parent;
|
|
@@ -425,13 +425,13 @@ class TreeView {
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
#applyDropTarget(target, position) {
|
|
428
|
-
if (this.#
|
|
429
|
-
this.#
|
|
430
|
-
this.#
|
|
428
|
+
if (this.#dropTarget !== undefined) {
|
|
429
|
+
this.#dropTarget.node.classList.remove(DROP_CLASSES[this.#dropTarget.position]);
|
|
430
|
+
this.#dropTarget = undefined;
|
|
431
431
|
}
|
|
432
432
|
if (target !== undefined && position !== undefined) {
|
|
433
433
|
target.classList.add(DROP_CLASSES[position]);
|
|
434
|
-
this.#
|
|
434
|
+
this.#dropTarget = { node: target, position: position };
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
|
|
@@ -462,8 +462,8 @@ class TreeView {
|
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
#finishDrag() {
|
|
465
|
-
if (this.#
|
|
466
|
-
this.#
|
|
465
|
+
if (this.#dragSource !== undefined) this.#dragSource.classList.remove(DRAGGING_CLASS);
|
|
466
|
+
this.#dragSource = undefined;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
#descriptor(node) {
|
|
@@ -484,9 +484,9 @@ class TreeView {
|
|
|
484
484
|
// Строится один раз по полному DOM, ленивые поддеревья и переносы достраивают её точечно.
|
|
485
485
|
#buildStructure() {
|
|
486
486
|
this.#nodes = [];
|
|
487
|
-
this.#
|
|
487
|
+
this.#nodeIndex = new Map();
|
|
488
488
|
this.#roots = [];
|
|
489
|
-
for (const child of [...this.#
|
|
489
|
+
for (const child of [...this.#mainTest.children]) {
|
|
490
490
|
if (child.tagName.toLowerCase() !== "tree_view_button") continue;
|
|
491
491
|
this.#register(child, -1);
|
|
492
492
|
}
|
|
@@ -495,7 +495,7 @@ class TreeView {
|
|
|
495
495
|
|
|
496
496
|
/** Структура изменилась: плоский список и окно надо пересобрать */
|
|
497
497
|
#touch() {
|
|
498
|
-
this.#
|
|
498
|
+
this.#rowsDirty = true;
|
|
499
499
|
this.#stamp += 1;
|
|
500
500
|
}
|
|
501
501
|
|
|
@@ -516,7 +516,7 @@ class TreeView {
|
|
|
516
516
|
hidden: false,
|
|
517
517
|
};
|
|
518
518
|
this.#nodes.push(record);
|
|
519
|
-
this.#
|
|
519
|
+
this.#nodeIndex.set(button, index);
|
|
520
520
|
if (parent === -1) this.#roots.push(index);
|
|
521
521
|
else this.#nodes[parent].children.push(index);
|
|
522
522
|
if (html !== null) {
|
|
@@ -535,7 +535,7 @@ class TreeView {
|
|
|
535
535
|
* можно собирать из него, не оглядываясь на документ.
|
|
536
536
|
*/
|
|
537
537
|
#project() {
|
|
538
|
-
if (!this.#
|
|
538
|
+
if (!this.#rowsDirty) return this.#rows;
|
|
539
539
|
const rows = [];
|
|
540
540
|
const positions = new Map();
|
|
541
541
|
const childrenOf = new Map();
|
|
@@ -571,17 +571,17 @@ class TreeView {
|
|
|
571
571
|
const roots = [];
|
|
572
572
|
for (const root of this.#roots) if (walk(root, 0) > 0) roots.push(root);
|
|
573
573
|
this.#rows = rows;
|
|
574
|
-
this.#
|
|
575
|
-
this.#
|
|
576
|
-
this.#
|
|
577
|
-
this.#
|
|
578
|
-
this.#
|
|
579
|
-
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;
|
|
580
580
|
// Верхняя граница каждой позиции: для переменной высоты окно и прокрутка считаются по ней
|
|
581
581
|
const offsets = [0];
|
|
582
582
|
for (const value of heights) offsets.push(offsets[offsets.length - 1] + value);
|
|
583
583
|
this.#offsets = offsets;
|
|
584
|
-
this.#
|
|
584
|
+
this.#rowsDirty = false;
|
|
585
585
|
return rows;
|
|
586
586
|
}
|
|
587
587
|
|
|
@@ -593,19 +593,19 @@ class TreeView {
|
|
|
593
593
|
|
|
594
594
|
/** Переменная высота строк: высоты из данных (функция) или измеренные по DOM */
|
|
595
595
|
#isVariable() {
|
|
596
|
-
return this.#
|
|
596
|
+
return this.#variableHeight || this.#measureDom;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
/** Высота строки с индексом модели: из данных, из кеша замеров или оценка */
|
|
600
600
|
#heightOf(index) {
|
|
601
|
-
if (this.#
|
|
601
|
+
if (this.#variableHeight) {
|
|
602
602
|
if (!this.#heights.has(index)) {
|
|
603
603
|
const value = Number(this.#virtual.rowHeight(this.#nodes[index], index));
|
|
604
604
|
this.#heights.set(index, Number.isFinite(value) && value > 0 ? value : 26);
|
|
605
605
|
}
|
|
606
606
|
return this.#heights.get(index);
|
|
607
607
|
}
|
|
608
|
-
if (this.#
|
|
608
|
+
if (this.#measureDom) {
|
|
609
609
|
if (this.#heights.has(index)) return this.#heights.get(index);
|
|
610
610
|
return this.#virtual.estimatedRowHeight;
|
|
611
611
|
}
|
|
@@ -619,13 +619,13 @@ class TreeView {
|
|
|
619
619
|
* тела `DataGrid`).
|
|
620
620
|
*/
|
|
621
621
|
#measureWindow() {
|
|
622
|
-
for (const button of this.#
|
|
623
|
-
const index = this.#
|
|
622
|
+
for (const button of this.#mainTest.querySelectorAll("tree_view_button")) {
|
|
623
|
+
const index = this.#nodeIndex.get(button);
|
|
624
624
|
if (index === undefined) continue;
|
|
625
625
|
const measured = Math.round(button.getBoundingClientRect().height);
|
|
626
626
|
if (measured > 0 && this.#heights.get(index) !== measured) {
|
|
627
627
|
this.#heights.set(index, measured);
|
|
628
|
-
this.#
|
|
628
|
+
this.#rowsDirty = true;
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
}
|
|
@@ -634,12 +634,12 @@ class TreeView {
|
|
|
634
634
|
#rowHeight() {
|
|
635
635
|
if (this.#virtual === undefined) return 26;
|
|
636
636
|
if (this.#virtual.rowHeight !== undefined) return this.#virtual.rowHeight;
|
|
637
|
-
return this.#
|
|
637
|
+
return this.#measuredRowHeight === undefined ? 26 : this.#measuredRowHeight;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
640
|
#measureRow() {
|
|
641
641
|
if (this.#virtual === undefined || this.#isVariable() || this.#virtual.rowHeight !== undefined) return 0;
|
|
642
|
-
const sample = this.#
|
|
642
|
+
const sample = this.#mainTest.querySelector("tree_view_button");
|
|
643
643
|
return sample === null ? 0 : Math.round(sample.getBoundingClientRect().height);
|
|
644
644
|
}
|
|
645
645
|
|
|
@@ -648,9 +648,9 @@ class TreeView {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
#bindScroll() {
|
|
651
|
-
if (this.#
|
|
652
|
-
this.#
|
|
653
|
-
this.#
|
|
651
|
+
if (this.#scrollBound) return;
|
|
652
|
+
this.#scrollBound = true;
|
|
653
|
+
this.#chuiTreeView.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
#onScroll = () => {
|
|
@@ -679,15 +679,15 @@ class TreeView {
|
|
|
679
679
|
if (remeasure) {
|
|
680
680
|
// Переменный режим по DOM: сначала измеряем уже отрисованные строки — их настоящие
|
|
681
681
|
// высоты уточняют смещения, и только потом считается окно
|
|
682
|
-
if (this.#
|
|
682
|
+
if (this.#measureDom) this.#measureWindow();
|
|
683
683
|
// Настоящая высота строки может отличаться от умолчания: распорки и окно считаются
|
|
684
684
|
// по ней, поэтому замер идёт до расчёта окна
|
|
685
685
|
const measured = this.#measureRow();
|
|
686
|
-
if (measured > 0 && measured !== this.#
|
|
687
|
-
this.#
|
|
688
|
-
this.#
|
|
686
|
+
if (measured > 0 && measured !== this.#measuredRowHeight) {
|
|
687
|
+
this.#measuredRowHeight = measured;
|
|
688
|
+
this.#chuiTreeView.style.setProperty("--tree_view_row_height", `${measured}px`);
|
|
689
689
|
// Смещения в проекции считались по прежней высоте — пересобрать
|
|
690
|
-
this.#
|
|
690
|
+
this.#rowsDirty = true;
|
|
691
691
|
force = true;
|
|
692
692
|
}
|
|
693
693
|
}
|
|
@@ -695,7 +695,7 @@ class TreeView {
|
|
|
695
695
|
const variable = this.#isVariable();
|
|
696
696
|
const height = this.#rowHeight();
|
|
697
697
|
const buffer = this.#buffer();
|
|
698
|
-
const viewport = Math.max(1, this.#
|
|
698
|
+
const viewport = Math.max(1, this.#chuiTreeView.clientHeight);
|
|
699
699
|
// Позиция строки по верхней границе прокрутки (двоичный поиск по смещениям)
|
|
700
700
|
const positionFor = (top) => {
|
|
701
701
|
let low = 0;
|
|
@@ -713,8 +713,8 @@ class TreeView {
|
|
|
713
713
|
// нет паддинга, рамки и зазоров между отрисованными строками, поэтому «модельный» низ чуть
|
|
714
714
|
// выше настоящего — только по нему последняя строка (End, стрелка вниз) осталась бы обрезанной
|
|
715
715
|
const modelBottom = Math.max(0, total - viewport);
|
|
716
|
-
const realBottom = Math.max(0, this.#
|
|
717
|
-
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));
|
|
718
718
|
// Низ окна для переменной высоты — позиция строки под нижней кромкой плюс одна строка,
|
|
719
719
|
// иначе последняя видимая строка выпадала бы из окна
|
|
720
720
|
const windowTo = (top) =>
|
|
@@ -730,7 +730,7 @@ class TreeView {
|
|
|
730
730
|
from = Math.max(0, positionFor(scrollTop) - buffer);
|
|
731
731
|
to = windowTo(scrollTop);
|
|
732
732
|
}
|
|
733
|
-
if (scrollTop !== this.#
|
|
733
|
+
if (scrollTop !== this.#chuiTreeView.scrollTop) this.#chuiTreeView.scrollTop = scrollTop;
|
|
734
734
|
// Ни окно, ни структура не изменились (прокрутка в пределах строки) — пересобирать
|
|
735
735
|
// нечего: пересборка на каждом кадре только дёргала бы DOM и фокус
|
|
736
736
|
if (
|
|
@@ -738,13 +738,13 @@ class TreeView {
|
|
|
738
738
|
this.#win !== undefined &&
|
|
739
739
|
from === this.#win.from &&
|
|
740
740
|
to === this.#win.to &&
|
|
741
|
-
this.#
|
|
742
|
-
this.#
|
|
741
|
+
this.#winRows === rows.length &&
|
|
742
|
+
this.#winStamp === this.#stamp
|
|
743
743
|
)
|
|
744
744
|
return;
|
|
745
745
|
// Кто держит фокус: снимок до мутации — после неё `document.activeElement` уже `body`
|
|
746
746
|
const active = document.activeElement;
|
|
747
|
-
const activeIndex = active instanceof Element ? this.#
|
|
747
|
+
const activeIndex = active instanceof Element ? this.#nodeIndex.get(active) : undefined;
|
|
748
748
|
const keepIndex = activeIndex === undefined ? undefined : this.#pinRow(rows, activeIndex);
|
|
749
749
|
const visible = new Set();
|
|
750
750
|
for (let position = from; position < to; position++) {
|
|
@@ -763,7 +763,7 @@ class TreeView {
|
|
|
763
763
|
current = this.#nodes[current].parent;
|
|
764
764
|
}
|
|
765
765
|
}
|
|
766
|
-
const sizeOf = (index) => this.#
|
|
766
|
+
const sizeOf = (index) => this.#rowSizes.get(index) || 1;
|
|
767
767
|
const spacer = (px) => {
|
|
768
768
|
const node = document.createElement("tree_view_spacer");
|
|
769
769
|
node.setAttribute("aria-hidden", "true");
|
|
@@ -771,10 +771,10 @@ class TreeView {
|
|
|
771
771
|
return node;
|
|
772
772
|
};
|
|
773
773
|
// Сколько занимает пропущенная ветка: в переменном режиме — суммарная высота её поддерева
|
|
774
|
-
const subtreePx = (index) => (variable ? this.#
|
|
774
|
+
const subtreePx = (index) => (variable ? this.#subtreeHeights.get(index) || 0 : sizeOf(index) * height);
|
|
775
775
|
const fill = (container, parentIndex) => {
|
|
776
776
|
// Дети берутся из проекции: скрытое поиском и свёрнутое в неё уже не попадает
|
|
777
|
-
const children = parentIndex === -1 ? this.#
|
|
777
|
+
const children = parentIndex === -1 ? this.#rootRows : this.#childRows.get(parentIndex) || [];
|
|
778
778
|
const parts = [];
|
|
779
779
|
let gap = 0;
|
|
780
780
|
for (const index of children) {
|
|
@@ -789,7 +789,7 @@ class TreeView {
|
|
|
789
789
|
}
|
|
790
790
|
const record = this.#nodes[index];
|
|
791
791
|
// Высота из данных задаётся строке инлайном — иначе DOM разошёлся бы с моделью
|
|
792
|
-
if (this.#
|
|
792
|
+
if (this.#variableHeight) {
|
|
793
793
|
const rowHeightPx = this.#heightOf(index);
|
|
794
794
|
record.button.style.minHeight = `${rowHeightPx}px`;
|
|
795
795
|
record.button.style.height = `${rowHeightPx}px`;
|
|
@@ -809,14 +809,14 @@ class TreeView {
|
|
|
809
809
|
else record.html.replaceChildren();
|
|
810
810
|
}
|
|
811
811
|
};
|
|
812
|
-
fill(this.#
|
|
813
|
-
if (this.#status !== undefined) this.#
|
|
812
|
+
fill(this.#mainTest, -1);
|
|
813
|
+
if (this.#status !== undefined) this.#mainTest.appendChild(this.#status);
|
|
814
814
|
this.#restoreFocus(keepIndex);
|
|
815
815
|
this.#applySelection(false);
|
|
816
816
|
this.#syncDraggable();
|
|
817
817
|
this.#win = { from: from, to: to };
|
|
818
|
-
this.#
|
|
819
|
-
this.#
|
|
818
|
+
this.#winStamp = this.#stamp;
|
|
819
|
+
this.#winRows = rows.length;
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
/**
|
|
@@ -825,7 +825,7 @@ class TreeView {
|
|
|
825
825
|
* видимая: фокус остаётся в дереве, а не уходит в `body`.
|
|
826
826
|
*/
|
|
827
827
|
#pinRow(rows, index) {
|
|
828
|
-
const position = this.#
|
|
828
|
+
const position = this.#rowPositions.get(index);
|
|
829
829
|
if (position !== undefined) return rows[position].index;
|
|
830
830
|
let nearest = undefined;
|
|
831
831
|
for (const row of rows) {
|
|
@@ -881,17 +881,17 @@ class TreeView {
|
|
|
881
881
|
return;
|
|
882
882
|
}
|
|
883
883
|
// Позиция строки в плоском списке: карта проекции, чтобы не перебирать все строки
|
|
884
|
-
const modelIndex = this.#
|
|
885
|
-
const place = modelIndex === undefined ? undefined : this.#
|
|
884
|
+
const modelIndex = this.#nodeIndex.get(button);
|
|
885
|
+
const place = modelIndex === undefined ? undefined : this.#rowPositions.get(modelIndex);
|
|
886
886
|
if (place === undefined) return;
|
|
887
887
|
// В переменном режиме верх и высота строки берутся из смещений проекции
|
|
888
888
|
const variable = this.#isVariable();
|
|
889
|
-
const height = variable ? this.#
|
|
890
|
-
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);
|
|
891
891
|
const top = variable ? this.#offsets[place] : place * height;
|
|
892
|
-
const scrollTop = this.#
|
|
892
|
+
const scrollTop = this.#chuiTreeView.scrollTop;
|
|
893
893
|
if (top < scrollTop || top + height > scrollTop + viewport) {
|
|
894
|
-
this.#
|
|
894
|
+
this.#chuiTreeView.scrollTop = Math.max(0, top - Math.floor(viewport / 2));
|
|
895
895
|
this.#renderVirtual();
|
|
896
896
|
}
|
|
897
897
|
button.focus();
|
|
@@ -901,7 +901,7 @@ class TreeView {
|
|
|
901
901
|
#onStructureClick = (event) => {
|
|
902
902
|
const button = event.target instanceof Element ? event.target.closest("tree_view_button[aria-expanded]") : null;
|
|
903
903
|
if (button === null) return;
|
|
904
|
-
const index = this.#
|
|
904
|
+
const index = this.#nodeIndex.get(button);
|
|
905
905
|
if (index !== undefined) this.#syncSubtree(index);
|
|
906
906
|
const panel = button.nextElementSibling;
|
|
907
907
|
if (panel !== null && panel.tagName.toLowerCase() === "tree_view_panel") {
|
|
@@ -917,7 +917,7 @@ class TreeView {
|
|
|
917
917
|
if (record === undefined || record.html === null) return;
|
|
918
918
|
for (const child of [...record.html.children]) {
|
|
919
919
|
if (child.tagName.toLowerCase() !== "tree_view_button") continue;
|
|
920
|
-
if (this.#
|
|
920
|
+
if (this.#nodeIndex.has(child)) continue;
|
|
921
921
|
this.#register(child, index);
|
|
922
922
|
}
|
|
923
923
|
}
|
|
@@ -937,8 +937,8 @@ class TreeView {
|
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
#syncMove(source, target, position) {
|
|
940
|
-
const index = this.#
|
|
941
|
-
const targetIndex = this.#
|
|
940
|
+
const index = this.#nodeIndex.get(source);
|
|
941
|
+
const targetIndex = this.#nodeIndex.get(target);
|
|
942
942
|
if (index === undefined || targetIndex === undefined) return;
|
|
943
943
|
if (position === TreeView.DROP.INSIDE) this.#relink(index, targetIndex);
|
|
944
944
|
else {
|
|
@@ -978,7 +978,7 @@ class TreeView {
|
|
|
978
978
|
this.#announce(I18n.t("treeView.found", { count: this.#matches.size }));
|
|
979
979
|
return this;
|
|
980
980
|
}
|
|
981
|
-
const buttons = [...this.#
|
|
981
|
+
const buttons = [...this.#mainTest.querySelectorAll("tree_view_button")];
|
|
982
982
|
const visible = new Set();
|
|
983
983
|
for (const button of buttons) {
|
|
984
984
|
if (!this.#title(button).toLocaleLowerCase().includes(text)) continue;
|
|
@@ -1000,7 +1000,7 @@ class TreeView {
|
|
|
1000
1000
|
}
|
|
1001
1001
|
}
|
|
1002
1002
|
for (const button of buttons) if (!visible.has(button)) button.classList.add(TreeView.#HIDDEN_CLASS);
|
|
1003
|
-
for (const panel of this.#
|
|
1003
|
+
for (const panel of this.#mainTest.querySelectorAll("tree_view_panel")) {
|
|
1004
1004
|
if (!visible.has(panel)) panel.classList.add(TreeView.#HIDDEN_CLASS);
|
|
1005
1005
|
// Раскрытая панель должна быть видна целиком: max-height после раскрытия пересчитываем
|
|
1006
1006
|
else if (panel.style.maxHeight !== "") panel.style.maxHeight = "max-content";
|
|
@@ -1061,7 +1061,7 @@ class TreeView {
|
|
|
1061
1061
|
if (target === undefined || target === null) return undefined;
|
|
1062
1062
|
const node = target instanceof Element ? target : target.button;
|
|
1063
1063
|
if (!(node instanceof Element)) return undefined;
|
|
1064
|
-
const index = this.#
|
|
1064
|
+
const index = this.#nodeIndex.get(node);
|
|
1065
1065
|
return index === undefined ? undefined : this.#nodes[index];
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
@@ -1092,7 +1092,7 @@ class TreeView {
|
|
|
1092
1092
|
|
|
1093
1093
|
/** Снять фильтр поиска: возвращает все узлы и снимает подсветку */
|
|
1094
1094
|
clearSearch() {
|
|
1095
|
-
for (const node of this.#
|
|
1095
|
+
for (const node of this.#mainTest.querySelectorAll("." + TreeView.#HIDDEN_CLASS))
|
|
1096
1096
|
node.classList.remove(TreeView.#HIDDEN_CLASS);
|
|
1097
1097
|
for (const node of this.#matches) node.classList.remove("tree_view_button_match");
|
|
1098
1098
|
for (const record of this.#nodes) record.hidden = false;
|
|
@@ -1113,7 +1113,7 @@ class TreeView {
|
|
|
1113
1113
|
});
|
|
1114
1114
|
this.#status.setAttribute("role", "status");
|
|
1115
1115
|
this.#status.setAttribute("aria-live", "polite");
|
|
1116
|
-
this.#
|
|
1116
|
+
this.#mainTest.appendChild(this.#status);
|
|
1117
1117
|
}
|
|
1118
1118
|
this.#status.innerText = text;
|
|
1119
1119
|
}
|
|
@@ -1121,8 +1121,8 @@ class TreeView {
|
|
|
1121
1121
|
// === ОБРАБОТЧИКИ ===
|
|
1122
1122
|
#onClick = (event) => {
|
|
1123
1123
|
const node = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1124
|
-
if (node === null || !this.#
|
|
1125
|
-
const multiple = this.#
|
|
1124
|
+
if (node === null || !this.#mainTest.contains(node)) return;
|
|
1125
|
+
const multiple = this.#selectionMode === TreeView.SELECTION.MULTIPLE;
|
|
1126
1126
|
if (multiple && (event.ctrlKey || event.metaKey)) this.#toggleNode(node);
|
|
1127
1127
|
else if (multiple && event.shiftKey) this.#selectRange(node);
|
|
1128
1128
|
else {
|
|
@@ -1140,10 +1140,10 @@ class TreeView {
|
|
|
1140
1140
|
if (!event.ctrlKey && !event.metaKey && !event.shiftKey) return;
|
|
1141
1141
|
const target = event.target instanceof Element ? event.target : document.activeElement;
|
|
1142
1142
|
const node = target instanceof Element ? target.closest("tree_view_button") : null;
|
|
1143
|
-
if (node === null || !this.#
|
|
1143
|
+
if (node === null || !this.#mainTest.contains(node)) return;
|
|
1144
1144
|
event.preventDefault();
|
|
1145
1145
|
event.stopPropagation();
|
|
1146
|
-
if (event.shiftKey && this.#
|
|
1146
|
+
if (event.shiftKey && this.#selectionMode === TreeView.SELECTION.MULTIPLE) this.#selectRange(node);
|
|
1147
1147
|
else this.#toggleNode(node);
|
|
1148
1148
|
};
|
|
1149
1149
|
|
|
@@ -1183,7 +1183,7 @@ class TreeView {
|
|
|
1183
1183
|
// Клавиатурная альтернатива перетаскиванию: Alt+↑/↓ переставляет узел среди соседей.
|
|
1184
1184
|
// Соседей берём из модели: в виртуальном режиме в DOM только окно
|
|
1185
1185
|
#moveByKeyboard(node, step) {
|
|
1186
|
-
const index = this.#
|
|
1186
|
+
const index = this.#nodeIndex.get(node);
|
|
1187
1187
|
if (index === undefined) return;
|
|
1188
1188
|
const parent = this.#nodes[index].parent;
|
|
1189
1189
|
const siblings = parent === -1 ? this.#roots : this.#nodes[parent].children;
|
|
@@ -1192,14 +1192,14 @@ class TreeView {
|
|
|
1192
1192
|
const target = this.#nodes[targetIndex].button;
|
|
1193
1193
|
const position = step < 0 ? TreeView.DROP.BEFORE : TreeView.DROP.AFTER;
|
|
1194
1194
|
if (!this.#moveNodes(node, target, position)) return;
|
|
1195
|
-
if (this.#
|
|
1195
|
+
if (this.#onDrop !== undefined) this.#onDrop(this.#descriptor(node), this.#descriptor(target), position);
|
|
1196
1196
|
node.focus();
|
|
1197
1197
|
}
|
|
1198
1198
|
|
|
1199
1199
|
#onDragStart = (event) => {
|
|
1200
1200
|
const node = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1201
1201
|
if (node === null) return;
|
|
1202
|
-
this.#
|
|
1202
|
+
this.#dragSource = node;
|
|
1203
1203
|
node.classList.add(DRAGGING_CLASS);
|
|
1204
1204
|
if (event.dataTransfer === null) return;
|
|
1205
1205
|
event.dataTransfer.effectAllowed = "move";
|
|
@@ -1207,10 +1207,10 @@ class TreeView {
|
|
|
1207
1207
|
};
|
|
1208
1208
|
|
|
1209
1209
|
#onDragOver = (event) => {
|
|
1210
|
-
if (this.#
|
|
1210
|
+
if (this.#dragSource === undefined) return;
|
|
1211
1211
|
const target = event.target instanceof Element ? event.target.closest("tree_view_button") : null;
|
|
1212
1212
|
// В себя и в собственное поддерево бросать нельзя: узел потерял бы ветку
|
|
1213
|
-
if (target === null || this.#inOwnSubtree(this.#
|
|
1213
|
+
if (target === null || this.#inOwnSubtree(this.#dragSource, target)) {
|
|
1214
1214
|
this.#applyDropTarget(undefined, undefined);
|
|
1215
1215
|
if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "none";
|
|
1216
1216
|
return;
|
|
@@ -1221,20 +1221,20 @@ class TreeView {
|
|
|
1221
1221
|
};
|
|
1222
1222
|
|
|
1223
1223
|
#onDragLeave = (event) => {
|
|
1224
|
-
if (event.relatedTarget instanceof Element && this.#
|
|
1224
|
+
if (event.relatedTarget instanceof Element && this.#mainTest.contains(event.relatedTarget)) return;
|
|
1225
1225
|
this.#applyDropTarget(undefined, undefined);
|
|
1226
1226
|
};
|
|
1227
1227
|
|
|
1228
1228
|
#onDropEvent = (event) => {
|
|
1229
|
-
if (this.#
|
|
1229
|
+
if (this.#dragSource === undefined || this.#dropTarget === undefined) return;
|
|
1230
1230
|
event.preventDefault();
|
|
1231
|
-
const source = this.#
|
|
1232
|
-
const target = this.#
|
|
1233
|
-
const position = this.#
|
|
1231
|
+
const source = this.#dragSource;
|
|
1232
|
+
const target = this.#dropTarget.node;
|
|
1233
|
+
const position = this.#dropTarget.position;
|
|
1234
1234
|
this.#applyDropTarget(undefined, undefined);
|
|
1235
1235
|
this.#finishDrag();
|
|
1236
|
-
if (this.#
|
|
1237
|
-
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);
|
|
1238
1238
|
};
|
|
1239
1239
|
|
|
1240
1240
|
#onDragEnd = () => {
|
|
@@ -1243,27 +1243,29 @@ class TreeView {
|
|
|
1243
1243
|
};
|
|
1244
1244
|
|
|
1245
1245
|
static Button(options = { title: String(), listener: () => {} }) {
|
|
1246
|
-
|
|
1246
|
+
const button = document.createElement("tree_view_button");
|
|
1247
1247
|
if (options.listener !== undefined) button.addEventListener("click", options.listener);
|
|
1248
1248
|
makeKeyboardButton(button, "treeitem");
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1249
|
+
const buttonText = document.createElement("tree_view_button_text");
|
|
1250
|
+
// Заголовок — текст, а не разметка: в `title` приходят имена файлов и папок, а тот же
|
|
1251
|
+
// узел читается как `textContent` (`#title`), и поиск сравнивал бы текст с тегами
|
|
1252
|
+
buttonText.textContent = options.title;
|
|
1253
|
+
button.appendChild(buttonText);
|
|
1252
1254
|
const item = { title: options.title, button: button };
|
|
1253
1255
|
TreeView.#registry.set(button, item);
|
|
1254
1256
|
return item;
|
|
1255
1257
|
}
|
|
1256
1258
|
|
|
1257
1259
|
static ExpandButton(options = { title: String(), subButtons: [], components: [], lazySubButtons: undefined }) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
+
const button = document.createElement("tree_view_button");
|
|
1261
|
+
const panel = document.createElement("tree_view_panel");
|
|
1260
1262
|
// Раскрытая панель переходит с фиксированной высоты на max-content: иначе высота
|
|
1261
1263
|
// остаётся «замороженной» после первого раскрытия
|
|
1262
1264
|
const transitionEndEvent = (event) => {
|
|
1263
1265
|
const target = event.currentTarget;
|
|
1264
1266
|
if (target.style.maxHeight && target.style.maxHeight !== "0px") target.style.maxHeight = "max-content";
|
|
1265
1267
|
};
|
|
1266
|
-
panel.id = "chui_tree_panel_" + (
|
|
1268
|
+
panel.id = "chui_tree_panel_" + (treePanelCounter += 1);
|
|
1267
1269
|
button.setAttribute("aria-controls", panel.id);
|
|
1268
1270
|
button.setAttribute("aria-expanded", "false");
|
|
1269
1271
|
makeKeyboardButton(button, "treeitem");
|
|
@@ -1283,19 +1285,19 @@ class TreeView {
|
|
|
1283
1285
|
panel.style.maxHeight = "0px";
|
|
1284
1286
|
}
|
|
1285
1287
|
});
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
button.appendChild(
|
|
1289
|
-
|
|
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");
|
|
1290
1292
|
barrow.innerHTML = new Icon(Icons.HARDWARE.KEYBOARD_ARROW_DOWN).getHTML();
|
|
1291
1293
|
button.appendChild(barrow);
|
|
1292
|
-
|
|
1294
|
+
const html = document.createElement("tree_view_html");
|
|
1293
1295
|
// Ленивое поддерево: содержимое строится один раз — при первом раскрытии,
|
|
1294
1296
|
// поэтому большое дерево не занимает DOM заранее (lazySubButtons: () => [...])
|
|
1295
|
-
let
|
|
1297
|
+
let subButtonsBuilt = false;
|
|
1296
1298
|
const buildSubs = () => {
|
|
1297
|
-
if (
|
|
1298
|
-
|
|
1299
|
+
if (subButtonsBuilt) return;
|
|
1300
|
+
subButtonsBuilt = true;
|
|
1299
1301
|
if (options.subButtons !== undefined) {
|
|
1300
1302
|
options.subButtons.forEach((sub) => {
|
|
1301
1303
|
html.appendChild(sub.button);
|