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,13 +1,13 @@
|
|
|
1
|
-
const {setStyles, optionOr} = require(
|
|
2
|
-
const {A11Y_ACTIVE_CLASS} = require(
|
|
3
|
-
const {Icon, Icons} = require(
|
|
4
|
-
const {Dialog} = require(
|
|
5
|
-
const {ContentBlock} = require(
|
|
6
|
-
const {Button} = require(
|
|
7
|
-
const {I18n} = require(
|
|
8
|
-
const {Desktop} = require(
|
|
9
|
-
const {CheckBox} = require(
|
|
10
|
-
const {Select} = require(
|
|
1
|
+
const { setStyles, optionOr } = require("../../modules/chui_functions");
|
|
2
|
+
const { A11Y_ACTIVE_CLASS } = require("../../modules/chui_a11y/chui_a11y");
|
|
3
|
+
const { Icon, Icons } = require("../chui_icons/icons");
|
|
4
|
+
const { Dialog } = require("../chui_modal/modal");
|
|
5
|
+
const { ContentBlock } = require("../chui_content_block/content_block");
|
|
6
|
+
const { Button } = require("../chui_button/button");
|
|
7
|
+
const { I18n } = require("../../modules/chui_i18n/chui_i18n");
|
|
8
|
+
const { Desktop } = require("../../modules/chui_services/desktop");
|
|
9
|
+
const { CheckBox } = require("../chui_inputs/chui_check_box/check_box");
|
|
10
|
+
const { Select } = require("../chui_inputs/chui_select_box/select_box");
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* `DataGrid` — сетка данных с описанием колонок.
|
|
@@ -81,75 +81,78 @@ class DataGridBody {
|
|
|
81
81
|
START: "start",
|
|
82
82
|
CENTER: "center",
|
|
83
83
|
END: "end",
|
|
84
|
-
NEAREST: "nearest"
|
|
84
|
+
NEAREST: "nearest",
|
|
85
85
|
};
|
|
86
86
|
static DEFAULT_BUFFER = 6;
|
|
87
87
|
static DEFAULT_LOAD_THRESHOLD = 3;
|
|
88
88
|
static #counter = 0;
|
|
89
89
|
|
|
90
90
|
#root = document.createElement("data_grid_body");
|
|
91
|
-
#
|
|
92
|
-
#
|
|
91
|
+
#spacerTop = document.createElement("data_grid_spacer");
|
|
92
|
+
#spacerBottom = document.createElement("data_grid_spacer");
|
|
93
93
|
#empty = document.createElement("data_grid_body_empty");
|
|
94
94
|
#loader = document.createElement("data_grid_body_loader");
|
|
95
95
|
// Окно в DOM: индекс строки → узел
|
|
96
96
|
#nodes = new Map();
|
|
97
97
|
#id = undefined;
|
|
98
98
|
#items = [];
|
|
99
|
-
#
|
|
99
|
+
#itemHeight = 34;
|
|
100
100
|
#buffer = DataGridBody.DEFAULT_BUFFER;
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
101
|
+
#itemTag = "data_grid_row";
|
|
102
|
+
#renderItem = undefined;
|
|
103
|
+
#onItemClickCallback = undefined;
|
|
104
|
+
#onActivate = undefined;
|
|
105
|
+
#onRender = undefined;
|
|
106
|
+
#onLoadMore = undefined;
|
|
107
|
+
#loadingText = undefined;
|
|
108
|
+
#loadThreshold = DataGridBody.DEFAULT_LOAD_THRESHOLD;
|
|
109
|
+
#hasMore = false;
|
|
110
|
+
#loadingMore = false;
|
|
111
111
|
// Длина, для которой запрос уже сделан: не просим ещё раз, пока данных не прибавилось
|
|
112
|
-
#
|
|
113
|
-
#
|
|
112
|
+
#requestedAt = -1;
|
|
113
|
+
#windowState = { from: 0, to: 0 };
|
|
114
114
|
#raf = null;
|
|
115
|
-
#
|
|
116
|
-
#
|
|
115
|
+
#refreshRaf = null;
|
|
116
|
+
#resizeObserver = undefined;
|
|
117
117
|
#bound = false;
|
|
118
118
|
|
|
119
|
-
constructor(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
constructor(
|
|
120
|
+
options = {
|
|
121
|
+
id: String(),
|
|
122
|
+
itemTag: String(),
|
|
123
|
+
itemHeight: Number(),
|
|
124
|
+
buffer: Number(),
|
|
125
|
+
height: String(),
|
|
126
|
+
emptyText: String(),
|
|
127
|
+
loadingText: String(),
|
|
128
|
+
hasMore: Boolean(),
|
|
129
|
+
loadMoreThreshold: Number(),
|
|
130
|
+
renderItem: undefined,
|
|
131
|
+
onItemClick: undefined,
|
|
132
|
+
onActivate: undefined,
|
|
133
|
+
onRender: undefined,
|
|
134
|
+
onLoadMore: undefined,
|
|
135
|
+
}
|
|
136
|
+
) {
|
|
135
137
|
DataGridBody.#counter += 1;
|
|
136
138
|
this.#id = optionOr(options.id, `chui_data_grid_body_${DataGridBody.#counter}`);
|
|
137
|
-
this.#
|
|
138
|
-
this.#
|
|
139
|
+
this.#itemTag = optionOr(options.itemTag, "data_grid_row");
|
|
140
|
+
this.#itemHeight = options.itemHeight === undefined ? 34 : options.itemHeight;
|
|
139
141
|
this.#buffer = options.buffer === undefined ? DataGridBody.DEFAULT_BUFFER : options.buffer;
|
|
140
|
-
this.#
|
|
141
|
-
this.#
|
|
142
|
-
this.#
|
|
143
|
-
this.#
|
|
144
|
-
this.#
|
|
145
|
-
this.#
|
|
146
|
-
this.#
|
|
142
|
+
this.#renderItem = options.renderItem;
|
|
143
|
+
this.#onItemClickCallback = options.onItemClick;
|
|
144
|
+
this.#onActivate = options.onActivate;
|
|
145
|
+
this.#onRender = options.onRender;
|
|
146
|
+
this.#onLoadMore = options.onLoadMore;
|
|
147
|
+
this.#loadingText = optionOr(options.loadingText, I18n.t("common.loading"));
|
|
148
|
+
this.#loadThreshold =
|
|
149
|
+
options.loadMoreThreshold === undefined ? DataGridBody.DEFAULT_LOAD_THRESHOLD : options.loadMoreThreshold;
|
|
147
150
|
// Без onLoadMore тело считает, что загружено всё
|
|
148
|
-
this.#
|
|
151
|
+
this.#hasMore = options.hasMore === undefined ? options.onLoadMore !== undefined : options.hasMore === true;
|
|
149
152
|
this.#root.id = this.#id;
|
|
150
|
-
this.#
|
|
151
|
-
this.#
|
|
152
|
-
for (const spacer of [this.#
|
|
153
|
+
this.#spacerTop.dataset.pos = "top";
|
|
154
|
+
this.#spacerBottom.dataset.pos = "bottom";
|
|
155
|
+
for (const spacer of [this.#spacerTop, this.#spacerBottom]) {
|
|
153
156
|
spacer.setAttribute("aria-hidden", "true");
|
|
154
157
|
spacer.style.height = "0px";
|
|
155
158
|
this.#root.appendChild(spacer);
|
|
@@ -163,30 +166,40 @@ class DataGridBody {
|
|
|
163
166
|
this.#root.appendChild(this.#loader);
|
|
164
167
|
this.#applyLoader();
|
|
165
168
|
this.setHeight(optionOr(options.height, DataGrid.DEFAULT_HEIGHT));
|
|
166
|
-
this.#root.addEventListener("scroll", this.#onScroll, {passive: true});
|
|
169
|
+
this.#root.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
167
170
|
// Размер тела зависит от layout'а и появляется только после вставки в документ:
|
|
168
171
|
// по нему считается окно, поэтому пересобираем его при изменении размера. Работу
|
|
169
172
|
// уводим в кадр: правки layout внутри колбэка ResizeObserver дают
|
|
170
173
|
// «ResizeObserver loop completed with undelivered notifications»
|
|
171
|
-
this.#
|
|
172
|
-
this.#
|
|
174
|
+
this.#resizeObserver = new ResizeObserver(() => this.#scheduleRefresh());
|
|
175
|
+
this.#resizeObserver.observe(this.#root);
|
|
173
176
|
this.#bound = true;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
// === ЧТЕНИЕ ===
|
|
177
|
-
getId() {
|
|
178
|
-
|
|
180
|
+
getId() {
|
|
181
|
+
return this.#id;
|
|
182
|
+
}
|
|
183
|
+
getItemHeight() {
|
|
184
|
+
return this.#itemHeight;
|
|
185
|
+
}
|
|
179
186
|
/** Окно, которое сейчас в DOM (вместе с запасом `buffer`) */
|
|
180
|
-
getRenderedRange() {
|
|
187
|
+
getRenderedRange() {
|
|
188
|
+
return { ...this.#windowState };
|
|
189
|
+
}
|
|
181
190
|
/** Только видимые строки — без запаса; это то, что реально видит пользователь */
|
|
182
191
|
getVisibleRange() {
|
|
183
192
|
const from = this.#firstVisible();
|
|
184
|
-
return {from: from, to: Math.min(this.#items.length, from + this.#visibleCount())};
|
|
193
|
+
return { from: from, to: Math.min(this.#items.length, from + this.#visibleCount()) };
|
|
185
194
|
}
|
|
186
195
|
/** Узел строки, если он сейчас в окне: узлов на всё содержимое нет */
|
|
187
|
-
getItemElement(index = Number()) {
|
|
196
|
+
getItemElement(index = Number()) {
|
|
197
|
+
return this.#nodes.get(index);
|
|
198
|
+
}
|
|
188
199
|
/** Корень тела: его вставляет сетка */
|
|
189
|
-
set() {
|
|
200
|
+
set() {
|
|
201
|
+
return this.#root;
|
|
202
|
+
}
|
|
190
203
|
|
|
191
204
|
// === ИЗМЕНЕНИЕ ===
|
|
192
205
|
setHeight(height = String()) {
|
|
@@ -200,7 +213,7 @@ class DataGridBody {
|
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
setItemHeight(height = Number()) {
|
|
203
|
-
this.#
|
|
216
|
+
this.#itemHeight = height;
|
|
204
217
|
for (const node of this.#nodes.values()) node.style.height = `${height}px`;
|
|
205
218
|
return this.refresh();
|
|
206
219
|
}
|
|
@@ -214,18 +227,18 @@ class DataGridBody {
|
|
|
214
227
|
return this;
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
setHasMore(
|
|
218
|
-
this.#
|
|
230
|
+
setHasMore(hasMore = true) {
|
|
231
|
+
this.#hasMore = hasMore === true;
|
|
219
232
|
this.#applyLoader();
|
|
220
|
-
if (this.#
|
|
233
|
+
if (this.#hasMore) this.#checkLoadMore(this.#windowState.to);
|
|
221
234
|
return this;
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
setLoadingMore(loading = true) {
|
|
225
|
-
this.#
|
|
238
|
+
this.#loadingMore = loading === true;
|
|
226
239
|
this.#applyLoader();
|
|
227
240
|
// Загрузка завершилась, но окно всё ещё у конца — возможно, нужен следующий запрос
|
|
228
|
-
if (!this.#
|
|
241
|
+
if (!this.#loadingMore) this.#checkLoadMore(this.#windowState.to);
|
|
229
242
|
return this;
|
|
230
243
|
}
|
|
231
244
|
|
|
@@ -239,7 +252,7 @@ class DataGridBody {
|
|
|
239
252
|
const count = this.#items.length;
|
|
240
253
|
if (count === 0) return this;
|
|
241
254
|
const target = Math.max(0, Math.min(index, count - 1));
|
|
242
|
-
const height = this.#
|
|
255
|
+
const height = this.#itemHeight;
|
|
243
256
|
const view = this.#root.clientHeight || this.#visibleCount() * height;
|
|
244
257
|
let top = this.#root.scrollTop;
|
|
245
258
|
if (align === DataGridBody.ALIGN.START) {
|
|
@@ -255,8 +268,8 @@ class DataGridBody {
|
|
|
255
268
|
if (target < from) top = target * height;
|
|
256
269
|
else if (target >= to) top = (target + 1) * height - view;
|
|
257
270
|
}
|
|
258
|
-
const
|
|
259
|
-
top = Math.max(0, Math.min(top,
|
|
271
|
+
const maximumTop = Math.max(0, count * height - view);
|
|
272
|
+
top = Math.max(0, Math.min(top, maximumTop));
|
|
260
273
|
if (top !== this.#root.scrollTop) this.#root.scrollTop = top;
|
|
261
274
|
// Окно пересобираем сразу: не ждём события scroll, иначе состояние видно «на кадр позже»
|
|
262
275
|
this.#renderWindow(this.#firstVisible());
|
|
@@ -271,14 +284,14 @@ class DataGridBody {
|
|
|
271
284
|
cancelAnimationFrame(this.#raf);
|
|
272
285
|
this.#raf = null;
|
|
273
286
|
}
|
|
274
|
-
if (this.#
|
|
275
|
-
cancelAnimationFrame(this.#
|
|
276
|
-
this.#
|
|
287
|
+
if (this.#refreshRaf !== null) {
|
|
288
|
+
cancelAnimationFrame(this.#refreshRaf);
|
|
289
|
+
this.#refreshRaf = null;
|
|
277
290
|
}
|
|
278
291
|
this.#root.removeEventListener("scroll", this.#onScroll);
|
|
279
|
-
if (this.#
|
|
280
|
-
this.#
|
|
281
|
-
this.#
|
|
292
|
+
if (this.#resizeObserver !== undefined) {
|
|
293
|
+
this.#resizeObserver.disconnect();
|
|
294
|
+
this.#resizeObserver = undefined;
|
|
282
295
|
}
|
|
283
296
|
for (const node of this.#nodes.values()) {
|
|
284
297
|
node.removeEventListener("click", this.#onItemClick);
|
|
@@ -289,35 +302,35 @@ class DataGridBody {
|
|
|
289
302
|
|
|
290
303
|
// === ВНУТРЕННЕЕ ===
|
|
291
304
|
#scheduleRefresh() {
|
|
292
|
-
if (this.#
|
|
293
|
-
this.#
|
|
294
|
-
this.#
|
|
305
|
+
if (this.#refreshRaf !== null) return;
|
|
306
|
+
this.#refreshRaf = requestAnimationFrame(() => {
|
|
307
|
+
this.#refreshRaf = null;
|
|
295
308
|
if (this.#bound) this.refresh();
|
|
296
309
|
});
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
#applyLoader() {
|
|
300
|
-
this.#loader.innerText = this.#
|
|
301
|
-
this.#loader.hidden = !this.#
|
|
302
|
-
this.#root.dataset.loading = this.#
|
|
313
|
+
this.#loader.innerText = this.#loadingText;
|
|
314
|
+
this.#loader.hidden = !this.#loadingMore;
|
|
315
|
+
this.#root.dataset.loading = this.#loadingMore ? "true" : "false";
|
|
303
316
|
this.#syncEmpty();
|
|
304
317
|
}
|
|
305
318
|
|
|
306
319
|
// «Нет данных» показываем только когда данных нет и новых не ждём: иначе текст пустого
|
|
307
320
|
// состояния спорил бы с индикатором догрузки на время запроса
|
|
308
321
|
#syncEmpty() {
|
|
309
|
-
this.#empty.hidden = this.#items.length > 0 || this.#
|
|
322
|
+
this.#empty.hidden = this.#items.length > 0 || this.#hasMore || this.#loadingMore;
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
#firstVisible() {
|
|
313
326
|
const count = this.#items.length;
|
|
314
327
|
if (count === 0) return 0;
|
|
315
|
-
return Math.min(count - 1, Math.max(0, Math.floor(Math.max(0, this.#root.scrollTop) / this.#
|
|
328
|
+
return Math.min(count - 1, Math.max(0, Math.floor(Math.max(0, this.#root.scrollTop) / this.#itemHeight)));
|
|
316
329
|
}
|
|
317
330
|
|
|
318
331
|
#visibleCount() {
|
|
319
|
-
const view = this.#root.clientHeight || this.#
|
|
320
|
-
return Math.max(1, Math.ceil(view / this.#
|
|
332
|
+
const view = this.#root.clientHeight || this.#itemHeight * 8;
|
|
333
|
+
return Math.max(1, Math.ceil(view / this.#itemHeight));
|
|
321
334
|
}
|
|
322
335
|
|
|
323
336
|
/** Последний индекс окна: запас `buffer` считается в строках, а не в пикселях */
|
|
@@ -336,7 +349,7 @@ class DataGridBody {
|
|
|
336
349
|
this.#nodes.delete(index);
|
|
337
350
|
}
|
|
338
351
|
}
|
|
339
|
-
let previous = this.#
|
|
352
|
+
let previous = this.#spacerTop;
|
|
340
353
|
for (let index = from; index < to; index++) {
|
|
341
354
|
let node = this.#nodes.get(index);
|
|
342
355
|
if (node === undefined) {
|
|
@@ -344,49 +357,49 @@ class DataGridBody {
|
|
|
344
357
|
this.#nodes.set(index, node);
|
|
345
358
|
}
|
|
346
359
|
// Высоту задаём инлайном: иначе фактическая геометрия разошлась бы с распорками
|
|
347
|
-
const height = `${this.#
|
|
360
|
+
const height = `${this.#itemHeight}px`;
|
|
348
361
|
if (node.style.height !== height) node.style.height = height;
|
|
349
362
|
// insertBefore(previous.nextSibling) ставит узел сразу за предыдущим и не трогает
|
|
350
363
|
// остальные, если порядок уже верный
|
|
351
364
|
if (node.previousSibling !== previous) this.#root.insertBefore(node, previous.nextSibling);
|
|
352
365
|
previous = node;
|
|
353
366
|
}
|
|
354
|
-
this.#
|
|
367
|
+
this.#windowState = { from: from, to: to };
|
|
355
368
|
this.#syncSpacers();
|
|
356
|
-
if (this.#
|
|
369
|
+
if (this.#onRender !== undefined) this.#onRender({ from: from, to: to });
|
|
357
370
|
this.#checkLoadMore(to);
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
#syncSpacers() {
|
|
361
374
|
const count = this.#items.length;
|
|
362
|
-
const {from, to} = this.#
|
|
363
|
-
this.#
|
|
364
|
-
this.#
|
|
375
|
+
const { from, to } = this.#windowState;
|
|
376
|
+
this.#spacerTop.style.height = count === 0 ? "0px" : `${from * this.#itemHeight}px`;
|
|
377
|
+
this.#spacerBottom.style.height = count === 0 ? "0px" : `${Math.max(0, (count - to) * this.#itemHeight)}px`;
|
|
365
378
|
}
|
|
366
379
|
|
|
367
380
|
#createItem(index = Number()) {
|
|
368
|
-
const node = document.createElement(this.#
|
|
381
|
+
const node = document.createElement(this.#itemTag);
|
|
369
382
|
node.id = `${this.#id}_item_${index}`;
|
|
370
383
|
node.dataset.index = String(index);
|
|
371
|
-
if (this.#
|
|
372
|
-
else this.#
|
|
384
|
+
if (this.#renderItem === undefined) node.innerText = String(this.#items[index]);
|
|
385
|
+
else this.#renderItem(this.#items[index], index, node);
|
|
373
386
|
node.addEventListener("click", this.#onItemClick);
|
|
374
387
|
node.addEventListener("dblclick", this.#onItemDblClick);
|
|
375
388
|
return node;
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
#checkLoadMore(to = Number()) {
|
|
379
|
-
if (this.#
|
|
392
|
+
if (this.#onLoadMore === undefined || this.#loadingMore || !this.#hasMore) return;
|
|
380
393
|
const count = this.#items.length;
|
|
381
394
|
// Тело пусто или окно дошло до конца загруженного
|
|
382
|
-
if (count > 0 && to < count - this.#
|
|
395
|
+
if (count > 0 && to < count - this.#loadThreshold) return;
|
|
383
396
|
// Запрос на такую же длину уже был: ждём данных, а не повторяем в пустоту
|
|
384
|
-
if (this.#
|
|
385
|
-
this.#
|
|
386
|
-
this.#
|
|
397
|
+
if (this.#requestedAt === count) return;
|
|
398
|
+
this.#requestedAt = count;
|
|
399
|
+
this.#loadingMore = true;
|
|
387
400
|
this.#applyLoader();
|
|
388
401
|
try {
|
|
389
|
-
this.#
|
|
402
|
+
this.#onLoadMore(count);
|
|
390
403
|
} catch (error) {
|
|
391
404
|
// Ошибка обработчика не должна оставлять тело «в загрузке»;
|
|
392
405
|
// #requested_at не сбрасываем — повтор это `DataGrid.loadMore()`
|
|
@@ -399,24 +412,24 @@ class DataGridBody {
|
|
|
399
412
|
let node = event.target;
|
|
400
413
|
while (node !== null && node !== this.#root) {
|
|
401
414
|
if (node.parentElement === this.#root && node.dataset.index !== undefined) {
|
|
402
|
-
return {index: Number(node.dataset.index), node: node};
|
|
415
|
+
return { index: Number(node.dataset.index), node: node };
|
|
403
416
|
}
|
|
404
417
|
node = node.parentElement;
|
|
405
418
|
}
|
|
406
|
-
return {index: -1, node: undefined};
|
|
419
|
+
return { index: -1, node: undefined };
|
|
407
420
|
}
|
|
408
421
|
|
|
409
422
|
#onItemClick = (event) => {
|
|
410
|
-
const {index, node} = this.#itemFromEvent(event);
|
|
423
|
+
const { index, node } = this.#itemFromEvent(event);
|
|
411
424
|
if (index === -1) return;
|
|
412
425
|
// Событие сетке нужно целиком: по нему она читает модификаторы Ctrl/Shift
|
|
413
|
-
if (this.#
|
|
426
|
+
if (this.#onItemClickCallback !== undefined) this.#onItemClickCallback(this.#items[index], index, node, event);
|
|
414
427
|
};
|
|
415
428
|
|
|
416
429
|
#onItemDblClick = (event) => {
|
|
417
|
-
const {index, node} = this.#itemFromEvent(event);
|
|
430
|
+
const { index, node } = this.#itemFromEvent(event);
|
|
418
431
|
if (index === -1) return;
|
|
419
|
-
if (this.#
|
|
432
|
+
if (this.#onActivate !== undefined) this.#onActivate(this.#items[index], index, node);
|
|
420
433
|
};
|
|
421
434
|
|
|
422
435
|
#onScroll = () => {
|
|
@@ -424,7 +437,7 @@ class DataGridBody {
|
|
|
424
437
|
this.#raf = requestAnimationFrame(() => {
|
|
425
438
|
this.#raf = null;
|
|
426
439
|
const from = this.#firstVisible();
|
|
427
|
-
const state = this.#
|
|
440
|
+
const state = this.#windowState;
|
|
428
441
|
// Видимая часть уже внутри отрисованного окна — перерисовка не нужна
|
|
429
442
|
if (from >= state.from && this.#endIndex(from) <= state.to) return;
|
|
430
443
|
this.#renderWindow(from);
|
|
@@ -436,24 +449,24 @@ class DataGrid {
|
|
|
436
449
|
static SELECTION = {
|
|
437
450
|
NONE: "none",
|
|
438
451
|
SINGLE: "single",
|
|
439
|
-
MULTI: "multi"
|
|
452
|
+
MULTI: "multi",
|
|
440
453
|
};
|
|
441
454
|
static SORT = {
|
|
442
455
|
NONE: "none",
|
|
443
456
|
ASC: "asc",
|
|
444
|
-
DESC: "desc"
|
|
457
|
+
DESC: "desc",
|
|
445
458
|
};
|
|
446
459
|
static SORT_ATTRIBUTE = {
|
|
447
460
|
ASC: "ascending",
|
|
448
461
|
DESC: "descending",
|
|
449
|
-
NONE: "none"
|
|
462
|
+
NONE: "none",
|
|
450
463
|
};
|
|
451
464
|
/** Тип редактора ячейки (`column.editType`) */
|
|
452
465
|
static EDIT_TYPE = {
|
|
453
466
|
TEXT: "text",
|
|
454
467
|
NUMBER: "number",
|
|
455
468
|
CHECKBOX: "checkbox",
|
|
456
|
-
SELECT: "select"
|
|
469
|
+
SELECT: "select",
|
|
457
470
|
};
|
|
458
471
|
/** Виды агрегатов для группировки (`aggregates`) */
|
|
459
472
|
static AGGREGATE = {
|
|
@@ -461,7 +474,7 @@ class DataGrid {
|
|
|
461
474
|
AVG: "avg",
|
|
462
475
|
MIN: "min",
|
|
463
476
|
MAX: "max",
|
|
464
|
-
COUNT: "count"
|
|
477
|
+
COUNT: "count",
|
|
465
478
|
};
|
|
466
479
|
// Значения — ключи словаря: подпись агрегата берётся при показе
|
|
467
480
|
static AGGREGATE_LABEL = {
|
|
@@ -469,12 +482,12 @@ class DataGrid {
|
|
|
469
482
|
avg: "dataGrid.aggregate.avg",
|
|
470
483
|
min: "dataGrid.aggregate.min",
|
|
471
484
|
max: "dataGrid.aggregate.max",
|
|
472
|
-
count: "dataGrid.aggregate.count"
|
|
485
|
+
count: "dataGrid.aggregate.count",
|
|
473
486
|
};
|
|
474
487
|
/** Что экспортирует `exportCSV()`: текущий вид или все загруженные строки */
|
|
475
488
|
static EXPORT = {
|
|
476
489
|
VIEW: "view",
|
|
477
|
-
ALL: "all"
|
|
490
|
+
ALL: "all",
|
|
478
491
|
};
|
|
479
492
|
/** Куда выравнивать строку в `scrollToRow()` (тело сетки — своё, см. `DataGridBody`) */
|
|
480
493
|
static ALIGN = DataGridBody.ALIGN;
|
|
@@ -492,13 +505,13 @@ class DataGrid {
|
|
|
492
505
|
#scroll = document.createElement("data_grid_scroll");
|
|
493
506
|
#bar = document.createElement("data_grid_bar");
|
|
494
507
|
#head = document.createElement("data_grid_head");
|
|
495
|
-
#
|
|
508
|
+
#headRow = document.createElement("data_grid_row");
|
|
496
509
|
#error = document.createElement("data_grid_error");
|
|
497
|
-
#
|
|
510
|
+
#errorText = document.createElement("data_grid_error_text");
|
|
498
511
|
#status = document.createElement("data_grid_status");
|
|
499
512
|
#list = undefined;
|
|
500
|
-
#
|
|
501
|
-
#
|
|
513
|
+
#chooserDialog = undefined;
|
|
514
|
+
#chooserBlock = undefined;
|
|
502
515
|
|
|
503
516
|
// === СОСТОЯНИЕ ===
|
|
504
517
|
#id = String();
|
|
@@ -507,26 +520,26 @@ class DataGrid {
|
|
|
507
520
|
// Видимость по колонкам: ключ скрытой колонки лежит здесь, поэтому DOM собирается по видимым
|
|
508
521
|
#hidden = new Set();
|
|
509
522
|
#pinned = [];
|
|
510
|
-
#
|
|
523
|
+
#columnReorder = false;
|
|
511
524
|
#data = [];
|
|
512
|
-
#
|
|
525
|
+
#rowHeight = DataGrid.DEFAULT_ROW_HEIGHT;
|
|
513
526
|
#selection = DataGrid.SELECTION.SINGLE;
|
|
514
527
|
#selected = new Set();
|
|
515
528
|
#anchor = undefined;
|
|
516
529
|
// Индекс строки в исходных данных: `render(row, index)` и ключ по умолчанию
|
|
517
|
-
#
|
|
530
|
+
#rowIndex = new Map();
|
|
518
531
|
#filter = "";
|
|
519
|
-
#
|
|
520
|
-
#sort = {key: undefined, direction: DataGrid.SORT.NONE};
|
|
521
|
-
#
|
|
522
|
-
#
|
|
532
|
+
#filterKeys = [];
|
|
533
|
+
#sort = { key: undefined, direction: DataGrid.SORT.NONE };
|
|
534
|
+
#groupBy = undefined;
|
|
535
|
+
#groupLabel = undefined;
|
|
523
536
|
#aggregates = {};
|
|
524
|
-
#
|
|
537
|
+
#groupCollapsedDefault = false;
|
|
525
538
|
#collapsed = new Map();
|
|
526
539
|
#editable = undefined;
|
|
527
540
|
#editing = undefined;
|
|
528
|
-
#active = {row: 0, col: 0};
|
|
529
|
-
#
|
|
541
|
+
#active = { row: 0, col: 0 };
|
|
542
|
+
#entryCount = 0;
|
|
530
543
|
|
|
531
544
|
// Производные данные (пересобираются одним проходом — см. #recompute)
|
|
532
545
|
#filtered = [];
|
|
@@ -535,103 +548,107 @@ class DataGrid {
|
|
|
535
548
|
#entries = [];
|
|
536
549
|
|
|
537
550
|
// Серверный режим
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
#
|
|
551
|
+
#dataSource = undefined;
|
|
552
|
+
#pageSize = DataGrid.DEFAULT_PAGE_SIZE;
|
|
553
|
+
#serverTotal = undefined;
|
|
541
554
|
#loading = false;
|
|
542
555
|
#failed = false;
|
|
543
|
-
#
|
|
556
|
+
#hasMore = false;
|
|
544
557
|
#request = 0;
|
|
545
558
|
|
|
546
559
|
// Подписчики
|
|
547
|
-
#
|
|
548
|
-
#
|
|
549
|
-
#
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
#
|
|
556
|
-
#
|
|
557
|
-
#
|
|
558
|
-
#
|
|
560
|
+
#onRowActivate = undefined;
|
|
561
|
+
#onSelectionChange = undefined;
|
|
562
|
+
#onCellEdit = undefined;
|
|
563
|
+
#onActiveCellChange = undefined;
|
|
564
|
+
#onColumnOrderChange = undefined;
|
|
565
|
+
#onDataLoad = undefined;
|
|
566
|
+
#onDataError = undefined;
|
|
567
|
+
#rowClass = undefined;
|
|
568
|
+
#customKey = undefined;
|
|
569
|
+
#emptyText = undefined;
|
|
570
|
+
#contextMenu = undefined;
|
|
571
|
+
#resizeObserver = undefined;
|
|
559
572
|
|
|
560
573
|
// Перестановка колонок
|
|
561
574
|
#drag = undefined;
|
|
562
575
|
|
|
563
|
-
constructor(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
576
|
+
constructor(
|
|
577
|
+
options = {
|
|
578
|
+
id: String(),
|
|
579
|
+
columns: Array(),
|
|
580
|
+
data: Array(),
|
|
581
|
+
rowHeight: Number(),
|
|
582
|
+
height: String(),
|
|
583
|
+
buffer: Number(),
|
|
584
|
+
selectable: String(),
|
|
585
|
+
rowKey: undefined,
|
|
586
|
+
filterKeys: Array(),
|
|
587
|
+
emptyText: String(),
|
|
588
|
+
label: String(),
|
|
589
|
+
editable: undefined,
|
|
590
|
+
pinned: undefined,
|
|
591
|
+
hiddenColumns: Array(),
|
|
592
|
+
columnChooser: Boolean(),
|
|
593
|
+
columnReorder: Boolean(),
|
|
594
|
+
dataSource: undefined,
|
|
595
|
+
pageSize: Number(),
|
|
596
|
+
rowClass: undefined,
|
|
597
|
+
groupBy: undefined,
|
|
598
|
+
groupLabel: undefined,
|
|
599
|
+
aggregates: Object(),
|
|
600
|
+
groupCollapsed: Boolean(),
|
|
601
|
+
onRowActivate: undefined,
|
|
602
|
+
onSelectionChange: undefined,
|
|
603
|
+
onCellEdit: undefined,
|
|
604
|
+
onActiveCellChange: undefined,
|
|
605
|
+
onColumnOrderChange: undefined,
|
|
606
|
+
onDataLoad: undefined,
|
|
607
|
+
onDataError: undefined,
|
|
608
|
+
}
|
|
609
|
+
) {
|
|
610
|
+
setStyles(__dirname + "/styles.css", "chUiJS_DataGrid");
|
|
596
611
|
DataGrid.#counter = DataGrid.#counter + 1;
|
|
597
612
|
this.#id = optionOr(options.id, `chui_data_grid_${DataGrid.#counter}`);
|
|
598
613
|
this.#columns = DataGrid.#normalizeColumns(options.columns);
|
|
599
614
|
this.#hidden = new Set(DataGrid.#normalizeKeys(options.hiddenColumns, this.#columns));
|
|
600
615
|
this.#data = Array.isArray(options.data) ? options.data : [];
|
|
601
|
-
this.#
|
|
616
|
+
this.#rowHeight = options.rowHeight === undefined ? DataGrid.DEFAULT_ROW_HEIGHT : options.rowHeight;
|
|
602
617
|
this.#selection = DataGrid.#normalizeSelection(options.selectable);
|
|
603
|
-
this.#
|
|
604
|
-
this.#
|
|
605
|
-
this.#
|
|
618
|
+
this.#customKey = options.rowKey;
|
|
619
|
+
this.#filterKeys = DataGrid.#normalizeKeys(options.filterKeys, this.#columns);
|
|
620
|
+
this.#emptyText = options.emptyText;
|
|
606
621
|
this.#editable = options.editable;
|
|
607
|
-
this.#
|
|
608
|
-
this.#
|
|
622
|
+
this.#groupBy = options.groupBy;
|
|
623
|
+
this.#groupLabel = options.groupLabel;
|
|
609
624
|
this.#aggregates = options.aggregates === undefined ? {} : options.aggregates;
|
|
610
|
-
this.#
|
|
611
|
-
this.#
|
|
612
|
-
this.#
|
|
613
|
-
this.#
|
|
614
|
-
this.#
|
|
615
|
-
this.#
|
|
616
|
-
this.#
|
|
617
|
-
this.#
|
|
618
|
-
this.#
|
|
619
|
-
this.#
|
|
620
|
-
this.#
|
|
621
|
-
this.#
|
|
625
|
+
this.#groupCollapsedDefault = options.groupCollapsed === true;
|
|
626
|
+
this.#rowClass = options.rowClass;
|
|
627
|
+
this.#onRowActivate = options.onRowActivate;
|
|
628
|
+
this.#onSelectionChange = options.onSelectionChange;
|
|
629
|
+
this.#onCellEdit = options.onCellEdit;
|
|
630
|
+
this.#onActiveCellChange = options.onActiveCellChange;
|
|
631
|
+
this.#onColumnOrderChange = options.onColumnOrderChange;
|
|
632
|
+
this.#onDataLoad = options.onDataLoad;
|
|
633
|
+
this.#onDataError = options.onDataError;
|
|
634
|
+
this.#dataSource = options.dataSource;
|
|
635
|
+
this.#pageSize = DataGrid.#normalizePageSize(options.pageSize);
|
|
636
|
+
this.#columnReorder = options.columnReorder === true;
|
|
622
637
|
this.setPinned(options.pinned);
|
|
623
638
|
|
|
624
639
|
// === сборка разметки ===
|
|
625
640
|
this.#root.id = this.#id;
|
|
626
641
|
this.#root.setAttribute("role", "grid");
|
|
627
642
|
this.#head.setAttribute("role", "rowgroup");
|
|
628
|
-
this.#
|
|
629
|
-
this.#
|
|
630
|
-
this.#head.appendChild(this.#
|
|
643
|
+
this.#headRow.setAttribute("role", "row");
|
|
644
|
+
this.#headRow.setAttribute("aria-rowindex", "1");
|
|
645
|
+
this.#head.appendChild(this.#headRow);
|
|
631
646
|
this.#scroll.appendChild(this.#head);
|
|
632
647
|
this.#root.appendChild(this.#scroll);
|
|
633
|
-
this.#error.appendChild(this.#
|
|
634
|
-
this.#error.appendChild(
|
|
648
|
+
this.#error.appendChild(this.#errorText);
|
|
649
|
+
this.#error.appendChild(
|
|
650
|
+
new Button({ title: I18n.t("dataGrid.retry"), clickEvent: () => this.retryLoad() }).set()
|
|
651
|
+
);
|
|
635
652
|
this.#error.hidden = true;
|
|
636
653
|
this.#root.insertBefore(this.#error, this.#scroll);
|
|
637
654
|
this.#status.classList.add("chui_visually_hidden");
|
|
@@ -644,10 +661,10 @@ class DataGrid {
|
|
|
644
661
|
this.#syncColumns();
|
|
645
662
|
this.#list = new DataGridBody({
|
|
646
663
|
itemTag: "data_grid_row",
|
|
647
|
-
itemHeight: this.#
|
|
664
|
+
itemHeight: this.#rowHeight,
|
|
648
665
|
buffer: options.buffer,
|
|
649
666
|
height: options.height === undefined ? DataGrid.DEFAULT_HEIGHT : options.height,
|
|
650
|
-
emptyText: this.#
|
|
667
|
+
emptyText: this.#emptyText,
|
|
651
668
|
hasMore: false,
|
|
652
669
|
onLoadMore: () => this.loadMore(),
|
|
653
670
|
renderItem: (entry, index, node) => this.#renderRow(entry, index, node),
|
|
@@ -666,32 +683,32 @@ class DataGrid {
|
|
|
666
683
|
this.#syncActiveCells();
|
|
667
684
|
// Смещения закреплённых колонок считаются по отрисованным узлам
|
|
668
685
|
this.#syncPinned();
|
|
669
|
-
}
|
|
686
|
+
},
|
|
670
687
|
});
|
|
671
688
|
this.#scroll.appendChild(this.#list.set());
|
|
672
689
|
this.#root.appendChild(this.#status);
|
|
673
690
|
|
|
674
691
|
// Заголовок: делегирование вместо подписки на каждую ячейку
|
|
675
|
-
this.#
|
|
676
|
-
this.#
|
|
677
|
-
this.#
|
|
678
|
-
this.#
|
|
679
|
-
this.#
|
|
680
|
-
this.#
|
|
681
|
-
this.#
|
|
692
|
+
this.#headRow.addEventListener("click", this.#onHeaderClick);
|
|
693
|
+
this.#headRow.addEventListener("keydown", this.#onHeaderKeyDown);
|
|
694
|
+
this.#headRow.addEventListener("dragstart", this.#onHeaderDragStart);
|
|
695
|
+
this.#headRow.addEventListener("dragover", this.#onHeaderDragOver);
|
|
696
|
+
this.#headRow.addEventListener("dragleave", this.#onHeaderDragLeave);
|
|
697
|
+
this.#headRow.addEventListener("drop", this.#onHeaderDrop);
|
|
698
|
+
this.#headRow.addEventListener("dragend", this.#onHeaderDragEnd);
|
|
682
699
|
// capture: правку начинает перехват, иначе тело списка успеет сообщить об активации
|
|
683
700
|
this.#root.addEventListener("dblclick", this.#onDoubleClick, true);
|
|
684
701
|
this.#root.addEventListener("keydown", this.#onKeyDown);
|
|
685
702
|
// Ширины закреплённых колонок зависят от размера окна — пересчитываем при изменении
|
|
686
|
-
this.#
|
|
687
|
-
this.#
|
|
703
|
+
this.#resizeObserver = new ResizeObserver(() => this.#syncPinned());
|
|
704
|
+
this.#resizeObserver.observe(this.#scroll);
|
|
688
705
|
// Горизонтальная прокрутка: закреплённые колонки удерживаем на месте
|
|
689
|
-
this.#scroll.addEventListener("scroll", this.#onScrollX, {passive: true});
|
|
706
|
+
this.#scroll.addEventListener("scroll", this.#onScrollX, { passive: true });
|
|
690
707
|
|
|
691
708
|
this.#reindex();
|
|
692
709
|
this.#buildHeader();
|
|
693
710
|
this.#recompute();
|
|
694
|
-
if (this.#
|
|
711
|
+
if (this.#dataSource !== undefined) this.reload();
|
|
695
712
|
}
|
|
696
713
|
|
|
697
714
|
// === НОРМАЛИЗАЦИЯ ВХОДА ===
|
|
@@ -778,8 +795,8 @@ class DataGrid {
|
|
|
778
795
|
/** Ячейка CSV: кавычки удваиваются, разделитель и перевод строки берутся в кавычки */
|
|
779
796
|
static #csvCell(value = undefined, delimiter = String()) {
|
|
780
797
|
const text = String(value === undefined || value === null ? "" : value);
|
|
781
|
-
const quoted = text.includes("
|
|
782
|
-
return quoted ? `"${text.replace(/"/g, "
|
|
798
|
+
const quoted = text.includes('"') || text.includes(delimiter) || text.includes("\n") || text.includes("\r");
|
|
799
|
+
return quoted ? `"${text.replace(/"/g, '""')}"` : text;
|
|
783
800
|
}
|
|
784
801
|
|
|
785
802
|
/** Ответ сервера: массив строк или `{rows, total}` */
|
|
@@ -826,13 +843,16 @@ class DataGrid {
|
|
|
826
843
|
}
|
|
827
844
|
}
|
|
828
845
|
}
|
|
829
|
-
this.#
|
|
846
|
+
this.#entryCount = this.#entries.length;
|
|
830
847
|
// Число строк для скринридера: элементы списка плюс строка заголовка, а в серверном
|
|
831
848
|
// режиме — то, что сообщил сервер (иначе он знал бы только про загруженное окно)
|
|
832
|
-
this.#root.setAttribute(
|
|
849
|
+
this.#root.setAttribute(
|
|
850
|
+
"aria-rowcount",
|
|
851
|
+
String((this.#serverTotal === undefined ? this.#entryCount : this.#serverTotal) + 1)
|
|
852
|
+
);
|
|
833
853
|
this.#syncColumns();
|
|
834
854
|
this.#list.setItems(this.#entries);
|
|
835
|
-
this.#list.setHasMore(this.#
|
|
855
|
+
this.#list.setHasMore(this.#hasMore);
|
|
836
856
|
this.#syncActiveCells();
|
|
837
857
|
this.#syncPinned();
|
|
838
858
|
}
|
|
@@ -857,12 +877,14 @@ class DataGrid {
|
|
|
857
877
|
|
|
858
878
|
#filterRows(rows = []) {
|
|
859
879
|
// Серверный режим фильтрует сам: локальный фильтр включился бы поверх серверного
|
|
860
|
-
if (this.#
|
|
880
|
+
if (this.#dataSource !== undefined) return rows.slice();
|
|
861
881
|
const text = this.#filter.trim().toLowerCase();
|
|
862
|
-
if (text === "" || this.#
|
|
863
|
-
return rows.filter((row) =>
|
|
864
|
-
|
|
865
|
-
|
|
882
|
+
if (text === "" || this.#filterKeys.length === 0) return rows.slice();
|
|
883
|
+
return rows.filter((row) =>
|
|
884
|
+
this.#filterKeys.some((key) => {
|
|
885
|
+
return DataGrid.#valueText(row[key]).toLowerCase().includes(text);
|
|
886
|
+
})
|
|
887
|
+
);
|
|
866
888
|
}
|
|
867
889
|
|
|
868
890
|
/** Сортировка стабильная: при равных значениях строки остаются в исходном порядке */
|
|
@@ -870,11 +892,13 @@ class DataGrid {
|
|
|
870
892
|
const key = this.#sort.key;
|
|
871
893
|
if (key === undefined || this.#sort.direction === DataGrid.SORT.NONE) return rows.slice();
|
|
872
894
|
const column = this.#columns.find((item) => item.key === key);
|
|
873
|
-
const compare =
|
|
874
|
-
|
|
875
|
-
|
|
895
|
+
const compare =
|
|
896
|
+
column !== undefined && typeof column.compare === "function"
|
|
897
|
+
? column.compare
|
|
898
|
+
: (left, right) => DataGrid.#compareValues(left[key], right[key]);
|
|
876
899
|
const sign = this.#sort.direction === DataGrid.SORT.DESC ? -1 : 1;
|
|
877
|
-
return rows
|
|
900
|
+
return rows
|
|
901
|
+
.map((row, index) => ({ row: row, index: index }))
|
|
878
902
|
.sort((left, right) => {
|
|
879
903
|
const result = compare(left.row, right.row);
|
|
880
904
|
if (result === 0 || Number.isNaN(result)) return left.index - right.index;
|
|
@@ -893,29 +917,27 @@ class DataGrid {
|
|
|
893
917
|
const leftNumber = Number(left);
|
|
894
918
|
const rightNumber = Number(right);
|
|
895
919
|
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber - rightNumber;
|
|
896
|
-
return String(left).localeCompare(String(right), undefined, {numeric: true, sensitivity: "base"});
|
|
920
|
+
return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: "base" });
|
|
897
921
|
}
|
|
898
922
|
|
|
899
923
|
/** Группы: порядок — первое появление ключа, поэтому он не зависит от сортировки */
|
|
900
924
|
#buildGroups(rows = []) {
|
|
901
|
-
if (this.#
|
|
902
|
-
const keyOf = typeof this.#
|
|
925
|
+
if (this.#groupBy === undefined) return [];
|
|
926
|
+
const keyOf = typeof this.#groupBy === "function" ? this.#groupBy : (row) => row[this.#groupBy];
|
|
903
927
|
const map = new Map();
|
|
904
928
|
for (const row of rows) {
|
|
905
929
|
const raw = keyOf(row);
|
|
906
|
-
const key = raw === undefined || raw === null || raw === ""
|
|
907
|
-
? I18n.t("dataGrid.groupNoValue")
|
|
908
|
-
: String(raw);
|
|
930
|
+
const key = raw === undefined || raw === null || raw === "" ? I18n.t("dataGrid.groupNoValue") : String(raw);
|
|
909
931
|
if (map.has(key) === false) map.set(key, []);
|
|
910
932
|
map.get(key).push(row);
|
|
911
933
|
}
|
|
912
934
|
const out = [];
|
|
913
|
-
for (const [key,
|
|
935
|
+
for (const [key, groupRows] of map) {
|
|
914
936
|
out.push({
|
|
915
937
|
key: key,
|
|
916
|
-
rows:
|
|
917
|
-
aggregates: this.#aggregateValues(
|
|
918
|
-
collapsed: this.#collapsed.has(key) ? this.#collapsed.get(key) : this.#
|
|
938
|
+
rows: groupRows,
|
|
939
|
+
aggregates: this.#aggregateValues(groupRows),
|
|
940
|
+
collapsed: this.#collapsed.has(key) ? this.#collapsed.get(key) : this.#groupCollapsedDefault,
|
|
919
941
|
});
|
|
920
942
|
}
|
|
921
943
|
return out;
|
|
@@ -928,23 +950,30 @@ class DataGrid {
|
|
|
928
950
|
const spec = this.#aggregates[key];
|
|
929
951
|
const column = this.#columns.find((item) => item.key === key);
|
|
930
952
|
if (typeof spec === "function") {
|
|
931
|
-
out[key] = {text: String(spec(rows, key)), label: column === undefined ? key : column.title};
|
|
953
|
+
out[key] = { text: String(spec(rows, key)), label: column === undefined ? key : column.title };
|
|
932
954
|
continue;
|
|
933
955
|
}
|
|
934
956
|
const values = rows.map((row) => row[key]).filter((value) => value !== undefined && value !== null);
|
|
935
957
|
const numbers = values.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
|
936
958
|
let value = undefined;
|
|
937
959
|
if (spec === DataGrid.AGGREGATE.COUNT) value = values.length;
|
|
938
|
-
else if (spec === DataGrid.AGGREGATE.SUM)
|
|
939
|
-
|
|
960
|
+
else if (spec === DataGrid.AGGREGATE.SUM)
|
|
961
|
+
value = DataGrid.#round(numbers.reduce((sum, item) => sum + item, 0));
|
|
962
|
+
else if (spec === DataGrid.AGGREGATE.AVG)
|
|
963
|
+
value =
|
|
964
|
+
numbers.length === 0
|
|
965
|
+
? undefined
|
|
966
|
+
: DataGrid.#round(numbers.reduce((sum, item) => sum + item, 0) / numbers.length);
|
|
940
967
|
else if (spec === DataGrid.AGGREGATE.MIN) value = numbers.length === 0 ? undefined : Math.min(...numbers);
|
|
941
968
|
else if (spec === DataGrid.AGGREGATE.MAX) value = numbers.length === 0 ? undefined : Math.max(...numbers);
|
|
942
|
-
const label = I18n.t(
|
|
969
|
+
const label = I18n.t(
|
|
970
|
+
DataGrid.AGGREGATE_LABEL[spec] === undefined ? "dataGrid.aggregate.sum" : DataGrid.AGGREGATE_LABEL[spec]
|
|
971
|
+
);
|
|
943
972
|
out[key] = {
|
|
944
973
|
text: value === undefined ? "" : String(value),
|
|
945
974
|
label: column === undefined ? key : column.title,
|
|
946
975
|
kind: label,
|
|
947
|
-
area: `${column === undefined ? key : column.title}: ${label} ${value === undefined ? "" : value}
|
|
976
|
+
area: `${column === undefined ? key : column.title}: ${label} ${value === undefined ? "" : value}`,
|
|
948
977
|
};
|
|
949
978
|
}
|
|
950
979
|
return out;
|
|
@@ -959,7 +988,7 @@ class DataGrid {
|
|
|
959
988
|
#syncColumns() {
|
|
960
989
|
const visible = this.#visibleColumns();
|
|
961
990
|
this.#root.style.setProperty("--data_grid_columns", visible.map((entry) => entry.column.width).join(" "));
|
|
962
|
-
this.#root.style.setProperty("--data_grid_row_height", `${this.#
|
|
991
|
+
this.#root.style.setProperty("--data_grid_row_height", `${this.#rowHeight}px`);
|
|
963
992
|
const explicit = DataGrid.#explicitWidth(visible.map((entry) => entry.column));
|
|
964
993
|
if (explicit > 0) this.#root.style.setProperty("--data_grid_min_width", `${explicit}px`);
|
|
965
994
|
else this.#root.style.removeProperty("--data_grid_min_width");
|
|
@@ -972,7 +1001,7 @@ class DataGrid {
|
|
|
972
1001
|
for (let index = 0; index < this.#columns.length; index++) {
|
|
973
1002
|
const column = this.#columns[index];
|
|
974
1003
|
if (this.#hidden.has(column.key)) continue;
|
|
975
|
-
out.push({column: column, index: index});
|
|
1004
|
+
out.push({ column: column, index: index });
|
|
976
1005
|
}
|
|
977
1006
|
return out;
|
|
978
1007
|
}
|
|
@@ -993,7 +1022,7 @@ class DataGrid {
|
|
|
993
1022
|
}
|
|
994
1023
|
}
|
|
995
1024
|
this.#scroll.classList.toggle("data_grid_h_scroll", this.#scroll.scrollWidth > this.#scroll.clientWidth + 1);
|
|
996
|
-
const nodes = [this.#
|
|
1025
|
+
const nodes = [this.#headRow, ...this.#renderedRows()];
|
|
997
1026
|
for (const row of nodes) {
|
|
998
1027
|
for (const cell of row.querySelectorAll("data_grid_cell[data-column]")) {
|
|
999
1028
|
const index = Number(cell.dataset.column);
|
|
@@ -1008,7 +1037,7 @@ class DataGrid {
|
|
|
1008
1037
|
}
|
|
1009
1038
|
|
|
1010
1039
|
#cellWidth(index = Number()) {
|
|
1011
|
-
const cell = this.#
|
|
1040
|
+
const cell = this.#headRow.querySelector(`data_grid_cell[data-column="${index}"]`);
|
|
1012
1041
|
return cell === null ? 0 : Math.round(cell.getBoundingClientRect().width);
|
|
1013
1042
|
}
|
|
1014
1043
|
|
|
@@ -1028,7 +1057,7 @@ class DataGrid {
|
|
|
1028
1057
|
|
|
1029
1058
|
// === ШАПКА ===
|
|
1030
1059
|
#buildHeader() {
|
|
1031
|
-
this.#
|
|
1060
|
+
this.#headRow.innerHTML = "";
|
|
1032
1061
|
for (const entry of this.#visibleColumns()) {
|
|
1033
1062
|
const column = entry.column;
|
|
1034
1063
|
const cell = document.createElement("data_grid_cell");
|
|
@@ -1041,16 +1070,21 @@ class DataGrid {
|
|
|
1041
1070
|
const sort = document.createElement("data_grid_sort");
|
|
1042
1071
|
sort.setAttribute("aria-hidden", "true");
|
|
1043
1072
|
cell.appendChild(sort);
|
|
1044
|
-
cell.setAttribute(
|
|
1073
|
+
cell.setAttribute(
|
|
1074
|
+
"aria-sort",
|
|
1075
|
+
this.#sort.key === column.key
|
|
1076
|
+
? DataGrid.SORT_ATTRIBUTE[this.#sort.direction === DataGrid.SORT.ASC ? "ASC" : "DESC"]
|
|
1077
|
+
: DataGrid.SORT_ATTRIBUTE.NONE
|
|
1078
|
+
);
|
|
1045
1079
|
cell.setAttribute("tabindex", "0");
|
|
1046
1080
|
// Подпись заголовка объясняет, что Enter и Space сортируют колонку
|
|
1047
|
-
cell.setAttribute("aria-label", I18n.t("table.sortBy", {head: column.title}));
|
|
1081
|
+
cell.setAttribute("aria-label", I18n.t("table.sortBy", { head: column.title }));
|
|
1048
1082
|
}
|
|
1049
|
-
if (this.#
|
|
1083
|
+
if (this.#columnReorder) {
|
|
1050
1084
|
cell.setAttribute("draggable", "true");
|
|
1051
1085
|
if (column.sortable === false) cell.setAttribute("tabindex", "0");
|
|
1052
1086
|
}
|
|
1053
|
-
this.#
|
|
1087
|
+
this.#headRow.appendChild(cell);
|
|
1054
1088
|
this.#syncSortMark(cell, entry.index);
|
|
1055
1089
|
}
|
|
1056
1090
|
}
|
|
@@ -1061,20 +1095,24 @@ class DataGrid {
|
|
|
1061
1095
|
const sort = cell.querySelector("data_grid_sort");
|
|
1062
1096
|
if (sort === null) return;
|
|
1063
1097
|
const active = this.#sort.key === column.key && this.#sort.direction !== DataGrid.SORT.NONE;
|
|
1064
|
-
cell.setAttribute(
|
|
1065
|
-
|
|
1066
|
-
|
|
1098
|
+
cell.setAttribute(
|
|
1099
|
+
"aria-sort",
|
|
1100
|
+
active
|
|
1101
|
+
? DataGrid.SORT_ATTRIBUTE[this.#sort.direction === DataGrid.SORT.ASC ? "ASC" : "DESC"]
|
|
1102
|
+
: DataGrid.SORT_ATTRIBUTE.NONE
|
|
1103
|
+
);
|
|
1067
1104
|
if (active === false) {
|
|
1068
1105
|
sort.innerHTML = "";
|
|
1069
1106
|
return;
|
|
1070
1107
|
}
|
|
1071
|
-
const icon =
|
|
1108
|
+
const icon =
|
|
1109
|
+
this.#sort.direction === DataGrid.SORT.ASC ? Icons.NAVIGATION.EXPAND_LESS : Icons.NAVIGATION.EXPAND_MORE;
|
|
1072
1110
|
// Значок состояния — акцент, как подпись отсортированной колонки
|
|
1073
1111
|
sort.innerHTML = new Icon(icon, "16px", "var(--blue_prime_background)").getHTML();
|
|
1074
1112
|
}
|
|
1075
1113
|
|
|
1076
1114
|
#onHeaderClick = (event) => {
|
|
1077
|
-
const cell = DataGrid.#columnFromEvent(event, this.#
|
|
1115
|
+
const cell = DataGrid.#columnFromEvent(event, this.#headRow);
|
|
1078
1116
|
if (cell === undefined) return;
|
|
1079
1117
|
const index = Number(cell.dataset.column);
|
|
1080
1118
|
const column = this.#columns[index];
|
|
@@ -1083,7 +1121,7 @@ class DataGrid {
|
|
|
1083
1121
|
};
|
|
1084
1122
|
|
|
1085
1123
|
#onHeaderKeyDown = (event) => {
|
|
1086
|
-
const cell = DataGrid.#columnFromEvent(event, this.#
|
|
1124
|
+
const cell = DataGrid.#columnFromEvent(event, this.#headRow);
|
|
1087
1125
|
if (cell === undefined) return;
|
|
1088
1126
|
const index = Number(cell.dataset.column);
|
|
1089
1127
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -1092,7 +1130,7 @@ class DataGrid {
|
|
|
1092
1130
|
return;
|
|
1093
1131
|
}
|
|
1094
1132
|
if (event.altKey && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
|
|
1095
|
-
if (this.#
|
|
1133
|
+
if (this.#columnReorder === false) return;
|
|
1096
1134
|
event.preventDefault();
|
|
1097
1135
|
this.#moveColumn(index, event.key === "ArrowLeft" ? -1 : 1);
|
|
1098
1136
|
return;
|
|
@@ -1104,7 +1142,7 @@ class DataGrid {
|
|
|
1104
1142
|
const place = visible.findIndex((entry) => entry.index === index);
|
|
1105
1143
|
const next = visible[place + (event.key === "ArrowRight" ? 1 : -1)];
|
|
1106
1144
|
if (next === undefined) return;
|
|
1107
|
-
const node = this.#
|
|
1145
|
+
const node = this.#headRow.querySelector(`data_grid_cell[data-column="${next.index}"]`);
|
|
1108
1146
|
if (node !== null) node.focus();
|
|
1109
1147
|
}
|
|
1110
1148
|
};
|
|
@@ -1114,15 +1152,16 @@ class DataGrid {
|
|
|
1114
1152
|
const column = this.#columns[index];
|
|
1115
1153
|
if (column === undefined) return;
|
|
1116
1154
|
const same = this.#sort.key === column.key;
|
|
1117
|
-
if (same === false) this.#sort = {key: column.key, direction: DataGrid.SORT.ASC};
|
|
1118
|
-
else if (this.#sort.direction === DataGrid.SORT.ASC)
|
|
1119
|
-
|
|
1155
|
+
if (same === false) this.#sort = { key: column.key, direction: DataGrid.SORT.ASC };
|
|
1156
|
+
else if (this.#sort.direction === DataGrid.SORT.ASC)
|
|
1157
|
+
this.#sort = { key: column.key, direction: DataGrid.SORT.DESC };
|
|
1158
|
+
else this.#sort = { key: undefined, direction: DataGrid.SORT.NONE };
|
|
1120
1159
|
this.#applySort();
|
|
1121
1160
|
}
|
|
1122
1161
|
|
|
1123
1162
|
#applySort() {
|
|
1124
1163
|
this.#recompute();
|
|
1125
|
-
for (const cell of this.#
|
|
1164
|
+
for (const cell of this.#headRow.querySelectorAll("data_grid_cell[data-column]")) {
|
|
1126
1165
|
this.#syncSortMark(cell, Number(cell.dataset.column));
|
|
1127
1166
|
}
|
|
1128
1167
|
this.#announce(this.#sortSummary());
|
|
@@ -1133,7 +1172,7 @@ class DataGrid {
|
|
|
1133
1172
|
const column = this.#columns.find((item) => item.key === this.#sort.key);
|
|
1134
1173
|
return I18n.t("dataGrid.sortSummary", {
|
|
1135
1174
|
title: column === undefined ? this.#sort.key : column.title,
|
|
1136
|
-
direction: I18n.t(this.#sort.direction === DataGrid.SORT.DESC ? "data.descending" : "data.ascending")
|
|
1175
|
+
direction: I18n.t(this.#sort.direction === DataGrid.SORT.DESC ? "data.descending" : "data.ascending"),
|
|
1137
1176
|
});
|
|
1138
1177
|
}
|
|
1139
1178
|
|
|
@@ -1163,9 +1202,10 @@ class DataGrid {
|
|
|
1163
1202
|
// Свой класс строки ставим ДО класса выделения: запись в `className` затирает уже
|
|
1164
1203
|
// проставленные классы, и после перерисовки строки (правка ячейки) выделенная
|
|
1165
1204
|
// строка теряла бы подсветку при включённом `rowClass`
|
|
1166
|
-
if (typeof this.#
|
|
1167
|
-
const classes = this.#
|
|
1168
|
-
if (classes !== undefined && classes !== null && String(classes).trim() !== "")
|
|
1205
|
+
if (typeof this.#rowClass === "function") {
|
|
1206
|
+
const classes = this.#rowClass(row, entry.rowIndex);
|
|
1207
|
+
if (classes !== undefined && classes !== null && String(classes).trim() !== "")
|
|
1208
|
+
node.className = String(classes).trim();
|
|
1169
1209
|
}
|
|
1170
1210
|
node.classList.toggle("data_grid_row_selected", this.#selected.has(key));
|
|
1171
1211
|
if (this.#selection === DataGrid.SELECTION.NONE) node.removeAttribute("aria-selected");
|
|
@@ -1177,7 +1217,8 @@ class DataGrid {
|
|
|
1177
1217
|
cell.setAttribute("aria-colindex", String(visible.index + 1));
|
|
1178
1218
|
cell.dataset.column = String(visible.index);
|
|
1179
1219
|
DataGrid.#applyAlign(cell, visible.column.align);
|
|
1180
|
-
if (this.#editable !== undefined)
|
|
1220
|
+
if (this.#editable !== undefined)
|
|
1221
|
+
cell.setAttribute("aria-readonly", this.#isColumnEditable(visible.index) ? "false" : "true");
|
|
1181
1222
|
if (editing && this.#editing.col === visible.index && this.#editing.node !== undefined) {
|
|
1182
1223
|
cell.classList.add("data_grid_cell_editing");
|
|
1183
1224
|
// У редактора-списка свой отступ: его рамку рисует сам компонент
|
|
@@ -1196,8 +1237,12 @@ class DataGrid {
|
|
|
1196
1237
|
if (typeof column.render === "function") {
|
|
1197
1238
|
const content = column.render(row, index);
|
|
1198
1239
|
if (content instanceof Node) cell.appendChild(content);
|
|
1199
|
-
else if (content !== undefined && content !== null && typeof content.set === "function")
|
|
1200
|
-
|
|
1240
|
+
else if (content !== undefined && content !== null && typeof content.set === "function")
|
|
1241
|
+
cell.appendChild(content.set());
|
|
1242
|
+
else
|
|
1243
|
+
cell.appendChild(
|
|
1244
|
+
document.createTextNode(content === undefined || content === null ? "" : String(content))
|
|
1245
|
+
);
|
|
1201
1246
|
return;
|
|
1202
1247
|
}
|
|
1203
1248
|
const value = row[column.key];
|
|
@@ -1223,15 +1268,20 @@ class DataGrid {
|
|
|
1223
1268
|
cell.dataset.column = String(first.index);
|
|
1224
1269
|
cell.tabIndex = 0;
|
|
1225
1270
|
const toggle = document.createElement("data_grid_group_toggle");
|
|
1226
|
-
toggle.innerHTML = new Icon(
|
|
1271
|
+
toggle.innerHTML = new Icon(
|
|
1272
|
+
entry.group.collapsed ? Icons.NAVIGATION.CHEVRON_RIGHT : Icons.NAVIGATION.EXPAND_MORE,
|
|
1273
|
+
"14px",
|
|
1274
|
+
"var(--text_color_disabled)"
|
|
1275
|
+
).getHTML();
|
|
1227
1276
|
cell.appendChild(toggle);
|
|
1228
1277
|
const title = document.createElement("data_grid_group_title");
|
|
1229
1278
|
// Подпись группы — вся целиком: и ключ, и число строк. Отдельного счётчика рядом нет,
|
|
1230
1279
|
// иначе он повторял бы то же число (прежде там стояла подпись «групп: N», которая
|
|
1231
1280
|
// называла строки группы группами)
|
|
1232
|
-
title.innerText =
|
|
1233
|
-
|
|
1234
|
-
|
|
1281
|
+
title.innerText =
|
|
1282
|
+
typeof this.#groupLabel === "function"
|
|
1283
|
+
? String(this.#groupLabel(entry.group.key, entry.group.rows))
|
|
1284
|
+
: I18n.t("dataGrid.groupTitle", { key: entry.group.key, count: entry.group.rows.length });
|
|
1235
1285
|
cell.appendChild(title);
|
|
1236
1286
|
node.appendChild(cell);
|
|
1237
1287
|
for (let place = 1; place < visible.length; place++) {
|
|
@@ -1262,9 +1312,9 @@ class DataGrid {
|
|
|
1262
1312
|
#syncRowActive(node = new HTMLElement(), index = Number()) {
|
|
1263
1313
|
const active = this.#active.row === index;
|
|
1264
1314
|
for (const cell of node.querySelectorAll("data_grid_cell[data-column]")) {
|
|
1265
|
-
const
|
|
1266
|
-
cell.classList.toggle(A11Y_ACTIVE_CLASS,
|
|
1267
|
-
cell.tabIndex =
|
|
1315
|
+
const isActive = active && Number(cell.dataset.column) === this.#active.col;
|
|
1316
|
+
cell.classList.toggle(A11Y_ACTIVE_CLASS, isActive);
|
|
1317
|
+
cell.tabIndex = isActive ? 0 : -1;
|
|
1268
1318
|
}
|
|
1269
1319
|
}
|
|
1270
1320
|
|
|
@@ -1274,14 +1324,14 @@ class DataGrid {
|
|
|
1274
1324
|
|
|
1275
1325
|
#rowsSummary() {
|
|
1276
1326
|
const shown = this.#sorted.length;
|
|
1277
|
-
if (this.#groups.length === 0) return I18n.t("dataGrid.rowsSummary", {shown: shown});
|
|
1278
|
-
return `${I18n.t("dataGrid.rowsSummary", {shown: shown})}, ${I18n.t("dataGrid.groupsSummary", {count: this.#groups.length})}`;
|
|
1327
|
+
if (this.#groups.length === 0) return I18n.t("dataGrid.rowsSummary", { shown: shown });
|
|
1328
|
+
return `${I18n.t("dataGrid.rowsSummary", { shown: shown })}, ${I18n.t("dataGrid.groupsSummary", { count: this.#groups.length })}`;
|
|
1279
1329
|
}
|
|
1280
1330
|
// === ВЫБОР СТРОК ВНУТРЕННЕЕ ===
|
|
1281
1331
|
/** Ключ строки: имя поля, функция или место в исходных данных */
|
|
1282
1332
|
#keyOf(entry = new DataGridEntry()) {
|
|
1283
|
-
if (typeof this.#
|
|
1284
|
-
if (this.#
|
|
1333
|
+
if (typeof this.#customKey === "function") return this.#customKey(entry.row);
|
|
1334
|
+
if (this.#customKey !== undefined) return entry.row[this.#customKey];
|
|
1285
1335
|
return this.#rowIndexOf(entry.row);
|
|
1286
1336
|
}
|
|
1287
1337
|
|
|
@@ -1312,7 +1362,8 @@ class DataGrid {
|
|
|
1312
1362
|
if (from === -1 || until === -1) return this.#selectRow(to);
|
|
1313
1363
|
const [start, end] = from <= until ? [from, until] : [until, from];
|
|
1314
1364
|
this.#selected.clear();
|
|
1315
|
-
for (let index = start; index <= end; index++)
|
|
1365
|
+
for (let index = start; index <= end; index++)
|
|
1366
|
+
this.#selected.add(this.#keyOf(DataGrid.#entryOf(rows[index], this.#rowIndexOf(rows[index]))));
|
|
1316
1367
|
this.#applySelection();
|
|
1317
1368
|
}
|
|
1318
1369
|
|
|
@@ -1322,33 +1373,36 @@ class DataGrid {
|
|
|
1322
1373
|
if (entry === undefined || entry.kind !== "row") continue;
|
|
1323
1374
|
const selected = this.#selected.has(this.#keyOf(entry));
|
|
1324
1375
|
node.classList.toggle("data_grid_row_selected", selected);
|
|
1325
|
-
if (this.#selection !== DataGrid.SELECTION.NONE)
|
|
1376
|
+
if (this.#selection !== DataGrid.SELECTION.NONE)
|
|
1377
|
+
node.setAttribute("aria-selected", selected ? "true" : "false");
|
|
1326
1378
|
}
|
|
1327
1379
|
this.#syncPinned();
|
|
1328
|
-
if (this.#
|
|
1329
|
-
const rows = this.#viewRows().filter((row) =>
|
|
1330
|
-
|
|
1380
|
+
if (this.#onSelectionChange !== undefined) {
|
|
1381
|
+
const rows = this.#viewRows().filter((row) =>
|
|
1382
|
+
this.#selected.has(this.#keyOf(DataGrid.#entryOf(row, this.#rowIndexOf(row))))
|
|
1383
|
+
);
|
|
1384
|
+
this.#onSelectionChange(rows, Array.from(this.#selected));
|
|
1331
1385
|
}
|
|
1332
|
-
this.#announce(I18n.t("dataGrid.rowsSummary", {shown: this.#selected.size}));
|
|
1386
|
+
this.#announce(I18n.t("dataGrid.rowsSummary", { shown: this.#selected.size }));
|
|
1333
1387
|
}
|
|
1334
1388
|
|
|
1335
1389
|
// === АКТИВНАЯ ЯЧЕЙКА И КЛАВИАТУРА ===
|
|
1336
1390
|
#setActive(row = Number(), col = Number(), focus = true) {
|
|
1337
|
-
if (this.#
|
|
1338
|
-
const place = Math.min(Math.max(row, 0), this.#
|
|
1391
|
+
if (this.#entryCount === 0) return;
|
|
1392
|
+
const place = Math.min(Math.max(row, 0), this.#entryCount - 1);
|
|
1339
1393
|
const entry = this.#entryAt(place);
|
|
1340
1394
|
const visible = this.#visibleColumns();
|
|
1341
1395
|
if (visible.length === 0) return;
|
|
1342
1396
|
const known = visible.some((item) => item.index === col) ? col : visible[0].index;
|
|
1343
|
-
this.#active = {row: place, col: known};
|
|
1397
|
+
this.#active = { row: place, col: known };
|
|
1344
1398
|
if (entry !== undefined && entry.kind === "row") {
|
|
1345
1399
|
this.#list.scrollToIndex(place, DataGrid.ALIGN.NEAREST);
|
|
1346
1400
|
}
|
|
1347
1401
|
this.#syncActiveCells();
|
|
1348
1402
|
const node = this.#cellNode(place, known);
|
|
1349
|
-
if (node !== undefined && focus) node.focus({preventScroll: true});
|
|
1350
|
-
if (this.#
|
|
1351
|
-
this.#
|
|
1403
|
+
if (node !== undefined && focus) node.focus({ preventScroll: true });
|
|
1404
|
+
if (this.#onActiveCellChange !== undefined && entry !== undefined && entry.kind === "row") {
|
|
1405
|
+
this.#onActiveCellChange(place, known, entry.row);
|
|
1352
1406
|
}
|
|
1353
1407
|
}
|
|
1354
1408
|
|
|
@@ -1365,23 +1419,24 @@ class DataGrid {
|
|
|
1365
1419
|
const place = visible.findIndex((item) => item.index === col);
|
|
1366
1420
|
if (place === -1) return undefined;
|
|
1367
1421
|
let next = place + step;
|
|
1368
|
-
let
|
|
1422
|
+
let nextRow = row;
|
|
1369
1423
|
if (next >= visible.length) {
|
|
1370
1424
|
next = 0;
|
|
1371
|
-
|
|
1425
|
+
nextRow = row + 1;
|
|
1372
1426
|
}
|
|
1373
1427
|
if (next < 0) {
|
|
1374
1428
|
next = visible.length - 1;
|
|
1375
|
-
|
|
1429
|
+
nextRow = row - 1;
|
|
1376
1430
|
}
|
|
1377
|
-
if (
|
|
1378
|
-
const entry = this.#entryAt(
|
|
1431
|
+
if (nextRow < 0 || nextRow >= this.#entryCount) return undefined;
|
|
1432
|
+
const entry = this.#entryAt(nextRow);
|
|
1379
1433
|
if (entry === undefined || entry.kind !== "row") return undefined;
|
|
1380
|
-
return {row:
|
|
1434
|
+
return { row: nextRow, col: visible[next].index };
|
|
1381
1435
|
}
|
|
1382
1436
|
|
|
1383
1437
|
#onRowClick = (entry = new DataGridEntry(), index = Number(), _node = new HTMLElement(), event = Object()) => {
|
|
1384
|
-
const cell =
|
|
1438
|
+
const cell =
|
|
1439
|
+
event === undefined || event.target === undefined ? undefined : event.target.closest("data_grid_cell");
|
|
1385
1440
|
const col = cell === undefined || cell === null ? this.#active.col : Number(cell.dataset.column);
|
|
1386
1441
|
if (entry.kind === "group") {
|
|
1387
1442
|
this.#setGroupCollapsed(entry.group.key, entry.group.collapsed === false);
|
|
@@ -1413,13 +1468,13 @@ class DataGrid {
|
|
|
1413
1468
|
// Space добавлял бы в выборку несуществующую строку, а перестановка колонки
|
|
1414
1469
|
// (Alt+←/→) пересобирает шапку и ячейка под уже всплывающим событием осиротеет —
|
|
1415
1470
|
// `closest` вернул бы null и обработчик падал бы с TypeError
|
|
1416
|
-
const
|
|
1417
|
-
if (
|
|
1418
|
-
const row = Number(
|
|
1471
|
+
const rowNode = cell.closest("data_grid_row");
|
|
1472
|
+
if (rowNode === null || rowNode.dataset.index === undefined) return;
|
|
1473
|
+
const row = Number(rowNode.dataset.index);
|
|
1419
1474
|
const col = Number(cell.dataset.column);
|
|
1420
1475
|
const visible = this.#visibleColumns();
|
|
1421
1476
|
const place = visible.findIndex((item) => item.index === col);
|
|
1422
|
-
const last = this.#
|
|
1477
|
+
const last = this.#entryCount - 1;
|
|
1423
1478
|
const entry = this.#entryAt(row);
|
|
1424
1479
|
if (entry !== undefined && entry.kind === "group") {
|
|
1425
1480
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -1440,9 +1495,9 @@ class DataGrid {
|
|
|
1440
1495
|
return;
|
|
1441
1496
|
}
|
|
1442
1497
|
}
|
|
1443
|
-
const move = (
|
|
1498
|
+
const move = (nextRow, nextColumn) => {
|
|
1444
1499
|
event.preventDefault();
|
|
1445
|
-
this.#setActive(
|
|
1500
|
+
this.#setActive(nextRow, nextColumn === undefined ? col : nextColumn, true);
|
|
1446
1501
|
};
|
|
1447
1502
|
if (event.key === "ArrowDown") return move(Math.min(row + 1, last));
|
|
1448
1503
|
if (event.key === "ArrowUp") return move(Math.max(row - 1, 0));
|
|
@@ -1482,7 +1537,7 @@ class DataGrid {
|
|
|
1482
1537
|
#activateRow(index = Number()) {
|
|
1483
1538
|
const entry = this.#entryAt(index);
|
|
1484
1539
|
if (entry === undefined || entry.kind !== "row") return;
|
|
1485
|
-
if (this.#
|
|
1540
|
+
if (this.#onRowActivate !== undefined) this.#onRowActivate(entry.row, entry.rowIndex);
|
|
1486
1541
|
}
|
|
1487
1542
|
|
|
1488
1543
|
// === ПРАВКА ЯЧЕЕК ===
|
|
@@ -1517,7 +1572,7 @@ class DataGrid {
|
|
|
1517
1572
|
old_value: entry.row[column.key],
|
|
1518
1573
|
forced: undefined,
|
|
1519
1574
|
component: undefined,
|
|
1520
|
-
node: undefined
|
|
1575
|
+
node: undefined,
|
|
1521
1576
|
};
|
|
1522
1577
|
const node = this.#createEditor(column, entry.row, row, editing);
|
|
1523
1578
|
if (node === undefined) return false;
|
|
@@ -1528,7 +1583,7 @@ class DataGrid {
|
|
|
1528
1583
|
if (editing.component !== undefined) editing.component.focus();
|
|
1529
1584
|
else node.focus();
|
|
1530
1585
|
if (typeof node.select === "function" && editing.component === undefined) node.select();
|
|
1531
|
-
this.#announce(I18n.t("dataGrid.editStart", {title: column.title, row: row + 1}));
|
|
1586
|
+
this.#announce(I18n.t("dataGrid.editStart", { title: column.title, row: row + 1 }));
|
|
1532
1587
|
return true;
|
|
1533
1588
|
}
|
|
1534
1589
|
|
|
@@ -1540,7 +1595,7 @@ class DataGrid {
|
|
|
1540
1595
|
if (this.#editing === editing && value !== undefined) editing.forced = value;
|
|
1541
1596
|
this.#finishEdit(true, true, true);
|
|
1542
1597
|
},
|
|
1543
|
-
cancel: () => this.#finishEdit(false, false, true)
|
|
1598
|
+
cancel: () => this.#finishEdit(false, false, true),
|
|
1544
1599
|
};
|
|
1545
1600
|
const custom = column.editor(row, index, api);
|
|
1546
1601
|
if (custom === undefined || custom === null) return undefined;
|
|
@@ -1630,23 +1685,25 @@ class DataGrid {
|
|
|
1630
1685
|
}
|
|
1631
1686
|
|
|
1632
1687
|
#readEditorValue(column = new DataGridColumn(), editing = Object()) {
|
|
1633
|
-
if (editing.forced !== undefined) return {ok: true, value: editing.forced};
|
|
1688
|
+
if (editing.forced !== undefined) return { ok: true, value: editing.forced };
|
|
1634
1689
|
const editor = editing.node;
|
|
1635
|
-
if (column.editType === DataGrid.EDIT_TYPE.CHECKBOX) return {ok: true, value: editor.checked === true};
|
|
1690
|
+
if (column.editType === DataGrid.EDIT_TYPE.CHECKBOX) return { ok: true, value: editor.checked === true };
|
|
1636
1691
|
if (column.editType === DataGrid.EDIT_TYPE.SELECT) {
|
|
1637
1692
|
// Значение списка отдаёт компонент: у него подпись и значение живут отдельно
|
|
1638
|
-
if (editing.component !== undefined) return {ok: true, value: editing.component.getSelectedValue()};
|
|
1639
|
-
return {ok: true, value: editor.value};
|
|
1693
|
+
if (editing.component !== undefined) return { ok: true, value: editing.component.getSelectedValue() };
|
|
1694
|
+
return { ok: true, value: editor.value };
|
|
1640
1695
|
}
|
|
1641
1696
|
const text = editor.value;
|
|
1642
1697
|
if (column.editType === DataGrid.EDIT_TYPE.NUMBER) {
|
|
1643
1698
|
const normalized = String(text).trim().replace(",", ".");
|
|
1644
|
-
if (normalized === "")
|
|
1699
|
+
if (normalized === "")
|
|
1700
|
+
return { ok: false, message: I18n.t("dataGrid.enterNumber", { title: column.title }) };
|
|
1645
1701
|
const number = Number(normalized);
|
|
1646
|
-
if (Number.isNaN(number))
|
|
1647
|
-
|
|
1702
|
+
if (Number.isNaN(number))
|
|
1703
|
+
return { ok: false, message: I18n.t("dataGrid.notNumber", { title: column.title, text: text }) };
|
|
1704
|
+
return { ok: true, value: number };
|
|
1648
1705
|
}
|
|
1649
|
-
return {ok: true, value: text};
|
|
1706
|
+
return { ok: true, value: text };
|
|
1650
1707
|
}
|
|
1651
1708
|
|
|
1652
1709
|
/**
|
|
@@ -1658,7 +1715,7 @@ class DataGrid {
|
|
|
1658
1715
|
if (editing === undefined) return false;
|
|
1659
1716
|
const column = this.#columns[editing.col];
|
|
1660
1717
|
const entry = this.#entryAt(editing.row);
|
|
1661
|
-
const parsed = save ? this.#readEditorValue(column, editing) : {ok: true, value: editing.old_value};
|
|
1718
|
+
const parsed = save ? this.#readEditorValue(column, editing) : { ok: true, value: editing.old_value };
|
|
1662
1719
|
if (parsed.ok === false && keepInvalid) {
|
|
1663
1720
|
// Неверный ввод объясняем и оставляем редактор: начать другую правку нельзя
|
|
1664
1721
|
this.#announce(parsed.message);
|
|
@@ -1666,21 +1723,22 @@ class DataGrid {
|
|
|
1666
1723
|
}
|
|
1667
1724
|
const value = parsed.ok === false ? editing.old_value : parsed.value;
|
|
1668
1725
|
this.#editing = undefined;
|
|
1669
|
-
if (editing.component !== undefined && typeof editing.component.destroy === "function")
|
|
1726
|
+
if (editing.component !== undefined && typeof editing.component.destroy === "function")
|
|
1727
|
+
editing.component.destroy();
|
|
1670
1728
|
if (entry !== undefined && entry.kind === "row" && save && value !== editing.old_value) {
|
|
1671
1729
|
entry.row[editing.key] = value;
|
|
1672
|
-
if (this.#
|
|
1673
|
-
this.#announce(I18n.t("dataGrid.saved", {title: column.title, value: DataGrid.#valueText(value)}));
|
|
1730
|
+
if (this.#onCellEdit !== undefined) this.#onCellEdit(entry.row, editing.key, value, editing.old_value);
|
|
1731
|
+
this.#announce(I18n.t("dataGrid.saved", { title: column.title, value: DataGrid.#valueText(value) }));
|
|
1674
1732
|
} else if (save === false && parsed.ok === false) {
|
|
1675
|
-
this.#announce(I18n.t("dataGrid.editCanceledReason", {message: parsed.message}));
|
|
1733
|
+
this.#announce(I18n.t("dataGrid.editCanceledReason", { message: parsed.message }));
|
|
1676
1734
|
} else if (save === false) {
|
|
1677
|
-
this.#announce(I18n.t("dataGrid.editCanceled", {title: column.title}));
|
|
1735
|
+
this.#announce(I18n.t("dataGrid.editCanceled", { title: column.title }));
|
|
1678
1736
|
}
|
|
1679
1737
|
// Агрегаты группы считаются по строкам — после правки их надо пересчитать
|
|
1680
1738
|
this.#recompute();
|
|
1681
1739
|
if (focus) {
|
|
1682
1740
|
const node = this.#cellNode(editing.row, editing.col);
|
|
1683
|
-
if (node !== undefined) node.focus({preventScroll: true});
|
|
1741
|
+
if (node !== undefined) node.focus({ preventScroll: true });
|
|
1684
1742
|
}
|
|
1685
1743
|
return true;
|
|
1686
1744
|
}
|
|
@@ -1701,7 +1759,9 @@ class DataGrid {
|
|
|
1701
1759
|
#setGroupCollapsed(key = String(), collapsed = true) {
|
|
1702
1760
|
this.#collapsed.set(String(key), collapsed === true);
|
|
1703
1761
|
this.#recompute();
|
|
1704
|
-
this.#announce(
|
|
1762
|
+
this.#announce(
|
|
1763
|
+
collapsed === true ? I18n.t("dataGrid.groupsSummary", { count: this.#groups.length }) : this.#rowsSummary()
|
|
1764
|
+
);
|
|
1705
1765
|
}
|
|
1706
1766
|
|
|
1707
1767
|
// === ЗАКРЕПЛЁННЫЕ КОЛОНКИ И ГОРИЗОНТАЛЬНАЯ ПРОКРУТКА ===
|
|
@@ -1711,8 +1771,8 @@ class DataGrid {
|
|
|
1711
1771
|
|
|
1712
1772
|
// === ПЕРЕСТАНОВКА КОЛОНОК ===
|
|
1713
1773
|
#onHeaderDragStart = (event) => {
|
|
1714
|
-
if (this.#
|
|
1715
|
-
const cell = DataGrid.#columnFromEvent(event, this.#
|
|
1774
|
+
if (this.#columnReorder === false) return;
|
|
1775
|
+
const cell = DataGrid.#columnFromEvent(event, this.#headRow);
|
|
1716
1776
|
if (cell === undefined) return;
|
|
1717
1777
|
this.#drag = Number(cell.dataset.column);
|
|
1718
1778
|
cell.classList.add("data_grid_cell_dragging");
|
|
@@ -1720,7 +1780,7 @@ class DataGrid {
|
|
|
1720
1780
|
event.dataTransfer.effectAllowed = "move";
|
|
1721
1781
|
event.dataTransfer.setData("text/plain", String(this.#drag));
|
|
1722
1782
|
}
|
|
1723
|
-
this.#announce(I18n.t("dataGrid.columnMove", {title: this.#columns[this.#drag].title}));
|
|
1783
|
+
this.#announce(I18n.t("dataGrid.columnMove", { title: this.#columns[this.#drag].title }));
|
|
1724
1784
|
};
|
|
1725
1785
|
|
|
1726
1786
|
#onHeaderDragOver = (event) => {
|
|
@@ -1743,13 +1803,14 @@ class DataGrid {
|
|
|
1743
1803
|
|
|
1744
1804
|
#onHeaderDragEnd = () => {
|
|
1745
1805
|
this.#markDrop(undefined);
|
|
1746
|
-
for (const cell of this.#
|
|
1806
|
+
for (const cell of this.#headRow.querySelectorAll("data_grid_cell"))
|
|
1807
|
+
cell.classList.remove("data_grid_cell_dragging");
|
|
1747
1808
|
this.#drag = undefined;
|
|
1748
1809
|
};
|
|
1749
1810
|
|
|
1750
1811
|
/** Место вставки по координате курсора: середина каждой видимой колонки */
|
|
1751
1812
|
#dropSlot(clientX = Number()) {
|
|
1752
|
-
const cells = Array.from(this.#
|
|
1813
|
+
const cells = Array.from(this.#headRow.querySelectorAll("data_grid_cell[data-column]"));
|
|
1753
1814
|
for (let place = 0; place < cells.length; place++) {
|
|
1754
1815
|
const rect = cells[place].getBoundingClientRect();
|
|
1755
1816
|
if (clientX < rect.left + rect.width / 2) return place;
|
|
@@ -1758,9 +1819,10 @@ class DataGrid {
|
|
|
1758
1819
|
}
|
|
1759
1820
|
|
|
1760
1821
|
#markDrop(slot = undefined) {
|
|
1761
|
-
for (const cell of this.#
|
|
1822
|
+
for (const cell of this.#headRow.querySelectorAll("data_grid_cell"))
|
|
1823
|
+
cell.classList.remove("data_grid_cell_drop_after");
|
|
1762
1824
|
if (slot === undefined) return;
|
|
1763
|
-
const cells = Array.from(this.#
|
|
1825
|
+
const cells = Array.from(this.#headRow.querySelectorAll("data_grid_cell[data-column]"));
|
|
1764
1826
|
const target = cells[slot === 0 ? 0 : slot - 1];
|
|
1765
1827
|
if (target !== undefined) target.classList.add("data_grid_cell_drop_after");
|
|
1766
1828
|
}
|
|
@@ -1776,14 +1838,15 @@ class DataGrid {
|
|
|
1776
1838
|
const target = slot >= visible.length ? this.#columns.length : visible[slot].index;
|
|
1777
1839
|
const column = this.#columns[from];
|
|
1778
1840
|
const rest = this.#columns.filter((item) => item !== column);
|
|
1779
|
-
const place =
|
|
1841
|
+
const place =
|
|
1842
|
+
slot >= visible.length ? rest.length : rest.findIndex((item) => item.key === visible[slot].column.key);
|
|
1780
1843
|
rest.splice(place < 0 ? rest.length : place, 0, column);
|
|
1781
1844
|
this.#columns = rest;
|
|
1782
1845
|
this.#onHeaderDragEnd();
|
|
1783
1846
|
this.#recompute();
|
|
1784
1847
|
this.#buildHeader();
|
|
1785
|
-
if (this.#
|
|
1786
|
-
this.#
|
|
1848
|
+
if (this.#onColumnOrderChange !== undefined) {
|
|
1849
|
+
this.#onColumnOrderChange(this.getColumnOrder(), column.key, from, target);
|
|
1787
1850
|
}
|
|
1788
1851
|
}
|
|
1789
1852
|
|
|
@@ -1801,10 +1864,13 @@ class DataGrid {
|
|
|
1801
1864
|
this.#buildHeader();
|
|
1802
1865
|
const from = index;
|
|
1803
1866
|
const to = this.#columns.indexOf(column);
|
|
1804
|
-
const node = this.#
|
|
1867
|
+
const node = this.#headRow.querySelector(`data_grid_cell[data-column="${to}"]`);
|
|
1805
1868
|
if (node !== null) node.focus();
|
|
1806
|
-
this.#announce(
|
|
1807
|
-
|
|
1869
|
+
this.#announce(
|
|
1870
|
+
I18n.t("dataGrid.columnPlace", { title: column.title, place: to + 1, total: this.#columns.length })
|
|
1871
|
+
);
|
|
1872
|
+
if (this.#onColumnOrderChange !== undefined)
|
|
1873
|
+
this.#onColumnOrderChange(this.getColumnOrder(), column.key, from, to);
|
|
1808
1874
|
}
|
|
1809
1875
|
|
|
1810
1876
|
// === ПОЛОСА НАД СЕТКОЙ И ДИАЛОГ КОЛОНОК ===
|
|
@@ -1812,7 +1878,7 @@ class DataGrid {
|
|
|
1812
1878
|
const button = new Button({
|
|
1813
1879
|
title: I18n.t("dataGrid.columns"),
|
|
1814
1880
|
icon: Icons.ACTIONS.VIEW_COLUMN,
|
|
1815
|
-
clickEvent: () => this.openColumnChooser()
|
|
1881
|
+
clickEvent: () => this.openColumnChooser(),
|
|
1816
1882
|
});
|
|
1817
1883
|
const element = button.set().querySelector("button");
|
|
1818
1884
|
if (element !== null) element.setAttribute("aria-haspopup", "dialog");
|
|
@@ -1821,33 +1887,38 @@ class DataGrid {
|
|
|
1821
1887
|
}
|
|
1822
1888
|
|
|
1823
1889
|
#ensureChooser() {
|
|
1824
|
-
if (this.#
|
|
1890
|
+
if (this.#chooserDialog !== undefined) {
|
|
1825
1891
|
this.#refreshChooser();
|
|
1826
|
-
return this.#
|
|
1892
|
+
return this.#chooserDialog;
|
|
1827
1893
|
}
|
|
1828
|
-
const dialog = new Dialog({
|
|
1829
|
-
|
|
1894
|
+
const dialog = new Dialog({
|
|
1895
|
+
width: "340px",
|
|
1896
|
+
height: "max-content",
|
|
1897
|
+
closeOutSideClick: true,
|
|
1898
|
+
title: I18n.t("dataGrid.columns"),
|
|
1899
|
+
});
|
|
1900
|
+
const block = new ContentBlock({ direction: "column", wrap: "nowrap", width: "-webkit-fill-available" });
|
|
1830
1901
|
dialog.addToBody(block);
|
|
1831
|
-
dialog.addToFooter(new Button({title: I18n.t("dataGrid.showAll"), clickEvent: () => this.showAllColumns()}));
|
|
1832
|
-
this.#
|
|
1833
|
-
this.#
|
|
1902
|
+
dialog.addToFooter(new Button({ title: I18n.t("dataGrid.showAll"), clickEvent: () => this.showAllColumns() }));
|
|
1903
|
+
this.#chooserDialog = dialog;
|
|
1904
|
+
this.#chooserBlock = block;
|
|
1834
1905
|
document.body.appendChild(dialog.set());
|
|
1835
1906
|
this.#refreshChooser();
|
|
1836
1907
|
return dialog;
|
|
1837
1908
|
}
|
|
1838
1909
|
|
|
1839
1910
|
#refreshChooser() {
|
|
1840
|
-
if (this.#
|
|
1841
|
-
this.#
|
|
1911
|
+
if (this.#chooserBlock === undefined) return;
|
|
1912
|
+
this.#chooserBlock.clear();
|
|
1842
1913
|
for (const column of this.#columns) {
|
|
1843
1914
|
const checkbox = new CheckBox({
|
|
1844
1915
|
name: `${this.#id}_column_${column.key}`,
|
|
1845
1916
|
title: column.title,
|
|
1846
|
-
changeListener: (event) => this.setColumnVisible(column.key, event.target.checked)
|
|
1917
|
+
changeListener: (event) => this.setColumnVisible(column.key, event.target.checked),
|
|
1847
1918
|
});
|
|
1848
1919
|
// У CheckBox нет опции начального значения: ставим его отдельно
|
|
1849
1920
|
checkbox.setValue(this.isColumnVisible(column.key));
|
|
1850
|
-
this.#
|
|
1921
|
+
this.#chooserBlock.add(checkbox);
|
|
1851
1922
|
}
|
|
1852
1923
|
}
|
|
1853
1924
|
|
|
@@ -1855,48 +1926,51 @@ class DataGrid {
|
|
|
1855
1926
|
#requestParams(offset = Number()) {
|
|
1856
1927
|
return {
|
|
1857
1928
|
offset: offset,
|
|
1858
|
-
limit: this.#
|
|
1929
|
+
limit: this.#pageSize,
|
|
1859
1930
|
filter: this.#filter,
|
|
1860
|
-
filterKeys: this.#
|
|
1861
|
-
sort: {key: this.#sort.key, direction: this.#sort.direction}
|
|
1931
|
+
filterKeys: this.#filterKeys,
|
|
1932
|
+
sort: { key: this.#sort.key, direction: this.#sort.direction },
|
|
1862
1933
|
};
|
|
1863
1934
|
}
|
|
1864
1935
|
|
|
1865
1936
|
async #load(mode = "first") {
|
|
1866
|
-
if (this.#
|
|
1937
|
+
if (this.#dataSource === undefined || this.#loading) return;
|
|
1867
1938
|
const request = this.#request + 1;
|
|
1868
1939
|
this.#request = request;
|
|
1869
1940
|
this.#loading = true;
|
|
1870
1941
|
this.#setError(undefined);
|
|
1871
1942
|
this.#list.setLoadingMore(true);
|
|
1872
1943
|
try {
|
|
1873
|
-
const payload = await this.#
|
|
1944
|
+
const payload = await this.#dataSource(this.#requestParams(mode === "first" ? 0 : this.#data.length));
|
|
1874
1945
|
if (request !== this.#request) return;
|
|
1875
1946
|
const rows = DataGrid.#payloadRows(payload);
|
|
1876
1947
|
const total = DataGrid.#payloadTotal(payload);
|
|
1877
1948
|
this.#data = mode === "first" ? rows : this.#data.concat(rows);
|
|
1878
|
-
this.#
|
|
1879
|
-
this.#
|
|
1949
|
+
this.#serverTotal = total;
|
|
1950
|
+
this.#hasMore = total === undefined ? rows.length >= this.#pageSize : this.#data.length < total;
|
|
1880
1951
|
this.#reindex();
|
|
1881
1952
|
this.#recompute();
|
|
1882
|
-
this.#announce(
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1953
|
+
this.#announce(
|
|
1954
|
+
total === undefined
|
|
1955
|
+
? I18n.t("dataGrid.loadedRows", { shown: this.#data.length })
|
|
1956
|
+
: I18n.t("dataGrid.loadedRowsOf", { shown: this.#data.length, total: total })
|
|
1957
|
+
);
|
|
1958
|
+
if (this.#onDataLoad !== undefined) this.#onDataLoad(rows, total);
|
|
1886
1959
|
} catch (error) {
|
|
1887
1960
|
if (request !== this.#request) return;
|
|
1888
1961
|
this.#failed = true;
|
|
1889
|
-
const message =
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
this.#
|
|
1894
|
-
|
|
1962
|
+
const message =
|
|
1963
|
+
error !== undefined && error !== null && error.message !== undefined
|
|
1964
|
+
? String(error.message)
|
|
1965
|
+
: I18n.t("dataGrid.unknownError");
|
|
1966
|
+
this.#setError(I18n.t("dataGrid.loadFailed", { message: message }));
|
|
1967
|
+
this.#announce(I18n.t("dataGrid.loadFailedAnnounce", { message: message }));
|
|
1968
|
+
if (this.#onDataError !== undefined) this.#onDataError(error, this.#requestParams(this.#data.length));
|
|
1895
1969
|
} finally {
|
|
1896
1970
|
if (request === this.#request) {
|
|
1897
1971
|
this.#loading = false;
|
|
1898
1972
|
this.#list.setLoadingMore(false);
|
|
1899
|
-
this.#list.setHasMore(this.#
|
|
1973
|
+
this.#list.setHasMore(this.#hasMore && this.#failed === false);
|
|
1900
1974
|
}
|
|
1901
1975
|
}
|
|
1902
1976
|
}
|
|
@@ -1904,11 +1978,11 @@ class DataGrid {
|
|
|
1904
1978
|
#setError(message = undefined) {
|
|
1905
1979
|
const broken = message !== undefined;
|
|
1906
1980
|
this.#error.hidden = broken === false;
|
|
1907
|
-
this.#
|
|
1981
|
+
this.#errorText.innerText = broken ? String(message) : "";
|
|
1908
1982
|
// «Нет данных» и «не удалось загрузить» — разное, поэтому подпись пустого списка меняется
|
|
1909
|
-
this.#list.setEmptyText(
|
|
1910
|
-
? I18n.t("dataGrid.notLoaded")
|
|
1911
|
-
|
|
1983
|
+
this.#list.setEmptyText(
|
|
1984
|
+
broken ? I18n.t("dataGrid.notLoaded") : optionOr(this.#emptyText, I18n.t("dataGrid.empty"))
|
|
1985
|
+
);
|
|
1912
1986
|
}
|
|
1913
1987
|
|
|
1914
1988
|
// === ЭКСПОРТ ===
|
|
@@ -1916,26 +1990,35 @@ class DataGrid {
|
|
|
1916
1990
|
* CSV текущего вида: значения берутся из `render`, если он есть (в файле видно то же,
|
|
1917
1991
|
* что в ячейках), заголовки групп не попадают, кавычки и разделитель экранируются.
|
|
1918
1992
|
*/
|
|
1919
|
-
exportCSV(options = {delimiter: String(), columns: Array(), source: String(), includeHeaders: Boolean()}) {
|
|
1993
|
+
exportCSV(options = { delimiter: String(), columns: Array(), source: String(), includeHeaders: Boolean() }) {
|
|
1920
1994
|
const delimiter = optionOr(options.delimiter, ",");
|
|
1921
1995
|
const source = optionOr(options.source, DataGrid.EXPORT.VIEW);
|
|
1922
1996
|
const rows = source === DataGrid.EXPORT.ALL ? this.#data : this.#sorted;
|
|
1923
1997
|
// По умолчанию — видимые колонки: скрытой колонки нет в файле, как нет и в сетке
|
|
1924
|
-
const columns =
|
|
1925
|
-
|
|
1926
|
-
|
|
1998
|
+
const columns =
|
|
1999
|
+
options.columns === undefined
|
|
2000
|
+
? this.#visibleColumns().map((entry) => entry.column)
|
|
2001
|
+
: this.#columns.filter((column) => options.columns.includes(column.key));
|
|
1927
2002
|
const lines = [];
|
|
1928
|
-
if (options.includeHeaders !== false)
|
|
2003
|
+
if (options.includeHeaders !== false)
|
|
2004
|
+
lines.push(columns.map((column) => DataGrid.#csvCell(column.title, delimiter)).join(delimiter));
|
|
1929
2005
|
for (const row of rows) {
|
|
1930
|
-
lines.push(
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2006
|
+
lines.push(
|
|
2007
|
+
columns
|
|
2008
|
+
.map((column) => {
|
|
2009
|
+
const index = this.#rowIndexOf(row);
|
|
2010
|
+
const value = typeof column.render === "function" ? column.render(row, index) : row[column.key];
|
|
2011
|
+
// Компонент превращаем в текст: в файле должно быть видно то же, что в ячейке
|
|
2012
|
+
const node =
|
|
2013
|
+
value instanceof Node
|
|
2014
|
+
? value
|
|
2015
|
+
: value !== undefined && value !== null && typeof value.set === "function"
|
|
2016
|
+
? value.set()
|
|
2017
|
+
: value;
|
|
2018
|
+
return DataGrid.#csvCell(DataGrid.#valueText(node), delimiter);
|
|
2019
|
+
})
|
|
2020
|
+
.join(delimiter)
|
|
2021
|
+
);
|
|
1939
2022
|
}
|
|
1940
2023
|
return lines.join("\r\n");
|
|
1941
2024
|
}
|
|
@@ -1972,13 +2055,13 @@ class DataGrid {
|
|
|
1972
2055
|
// === ДАННЫЕ И СОСТОЯНИЕ СЕТКИ ===
|
|
1973
2056
|
/** Индекс строки в исходных данных: нужен и `render(row, index)`, и ключу по умолчанию */
|
|
1974
2057
|
#rowIndexOf(row = Object()) {
|
|
1975
|
-
const index = this.#
|
|
2058
|
+
const index = this.#rowIndex.get(row);
|
|
1976
2059
|
return index === undefined ? -1 : index;
|
|
1977
2060
|
}
|
|
1978
2061
|
|
|
1979
2062
|
#reindex() {
|
|
1980
|
-
this.#
|
|
1981
|
-
for (let index = 0; index < this.#data.length; index++) this.#
|
|
2063
|
+
this.#rowIndex = new Map();
|
|
2064
|
+
for (let index = 0; index < this.#data.length; index++) this.#rowIndex.set(this.#data[index], index);
|
|
1982
2065
|
}
|
|
1983
2066
|
|
|
1984
2067
|
/** Замыкание выравнивания и прокрутки: после смены колонок всё считается заново */
|
|
@@ -1995,53 +2078,125 @@ class DataGrid {
|
|
|
1995
2078
|
return out;
|
|
1996
2079
|
}
|
|
1997
2080
|
// === ПУБЛИЧНОЕ: ЧТЕНИЕ ===
|
|
1998
|
-
getId() {
|
|
1999
|
-
|
|
2081
|
+
getId() {
|
|
2082
|
+
return this.#id;
|
|
2083
|
+
}
|
|
2084
|
+
getLabel() {
|
|
2085
|
+
return this.#label;
|
|
2086
|
+
}
|
|
2000
2087
|
/** Колонки в текущем порядке (те же объекты, что читает разметка) */
|
|
2001
|
-
getColumns() {
|
|
2002
|
-
|
|
2088
|
+
getColumns() {
|
|
2089
|
+
return this.#columns.map((column) => ({ ...column }));
|
|
2090
|
+
}
|
|
2091
|
+
getData() {
|
|
2092
|
+
return this.#data.slice();
|
|
2093
|
+
}
|
|
2003
2094
|
/** Строки данных в порядке показа (без заголовков групп) */
|
|
2004
|
-
getRows() {
|
|
2005
|
-
|
|
2095
|
+
getRows() {
|
|
2096
|
+
return this.#viewRows();
|
|
2097
|
+
}
|
|
2098
|
+
getRowCount() {
|
|
2099
|
+
return this.#viewRows().length;
|
|
2100
|
+
}
|
|
2006
2101
|
/** Элементов списка: заголовки групп тоже считаются */
|
|
2007
|
-
getEntryCount() {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2102
|
+
getEntryCount() {
|
|
2103
|
+
return this.#entryCount;
|
|
2104
|
+
}
|
|
2105
|
+
getRowHeight() {
|
|
2106
|
+
return this.#rowHeight;
|
|
2107
|
+
}
|
|
2108
|
+
getVisibleRange() {
|
|
2109
|
+
return this.#list.getVisibleRange();
|
|
2110
|
+
}
|
|
2111
|
+
getRenderedRange() {
|
|
2112
|
+
return this.#list.getRenderedRange();
|
|
2113
|
+
}
|
|
2114
|
+
getRowElement(index = Number()) {
|
|
2115
|
+
return this.#list.getItemElement(index);
|
|
2116
|
+
}
|
|
2117
|
+
getActiveCell() {
|
|
2118
|
+
return {
|
|
2119
|
+
...this.#active,
|
|
2120
|
+
key: this.#columns[this.#active.col] === undefined ? undefined : this.#columns[this.#active.col].key,
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
getSelected() {
|
|
2124
|
+
return this.#selectedRows();
|
|
2125
|
+
}
|
|
2126
|
+
getSelectedKeys() {
|
|
2127
|
+
return Array.from(this.#selected);
|
|
2128
|
+
}
|
|
2129
|
+
getFilter() {
|
|
2130
|
+
return this.#filter;
|
|
2131
|
+
}
|
|
2132
|
+
getSort() {
|
|
2133
|
+
return { ...this.#sort };
|
|
2134
|
+
}
|
|
2135
|
+
getSelectionMode() {
|
|
2136
|
+
return this.#selection;
|
|
2137
|
+
}
|
|
2138
|
+
isServer() {
|
|
2139
|
+
return this.#dataSource !== undefined;
|
|
2140
|
+
}
|
|
2141
|
+
isLoading() {
|
|
2142
|
+
return this.#loading;
|
|
2143
|
+
}
|
|
2144
|
+
getServerTotal() {
|
|
2145
|
+
return this.#serverTotal;
|
|
2146
|
+
}
|
|
2147
|
+
getPageSize() {
|
|
2148
|
+
return this.#pageSize;
|
|
2149
|
+
}
|
|
2150
|
+
isEditing() {
|
|
2151
|
+
return this.#editing !== undefined;
|
|
2152
|
+
}
|
|
2153
|
+
getEditingCell() {
|
|
2154
|
+
return this.#editing === undefined
|
|
2155
|
+
? undefined
|
|
2156
|
+
: { row: this.#editing.row, col: this.#editing.col, key: this.#editing.key };
|
|
2157
|
+
}
|
|
2158
|
+
isEditable() {
|
|
2159
|
+
return this.#editable !== undefined && this.#editable !== false;
|
|
2160
|
+
}
|
|
2161
|
+
isColumnEditable(col = Number()) {
|
|
2162
|
+
return this.#isColumnEditable(col);
|
|
2163
|
+
}
|
|
2026
2164
|
/** Колонки в порядке показа: место в полном списке рядом с самой колонкой */
|
|
2027
|
-
getVisibleColumns() {
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2165
|
+
getVisibleColumns() {
|
|
2166
|
+
return this.#visibleColumns().map((entry) => ({ ...entry.column }));
|
|
2167
|
+
}
|
|
2168
|
+
getHiddenColumns() {
|
|
2169
|
+
return this.#columns.filter((column) => this.#hidden.has(column.key)).map((column) => ({ ...column }));
|
|
2170
|
+
}
|
|
2171
|
+
getPinned() {
|
|
2172
|
+
return this.#pinned.slice();
|
|
2173
|
+
}
|
|
2174
|
+
isPinned(key = String()) {
|
|
2175
|
+
return this.#isPinned(key);
|
|
2176
|
+
}
|
|
2177
|
+
isColumnVisible(key = String()) {
|
|
2178
|
+
return this.#hidden.has(key) === false;
|
|
2179
|
+
}
|
|
2180
|
+
isColumnReorder() {
|
|
2181
|
+
return this.#columnReorder;
|
|
2182
|
+
}
|
|
2183
|
+
getColumnOrder() {
|
|
2184
|
+
return this.#columns.map((column) => column.key);
|
|
2185
|
+
}
|
|
2186
|
+
getGroupBy() {
|
|
2187
|
+
return this.#groupBy;
|
|
2188
|
+
}
|
|
2035
2189
|
/** Группы с агрегатами и состоянием свёрнутости */
|
|
2036
2190
|
getGroups() {
|
|
2037
2191
|
return this.#groups.map((group) => ({
|
|
2038
2192
|
key: group.key,
|
|
2039
|
-
title:
|
|
2040
|
-
|
|
2041
|
-
|
|
2193
|
+
title:
|
|
2194
|
+
typeof this.#groupLabel === "function"
|
|
2195
|
+
? String(this.#groupLabel(group.key, group.rows))
|
|
2196
|
+
: I18n.t("dataGrid.groupTitle", { key: group.key, count: group.rows.length }),
|
|
2042
2197
|
rows: group.rows.slice(),
|
|
2043
2198
|
aggregates: group.aggregates,
|
|
2044
|
-
collapsed: group.collapsed
|
|
2199
|
+
collapsed: group.collapsed,
|
|
2045
2200
|
}));
|
|
2046
2201
|
}
|
|
2047
2202
|
isGroupCollapsed(key = String()) {
|
|
@@ -2071,9 +2226,9 @@ class DataGrid {
|
|
|
2071
2226
|
setData(data = []) {
|
|
2072
2227
|
this.#data = Array.isArray(data) ? data : [];
|
|
2073
2228
|
// Ручная замена данных выключает догрузку сервера: иначе она перезаписала бы их
|
|
2074
|
-
this.#
|
|
2075
|
-
this.#
|
|
2076
|
-
this.#
|
|
2229
|
+
this.#dataSource = undefined;
|
|
2230
|
+
this.#hasMore = false;
|
|
2231
|
+
this.#serverTotal = undefined;
|
|
2077
2232
|
this.#selected.clear();
|
|
2078
2233
|
this.#editing = undefined;
|
|
2079
2234
|
this.#failed = false;
|
|
@@ -2093,26 +2248,26 @@ class DataGrid {
|
|
|
2093
2248
|
}
|
|
2094
2249
|
|
|
2095
2250
|
setPageSize(size = Number()) {
|
|
2096
|
-
this.#
|
|
2251
|
+
this.#pageSize = DataGrid.#normalizePageSize(size);
|
|
2097
2252
|
return this;
|
|
2098
2253
|
}
|
|
2099
2254
|
|
|
2100
2255
|
setRowHeight(height = Number()) {
|
|
2101
|
-
this.#
|
|
2102
|
-
this.#list.setItemHeight(this.#
|
|
2256
|
+
this.#rowHeight = Number(height);
|
|
2257
|
+
this.#list.setItemHeight(this.#rowHeight);
|
|
2103
2258
|
this.#syncColumns();
|
|
2104
2259
|
return this;
|
|
2105
2260
|
}
|
|
2106
2261
|
|
|
2107
2262
|
/** Перечитать страницы с сервера с текущим фильтром и сортировкой */
|
|
2108
2263
|
reload() {
|
|
2109
|
-
if (this.#
|
|
2264
|
+
if (this.#dataSource === undefined) return this;
|
|
2110
2265
|
this.#request = this.#request + 1;
|
|
2111
2266
|
this.#loading = false;
|
|
2112
2267
|
this.#failed = false;
|
|
2113
2268
|
this.#data = [];
|
|
2114
|
-
this.#
|
|
2115
|
-
this.#
|
|
2269
|
+
this.#hasMore = true;
|
|
2270
|
+
this.#serverTotal = undefined;
|
|
2116
2271
|
this.#reindex();
|
|
2117
2272
|
this.#recompute();
|
|
2118
2273
|
this.#load("first");
|
|
@@ -2120,27 +2275,30 @@ class DataGrid {
|
|
|
2120
2275
|
}
|
|
2121
2276
|
|
|
2122
2277
|
loadMore() {
|
|
2123
|
-
if (this.#
|
|
2278
|
+
if (this.#dataSource === undefined || this.#hasMore === false || this.#failed) return this;
|
|
2124
2279
|
this.#load("more");
|
|
2125
2280
|
return this;
|
|
2126
2281
|
}
|
|
2127
2282
|
|
|
2128
2283
|
retryLoad() {
|
|
2129
|
-
if (this.#
|
|
2284
|
+
if (this.#dataSource === undefined) return this;
|
|
2130
2285
|
this.#failed = false;
|
|
2131
|
-
this.#list.setHasMore(this.#
|
|
2286
|
+
this.#list.setHasMore(this.#hasMore);
|
|
2132
2287
|
this.#load("more");
|
|
2133
2288
|
return this;
|
|
2134
2289
|
}
|
|
2135
2290
|
|
|
2136
2291
|
setFilter(text = String()) {
|
|
2137
2292
|
this.#filter = String(text);
|
|
2138
|
-
if (this.#
|
|
2293
|
+
if (this.#dataSource !== undefined) return this.reload();
|
|
2139
2294
|
this.#recompute();
|
|
2140
2295
|
// Про фильтр сообщаем числом строк: «фильтр «n»: 3 из 12»
|
|
2141
2296
|
const shown = this.#sorted.length;
|
|
2142
2297
|
if (this.#filter.trim() === "") this.#announce(this.#rowsSummary());
|
|
2143
|
-
else
|
|
2298
|
+
else
|
|
2299
|
+
this.#announce(
|
|
2300
|
+
I18n.t("dataGrid.filterSummary", { filter: this.#filter, shown: shown, total: this.#data.length })
|
|
2301
|
+
);
|
|
2144
2302
|
return this;
|
|
2145
2303
|
}
|
|
2146
2304
|
|
|
@@ -2151,8 +2309,8 @@ class DataGrid {
|
|
|
2151
2309
|
/** Сортировка: `NONE` снимает её; в серверном режиме строки спрашиваются заново */
|
|
2152
2310
|
setSort(key = undefined, direction = DataGrid.SORT.NONE) {
|
|
2153
2311
|
const known = this.#columns.some((column) => column.key === key) ? key : undefined;
|
|
2154
|
-
this.#sort = {key: known, direction: known === undefined ? DataGrid.SORT.NONE : direction};
|
|
2155
|
-
if (this.#
|
|
2312
|
+
this.#sort = { key: known, direction: known === undefined ? DataGrid.SORT.NONE : direction };
|
|
2313
|
+
if (this.#dataSource !== undefined) return this.reload();
|
|
2156
2314
|
this.#applySort();
|
|
2157
2315
|
return this;
|
|
2158
2316
|
}
|
|
@@ -2168,14 +2326,15 @@ class DataGrid {
|
|
|
2168
2326
|
}
|
|
2169
2327
|
|
|
2170
2328
|
setOnActiveCellChange(listener = undefined) {
|
|
2171
|
-
this.#
|
|
2329
|
+
this.#onActiveCellChange = listener;
|
|
2172
2330
|
return this;
|
|
2173
2331
|
}
|
|
2174
2332
|
|
|
2175
2333
|
setContextMenu(ctx = undefined) {
|
|
2176
|
-
if (this.#
|
|
2177
|
-
if (this.#
|
|
2178
|
-
|
|
2334
|
+
if (this.#contextMenu === ctx) return this;
|
|
2335
|
+
if (this.#contextMenu !== undefined && typeof this.#contextMenu.detach === "function")
|
|
2336
|
+
this.#contextMenu.detach();
|
|
2337
|
+
this.#contextMenu = ctx;
|
|
2179
2338
|
if (ctx !== undefined && typeof ctx.attach === "function") ctx.attach(this.#root);
|
|
2180
2339
|
return this;
|
|
2181
2340
|
}
|
|
@@ -2231,7 +2390,9 @@ class DataGrid {
|
|
|
2231
2390
|
if (visible) this.#hidden.delete(key);
|
|
2232
2391
|
else this.#hidden.add(key);
|
|
2233
2392
|
// Скрытая колонка не может быть активной: курсор уходит в первую видимую
|
|
2234
|
-
if (
|
|
2393
|
+
if (
|
|
2394
|
+
this.#hidden.has(this.#columns[this.#active.col] === undefined ? "" : this.#columns[this.#active.col].key)
|
|
2395
|
+
) {
|
|
2235
2396
|
this.#active.col = this.#visibleColumns().length === 0 ? 0 : this.#visibleColumns()[0].index;
|
|
2236
2397
|
}
|
|
2237
2398
|
this.#afterLayout();
|
|
@@ -2268,13 +2429,13 @@ class DataGrid {
|
|
|
2268
2429
|
}
|
|
2269
2430
|
|
|
2270
2431
|
setColumnReorder(enabled = Boolean()) {
|
|
2271
|
-
this.#
|
|
2432
|
+
this.#columnReorder = enabled === true;
|
|
2272
2433
|
this.#buildHeader();
|
|
2273
2434
|
return this;
|
|
2274
2435
|
}
|
|
2275
2436
|
|
|
2276
2437
|
setGroupBy(groupBy = undefined) {
|
|
2277
|
-
this.#
|
|
2438
|
+
this.#groupBy = groupBy;
|
|
2278
2439
|
this.#collapsed.clear();
|
|
2279
2440
|
this.#recompute();
|
|
2280
2441
|
return this;
|
|
@@ -2322,26 +2483,27 @@ class DataGrid {
|
|
|
2322
2483
|
|
|
2323
2484
|
/** Снять подписки и отпустить ресурсы; узел остаётся в документе */
|
|
2324
2485
|
destroy() {
|
|
2325
|
-
this.#
|
|
2326
|
-
this.#
|
|
2327
|
-
this.#
|
|
2328
|
-
this.#
|
|
2329
|
-
this.#
|
|
2330
|
-
this.#
|
|
2331
|
-
this.#
|
|
2332
|
-
this.#
|
|
2486
|
+
this.#resizeObserver.disconnect();
|
|
2487
|
+
this.#headRow.removeEventListener("click", this.#onHeaderClick);
|
|
2488
|
+
this.#headRow.removeEventListener("keydown", this.#onHeaderKeyDown);
|
|
2489
|
+
this.#headRow.removeEventListener("dragstart", this.#onHeaderDragStart);
|
|
2490
|
+
this.#headRow.removeEventListener("dragover", this.#onHeaderDragOver);
|
|
2491
|
+
this.#headRow.removeEventListener("dragleave", this.#onHeaderDragLeave);
|
|
2492
|
+
this.#headRow.removeEventListener("drop", this.#onHeaderDrop);
|
|
2493
|
+
this.#headRow.removeEventListener("dragend", this.#onHeaderDragEnd);
|
|
2333
2494
|
this.#root.removeEventListener("dblclick", this.#onDoubleClick, true);
|
|
2334
2495
|
this.#root.removeEventListener("keydown", this.#onKeyDown);
|
|
2335
2496
|
this.#scroll.removeEventListener("scroll", this.#onScrollX);
|
|
2336
2497
|
if (this.#editing !== undefined) this.#finishEdit(false, false, false);
|
|
2337
|
-
if (this.#
|
|
2498
|
+
if (this.#contextMenu !== undefined && typeof this.#contextMenu.detach === "function")
|
|
2499
|
+
this.#contextMenu.detach();
|
|
2338
2500
|
this.#list.destroy();
|
|
2339
|
-
if (this.#
|
|
2501
|
+
if (this.#chooserDialog !== undefined) {
|
|
2340
2502
|
// У диалога нет своего «удали себя»: узел снимаем с документа сами
|
|
2341
|
-
const node = this.#
|
|
2503
|
+
const node = this.#chooserDialog.set();
|
|
2342
2504
|
if (node !== undefined && node.parentNode !== null) node.parentNode.removeChild(node);
|
|
2343
|
-
this.#
|
|
2344
|
-
this.#
|
|
2505
|
+
this.#chooserDialog = undefined;
|
|
2506
|
+
this.#chooserBlock = undefined;
|
|
2345
2507
|
}
|
|
2346
2508
|
return this;
|
|
2347
2509
|
}
|