chuijs 4.0.2 → 4.0.3
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 +61 -0
- package/README.md +43 -644
- package/framework/appLayout/default/chui_app_layout.js +308 -260
- package/framework/appLayout/default/global_style.css +85 -48
- package/framework/components/chui_accordion/accordion.js +29 -26
- package/framework/components/chui_accordion/styles.css +6 -5
- package/framework/components/chui_action_sheet/action_sheet.js +59 -33
- package/framework/components/chui_action_sheet/styles.css +2 -1
- package/framework/components/chui_affix/affix.js +11 -6
- package/framework/components/chui_alert/alert.js +23 -14
- package/framework/components/chui_avatar/avatar.js +23 -22
- package/framework/components/chui_avatar_group/avatar_group.js +21 -19
- package/framework/components/chui_backtop/backtop.js +35 -19
- package/framework/components/chui_badge/badge.js +15 -13
- package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
- package/framework/components/chui_button/button.js +42 -22
- package/framework/components/chui_button/styles.css +2 -2
- package/framework/components/chui_calendar/calendar.js +12 -10
- package/framework/components/chui_card/card.js +43 -23
- package/framework/components/chui_chat/chat.js +72 -33
- package/framework/components/chui_clipboard_history/clipboard_history.js +30 -21
- package/framework/components/chui_clipboard_history/styles.css +4 -2
- package/framework/components/chui_code/code.js +2 -2
- package/framework/components/chui_code/styles.css +2 -2
- package/framework/components/chui_code_editor/code_editor.js +47 -31
- package/framework/components/chui_code_editor/styles.css +3 -2
- package/framework/components/chui_command_palette/command_palette.js +60 -37
- package/framework/components/chui_console/console.js +5 -5
- package/framework/components/chui_console/styles.css +1 -1
- package/framework/components/chui_content_block/content_block.js +33 -19
- package/framework/components/chui_content_block/styles.css +2 -2
- package/framework/components/chui_context_menu/context_menu.js +9 -4
- package/framework/components/chui_custom_element/custom_element.js +22 -15
- package/framework/components/chui_data_grid/data_grid.js +396 -234
- package/framework/components/chui_data_grid/styles.css +10 -8
- package/framework/components/chui_descriptions/descriptions.js +34 -17
- package/framework/components/chui_details/details.js +32 -26
- package/framework/components/chui_details/styles.css +5 -4
- package/framework/components/chui_diff_viewer/diff_viewer.js +91 -66
- package/framework/components/chui_diff_viewer/styles.css +1 -1
- package/framework/components/chui_divider/divider.js +13 -11
- package/framework/components/chui_drawer/drawer.js +27 -25
- package/framework/components/chui_drop_zone/drop_zone.js +31 -21
- package/framework/components/chui_empty_state/empty_state.js +20 -15
- package/framework/components/chui_fieldset/fieldset.js +37 -20
- package/framework/components/chui_fieldset/styles.css +1 -1
- package/framework/components/chui_file_explorer/file_explorer.js +157 -86
- package/framework/components/chui_file_explorer/styles.css +2 -1
- package/framework/components/chui_form/form.js +31 -19
- package/framework/components/chui_form/styles.css +1 -1
- package/framework/components/chui_gantt/gantt.js +64 -40
- package/framework/components/chui_graphs/graphs.js +301 -200
- package/framework/components/chui_graphs/styles.css +1 -1
- package/framework/components/chui_hero/hero.js +40 -22
- package/framework/components/chui_html_block/html_block.js +6 -6
- package/framework/components/chui_html_block/styles.css +2 -2
- package/framework/components/chui_hx/hx.js +3 -3
- package/framework/components/chui_hx/styles.css +11 -4
- package/framework/components/chui_icons/icons.js +2109 -2109
- package/framework/components/chui_icons/styles.css +2 -2
- package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
- package/framework/components/chui_inputs/chui_check_box/styles.css +18 -18
- package/framework/components/chui_inputs/chui_chips/chips.js +50 -40
- package/framework/components/chui_inputs/chui_chips/styles.css +4 -2
- package/framework/components/chui_inputs/chui_color_picker/color_picker.js +36 -24
- package/framework/components/chui_inputs/chui_combo_box/combo_box.js +100 -82
- package/framework/components/chui_inputs/chui_combo_box/styles.css +14 -12
- package/framework/components/chui_inputs/chui_date/date.js +102 -66
- package/framework/components/chui_inputs/chui_date/styles.css +12 -11
- package/framework/components/chui_inputs/chui_date_range/date_range.js +40 -29
- package/framework/components/chui_inputs/chui_datetime/datetime.js +35 -27
- package/framework/components/chui_inputs/chui_email/email.js +43 -31
- package/framework/components/chui_inputs/chui_email/styles.css +5 -5
- package/framework/components/chui_inputs/chui_file/file.js +68 -41
- package/framework/components/chui_inputs/chui_file/styles.css +13 -12
- package/framework/components/chui_inputs/chui_hotkey/hotkey.js +61 -36
- package/framework/components/chui_inputs/chui_masked/masked.js +39 -28
- package/framework/components/chui_inputs/chui_mention/mention.js +77 -30
- package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +173 -153
- package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +9 -7
- package/framework/components/chui_inputs/chui_number/number.js +58 -41
- package/framework/components/chui_inputs/chui_number/styles.css +7 -7
- package/framework/components/chui_inputs/chui_otp/otp.js +38 -29
- package/framework/components/chui_inputs/chui_password/password.js +58 -36
- package/framework/components/chui_inputs/chui_password/styles.css +8 -8
- package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
- package/framework/components/chui_inputs/chui_radio_button/styles.css +20 -20
- package/framework/components/chui_inputs/chui_rating/rating.js +26 -22
- package/framework/components/chui_inputs/chui_select_box/select_box.js +102 -78
- package/framework/components/chui_inputs/chui_select_box/styles.css +11 -11
- package/framework/components/chui_inputs/chui_slider/slider.js +29 -26
- package/framework/components/chui_inputs/chui_slider/styles.css +2 -1
- package/framework/components/chui_inputs/chui_text/styles.css +8 -8
- package/framework/components/chui_inputs/chui_text/text.js +65 -44
- package/framework/components/chui_inputs/chui_text_area/styles.css +6 -6
- package/framework/components/chui_inputs/chui_text_area/text_area.js +51 -28
- package/framework/components/chui_inputs/chui_time/styles.css +5 -3
- package/framework/components/chui_inputs/chui_time/time.js +38 -27
- package/framework/components/chui_inputs/chui_toggle/styles.css +3 -3
- package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
- package/framework/components/chui_json_editor/json_editor.js +53 -43
- package/framework/components/chui_json_viewer/json_viewer.js +52 -33
- package/framework/components/chui_json_viewer/styles.css +3 -2
- package/framework/components/chui_kanban/kanban.js +52 -34
- package/framework/components/chui_kbd/kbd.js +12 -10
- package/framework/components/chui_label/label.js +14 -12
- package/framework/components/chui_label/styles.css +2 -2
- package/framework/components/chui_link/link.js +27 -20
- package/framework/components/chui_list/list.js +31 -27
- package/framework/components/chui_list/styles.css +2 -1
- package/framework/components/chui_markdown_editor/markdown_editor.js +60 -44
- package/framework/components/chui_media/audio.js +326 -227
- package/framework/components/chui_media/audio_styles.css +40 -21
- package/framework/components/chui_media/image.js +70 -38
- package/framework/components/chui_media/image_compare.js +52 -21
- package/framework/components/chui_media/image_styles.css +3 -3
- package/framework/components/chui_media/video.js +155 -62
- package/framework/components/chui_media/video_styles.css +33 -16
- package/framework/components/chui_menu_bar/menu_bar.js +26 -24
- package/framework/components/chui_menu_bar/styles.css +4 -2
- package/framework/components/chui_modal/modal.js +44 -43
- package/framework/components/chui_modal/styles.css +3 -2
- package/framework/components/chui_notification/notification.js +50 -34
- package/framework/components/chui_notification/notification_download.js +31 -25
- package/framework/components/chui_notification/notification_download_progress.css +3 -2
- package/framework/components/chui_notification/notification_download_progress.js +47 -41
- package/framework/components/chui_notification/notification_update.js +23 -16
- package/framework/components/chui_notification/status_plate.js +2 -2
- package/framework/components/chui_notification/styles_download.css +1 -1
- package/framework/components/chui_notification/styles_update.css +1 -1
- package/framework/components/chui_org_chart/org_chart.js +74 -35
- package/framework/components/chui_pagination/pagination.js +62 -27
- package/framework/components/chui_paragraph/paragraph.js +2 -2
- package/framework/components/chui_paragraph/styles.css +1 -1
- package/framework/components/chui_popover/popover.js +49 -28
- package/framework/components/chui_popups/popups.js +137 -108
- package/framework/components/chui_popups/styles.css +2 -2
- package/framework/components/chui_print_button/print_button.js +33 -23
- package/framework/components/chui_progress_bar/progress_bar.js +23 -15
- package/framework/components/chui_progress_bar/styles.css +2 -2
- package/framework/components/chui_progress_ring/progress_ring.js +17 -15
- package/framework/components/chui_qrcode/qrcode.js +75 -31
- package/framework/components/chui_query_builder/query_builder.js +216 -123
- package/framework/components/chui_radio_group/radio_group.js +36 -26
- package/framework/components/chui_radio_group/styles.css +1 -1
- package/framework/components/chui_result/result.js +26 -16
- package/framework/components/chui_scheduler/scheduler.js +57 -35
- package/framework/components/chui_scheduler/styles.css +6 -2
- package/framework/components/chui_segmented_control/segmented_control.js +27 -21
- package/framework/components/chui_settings/settings.js +122 -48
- package/framework/components/chui_shortcut_help/shortcut_help.js +55 -32
- package/framework/components/chui_sidebar_nav/sidebar_nav.js +33 -23
- package/framework/components/chui_skeleton/skeleton.js +29 -24
- package/framework/components/chui_slideshow/slideshow.js +60 -41
- package/framework/components/chui_slideshow/styles.css +18 -12
- package/framework/components/chui_spinner/spinner.js +8 -8
- package/framework/components/chui_spinner/styles.css +0 -1
- package/framework/components/chui_split_button/split_button.js +23 -18
- package/framework/components/chui_split_button/styles.css +1 -1
- package/framework/components/chui_splitter/splitter.js +35 -28
- package/framework/components/chui_statistic/statistic.js +65 -38
- package/framework/components/chui_status_bar/status_bar.js +11 -9
- package/framework/components/chui_stepper/stepper.js +27 -22
- package/framework/components/chui_sticky_bar/sticky_bar.js +9 -4
- package/framework/components/chui_sticky_bar/styles.css +3 -1
- package/framework/components/chui_tabs/chui_tabs.js +28 -22
- package/framework/components/chui_tabs/styles.css +3 -3
- package/framework/components/chui_text_editor/styles.css +20 -19
- package/framework/components/chui_text_editor/text_editor.js +85 -61
- package/framework/components/chui_text_editor/text_editor_panel.js +346 -284
- package/framework/components/chui_theme_toggle/theme_toggle.js +27 -22
- package/framework/components/chui_timeline/timeline.js +14 -12
- package/framework/components/chui_toast_queue/toast_queue.js +52 -46
- package/framework/components/chui_toolbar/toolbar.js +20 -13
- package/framework/components/chui_tooltip/tooltip.js +37 -24
- package/framework/components/chui_tour/styles.css +8 -3
- package/framework/components/chui_tour/tour.js +61 -27
- package/framework/components/chui_transfer/styles.css +2 -1
- package/framework/components/chui_transfer/transfer.js +75 -42
- package/framework/components/chui_tree_table/tree_table.js +43 -29
- package/framework/components/chui_tree_view/styles.css +4 -2
- package/framework/components/chui_tree_view/tree_view.js +142 -99
- package/framework/components/chui_watermark/watermark.js +24 -16
- package/framework/components/chui_webview/styles.css +2 -2
- package/framework/components/chui_webview/webview.js +62 -50
- package/framework/components/telegram_bot/chui_telegram_bot.js +71 -64
- package/framework/modules/chui_a11y/chui_a11y.js +77 -46
- package/framework/modules/chui_animations/animations.js +42 -42
- package/framework/modules/chui_animations/styles.css +1 -1
- package/framework/modules/chui_bridge/preload.js +36 -25
- package/framework/modules/chui_functions.js +52 -50
- package/framework/modules/chui_highlight/chui_highlight.js +25 -22
- package/framework/modules/chui_i18n/chui_i18n.js +9 -7
- 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 +3 -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 +8 -8
- package/framework/modules/chui_i18n/locales/validate.js +3 -3
- package/framework/modules/chui_json/chui_json.js +9 -9
- package/framework/modules/chui_layout/chui_layout.js +12 -10
- package/framework/modules/chui_logger/chui_logger.js +29 -25
- package/framework/modules/chui_page/page.js +33 -27
- package/framework/modules/chui_qr/chui_qr.js +158 -67
- package/framework/modules/chui_scrollbar/chui_scrollbar.js +35 -24
- package/framework/modules/chui_scrollbar/styles.css +3 -1
- package/framework/modules/chui_services/chui_channels.js +27 -13
- package/framework/modules/chui_services/chui_services.js +39 -31
- package/framework/modules/chui_services/desktop.js +32 -31
- package/framework/modules/chui_sortable/chui_sortable.js +17 -12
- package/framework/modules/chui_validate/chui_validate.js +7 -4
- package/framework/tray_bar/chui_menu_items.js +136 -46
- package/index.d.ts +4814 -0
- package/index.js +385 -292
- package/package.json +79 -68
|
@@ -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,7 +81,7 @@ 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;
|
|
@@ -110,28 +110,30 @@ class DataGridBody {
|
|
|
110
110
|
#loading_more = false;
|
|
111
111
|
// Длина, для которой запрос уже сделан: не просим ещё раз, пока данных не прибавилось
|
|
112
112
|
#requested_at = -1;
|
|
113
|
-
#window_state = {from: 0, to: 0};
|
|
113
|
+
#window_state = { from: 0, to: 0 };
|
|
114
114
|
#raf = null;
|
|
115
115
|
#refresh_raf = null;
|
|
116
116
|
#resize_observer = 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
139
|
this.#item_tag = optionOr(options.itemTag, "data_grid_row");
|
|
@@ -143,7 +145,8 @@ class DataGridBody {
|
|
|
143
145
|
this.#on_render = options.onRender;
|
|
144
146
|
this.#on_load_more = options.onLoadMore;
|
|
145
147
|
this.#loading_text = optionOr(options.loadingText, I18n.t("common.loading"));
|
|
146
|
-
this.#load_threshold =
|
|
148
|
+
this.#load_threshold =
|
|
149
|
+
options.loadMoreThreshold === undefined ? DataGridBody.DEFAULT_LOAD_THRESHOLD : options.loadMoreThreshold;
|
|
147
150
|
// Без onLoadMore тело считает, что загружено всё
|
|
148
151
|
this.#has_more = options.hasMore === undefined ? options.onLoadMore !== undefined : options.hasMore === true;
|
|
149
152
|
this.#root.id = this.#id;
|
|
@@ -163,7 +166,7 @@ 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 дают
|
|
@@ -174,19 +177,29 @@ class DataGridBody {
|
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
// === ЧТЕНИЕ ===
|
|
177
|
-
getId() {
|
|
178
|
-
|
|
180
|
+
getId() {
|
|
181
|
+
return this.#id;
|
|
182
|
+
}
|
|
183
|
+
getItemHeight() {
|
|
184
|
+
return this.#item_height;
|
|
185
|
+
}
|
|
179
186
|
/** Окно, которое сейчас в DOM (вместе с запасом `buffer`) */
|
|
180
|
-
getRenderedRange() {
|
|
187
|
+
getRenderedRange() {
|
|
188
|
+
return { ...this.#window_state };
|
|
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()) {
|
|
@@ -351,15 +364,15 @@ class DataGridBody {
|
|
|
351
364
|
if (node.previousSibling !== previous) this.#root.insertBefore(node, previous.nextSibling);
|
|
352
365
|
previous = node;
|
|
353
366
|
}
|
|
354
|
-
this.#window_state = {from: from, to: to};
|
|
367
|
+
this.#window_state = { from: from, to: to };
|
|
355
368
|
this.#syncSpacers();
|
|
356
|
-
if (this.#on_render !== undefined) this.#on_render({from: from, to: to});
|
|
369
|
+
if (this.#on_render !== undefined) this.#on_render({ 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.#window_state;
|
|
375
|
+
const { from, to } = this.#window_state;
|
|
363
376
|
this.#spacer_top.style.height = count === 0 ? "0px" : `${from * this.#item_height}px`;
|
|
364
377
|
this.#spacer_bottom.style.height = count === 0 ? "0px" : `${Math.max(0, (count - to) * this.#item_height)}px`;
|
|
365
378
|
}
|
|
@@ -399,22 +412,22 @@ 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
426
|
if (this.#on_item_click !== undefined) this.#on_item_click(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
432
|
if (this.#on_activate !== undefined) this.#on_activate(this.#items[index], index, node);
|
|
420
433
|
};
|
|
@@ -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;
|
|
@@ -517,7 +530,7 @@ class DataGrid {
|
|
|
517
530
|
#row_index = new Map();
|
|
518
531
|
#filter = "";
|
|
519
532
|
#filter_keys = [];
|
|
520
|
-
#sort = {key: undefined, direction: DataGrid.SORT.NONE};
|
|
533
|
+
#sort = { key: undefined, direction: DataGrid.SORT.NONE };
|
|
521
534
|
#group_by = undefined;
|
|
522
535
|
#group_label = undefined;
|
|
523
536
|
#aggregates = {};
|
|
@@ -525,7 +538,7 @@ class DataGrid {
|
|
|
525
538
|
#collapsed = new Map();
|
|
526
539
|
#editable = undefined;
|
|
527
540
|
#editing = undefined;
|
|
528
|
-
#active = {row: 0, col: 0};
|
|
541
|
+
#active = { row: 0, col: 0 };
|
|
529
542
|
#entry_count = 0;
|
|
530
543
|
|
|
531
544
|
// Производные данные (пересобираются одним проходом — см. #recompute)
|
|
@@ -560,39 +573,41 @@ class DataGrid {
|
|
|
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);
|
|
@@ -631,7 +646,9 @@ class DataGrid {
|
|
|
631
646
|
this.#scroll.appendChild(this.#head);
|
|
632
647
|
this.#root.appendChild(this.#scroll);
|
|
633
648
|
this.#error.appendChild(this.#error_text);
|
|
634
|
-
this.#error.appendChild(
|
|
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");
|
|
@@ -666,7 +683,7 @@ 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);
|
|
@@ -686,7 +703,7 @@ class DataGrid {
|
|
|
686
703
|
this.#resize_observer = new ResizeObserver(() => this.#syncPinned());
|
|
687
704
|
this.#resize_observer.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();
|
|
@@ -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}` */
|
|
@@ -829,7 +846,10 @@ class DataGrid {
|
|
|
829
846
|
this.#entry_count = this.#entries.length;
|
|
830
847
|
// Число строк для скринридера: элементы списка плюс строка заголовка, а в серверном
|
|
831
848
|
// режиме — то, что сообщил сервер (иначе он знал бы только про загруженное окно)
|
|
832
|
-
this.#root.setAttribute(
|
|
849
|
+
this.#root.setAttribute(
|
|
850
|
+
"aria-rowcount",
|
|
851
|
+
String((this.#server_total === undefined ? this.#entry_count : this.#server_total) + 1)
|
|
852
|
+
);
|
|
833
853
|
this.#syncColumns();
|
|
834
854
|
this.#list.setItems(this.#entries);
|
|
835
855
|
this.#list.setHasMore(this.#has_more);
|
|
@@ -860,9 +880,11 @@ class DataGrid {
|
|
|
860
880
|
if (this.#data_source !== undefined) return rows.slice();
|
|
861
881
|
const text = this.#filter.trim().toLowerCase();
|
|
862
882
|
if (text === "" || this.#filter_keys.length === 0) return rows.slice();
|
|
863
|
-
return rows.filter((row) =>
|
|
864
|
-
|
|
865
|
-
|
|
883
|
+
return rows.filter((row) =>
|
|
884
|
+
this.#filter_keys.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,7 +917,7 @@ 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
|
/** Группы: порядок — первое появление ключа, поэтому он не зависит от сортировки */
|
|
@@ -903,9 +927,7 @@ class DataGrid {
|
|
|
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
|
}
|
|
@@ -915,7 +937,7 @@ class DataGrid {
|
|
|
915
937
|
key: key,
|
|
916
938
|
rows: group_rows,
|
|
917
939
|
aggregates: this.#aggregateValues(group_rows),
|
|
918
|
-
collapsed: this.#collapsed.has(key) ? this.#collapsed.get(key) : this.#group_collapsed_default
|
|
940
|
+
collapsed: this.#collapsed.has(key) ? this.#collapsed.get(key) : this.#group_collapsed_default,
|
|
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;
|
|
@@ -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
|
}
|
|
@@ -1041,10 +1070,15 @@ 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
1083
|
if (this.#column_reorder) {
|
|
1050
1084
|
cell.setAttribute("draggable", "true");
|
|
@@ -1061,14 +1095,18 @@ 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
|
}
|
|
@@ -1114,9 +1152,10 @@ 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
|
|
|
@@ -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
|
|
|
@@ -1165,7 +1204,8 @@ class DataGrid {
|
|
|
1165
1204
|
// строка теряла бы подсветку при включённом `rowClass`
|
|
1166
1205
|
if (typeof this.#row_class === "function") {
|
|
1167
1206
|
const classes = this.#row_class(row, entry.rowIndex);
|
|
1168
|
-
if (classes !== undefined && classes !== null && String(classes).trim() !== "")
|
|
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.#group_label === "function"
|
|
1283
|
+
? String(this.#group_label(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++) {
|
|
@@ -1274,8 +1324,8 @@ 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
|
/** Ключ строки: имя поля, функция или место в исходных данных */
|
|
@@ -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,14 +1373,17 @@ 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
1380
|
if (this.#on_selection_change !== undefined) {
|
|
1329
|
-
const rows = this.#viewRows().filter((row) =>
|
|
1381
|
+
const rows = this.#viewRows().filter((row) =>
|
|
1382
|
+
this.#selected.has(this.#keyOf(DataGrid.#entryOf(row, this.#rowIndexOf(row))))
|
|
1383
|
+
);
|
|
1330
1384
|
this.#on_selection_change(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
|
// === АКТИВНАЯ ЯЧЕЙКА И КЛАВИАТУРА ===
|
|
@@ -1340,13 +1394,13 @@ class DataGrid {
|
|
|
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});
|
|
1403
|
+
if (node !== undefined && focus) node.focus({ preventScroll: true });
|
|
1350
1404
|
if (this.#on_active_cell_change !== undefined && entry !== undefined && entry.kind === "row") {
|
|
1351
1405
|
this.#on_active_cell_change(place, known, entry.row);
|
|
1352
1406
|
}
|
|
@@ -1377,11 +1431,12 @@ class DataGrid {
|
|
|
1377
1431
|
if (next_row < 0 || next_row >= this.#entry_count) return undefined;
|
|
1378
1432
|
const entry = this.#entryAt(next_row);
|
|
1379
1433
|
if (entry === undefined || entry.kind !== "row") return undefined;
|
|
1380
|
-
return {row: next_row, col: visible[next].index};
|
|
1434
|
+
return { row: next_row, 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);
|
|
@@ -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
1730
|
if (this.#on_cell_edit !== undefined) this.#on_cell_edit(entry.row, editing.key, value, editing.old_value);
|
|
1673
|
-
this.#announce(I18n.t("dataGrid.saved", {title: column.title, value: DataGrid.#valueText(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
|
// === ЗАКРЕПЛЁННЫЕ КОЛОНКИ И ГОРИЗОНТАЛЬНАЯ ПРОКРУТКА ===
|
|
@@ -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,7 +1803,8 @@ class DataGrid {
|
|
|
1743
1803
|
|
|
1744
1804
|
#onHeaderDragEnd = () => {
|
|
1745
1805
|
this.#markDrop(undefined);
|
|
1746
|
-
for (const cell of this.#head_row.querySelectorAll("data_grid_cell"))
|
|
1806
|
+
for (const cell of this.#head_row.querySelectorAll("data_grid_cell"))
|
|
1807
|
+
cell.classList.remove("data_grid_cell_dragging");
|
|
1747
1808
|
this.#drag = undefined;
|
|
1748
1809
|
};
|
|
1749
1810
|
|
|
@@ -1758,7 +1819,8 @@ class DataGrid {
|
|
|
1758
1819
|
}
|
|
1759
1820
|
|
|
1760
1821
|
#markDrop(slot = undefined) {
|
|
1761
|
-
for (const cell of this.#head_row.querySelectorAll("data_grid_cell"))
|
|
1822
|
+
for (const cell of this.#head_row.querySelectorAll("data_grid_cell"))
|
|
1823
|
+
cell.classList.remove("data_grid_cell_drop_after");
|
|
1762
1824
|
if (slot === undefined) return;
|
|
1763
1825
|
const cells = Array.from(this.#head_row.querySelectorAll("data_grid_cell[data-column]"));
|
|
1764
1826
|
const target = cells[slot === 0 ? 0 : slot - 1];
|
|
@@ -1776,7 +1838,8 @@ 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();
|
|
@@ -1803,8 +1866,11 @@ class DataGrid {
|
|
|
1803
1866
|
const to = this.#columns.indexOf(column);
|
|
1804
1867
|
const node = this.#head_row.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.#on_column_order_change !== undefined)
|
|
1873
|
+
this.#on_column_order_change(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");
|
|
@@ -1825,10 +1891,15 @@ class DataGrid {
|
|
|
1825
1891
|
this.#refreshChooser();
|
|
1826
1892
|
return this.#chooser_dialog;
|
|
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()}));
|
|
1902
|
+
dialog.addToFooter(new Button({ title: I18n.t("dataGrid.showAll"), clickEvent: () => this.showAllColumns() }));
|
|
1832
1903
|
this.#chooser_dialog = dialog;
|
|
1833
1904
|
this.#chooser_block = block;
|
|
1834
1905
|
document.body.appendChild(dialog.set());
|
|
@@ -1843,7 +1914,7 @@ class DataGrid {
|
|
|
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));
|
|
@@ -1858,7 +1929,7 @@ class DataGrid {
|
|
|
1858
1929
|
limit: this.#page_size,
|
|
1859
1930
|
filter: this.#filter,
|
|
1860
1931
|
filterKeys: this.#filter_keys,
|
|
1861
|
-
sort: {key: this.#sort.key, direction: this.#sort.direction}
|
|
1932
|
+
sort: { key: this.#sort.key, direction: this.#sort.direction },
|
|
1862
1933
|
};
|
|
1863
1934
|
}
|
|
1864
1935
|
|
|
@@ -1879,18 +1950,21 @@ class DataGrid {
|
|
|
1879
1950
|
this.#has_more = total === undefined ? rows.length >= this.#page_size : this.#data.length < total;
|
|
1880
1951
|
this.#reindex();
|
|
1881
1952
|
this.#recompute();
|
|
1882
|
-
this.#announce(
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
+
);
|
|
1885
1958
|
if (this.#on_data_load !== undefined) this.#on_data_load(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.#
|
|
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 }));
|
|
1894
1968
|
if (this.#on_data_error !== undefined) this.#on_data_error(error, this.#requestParams(this.#data.length));
|
|
1895
1969
|
} finally {
|
|
1896
1970
|
if (request === this.#request) {
|
|
@@ -1906,9 +1980,9 @@ class DataGrid {
|
|
|
1906
1980
|
this.#error.hidden = broken === false;
|
|
1907
1981
|
this.#error_text.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.#empty_text, 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
|
}
|
|
@@ -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.#entry_count;
|
|
2104
|
+
}
|
|
2105
|
+
getRowHeight() {
|
|
2106
|
+
return this.#row_height;
|
|
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.#data_source !== undefined;
|
|
2140
|
+
}
|
|
2141
|
+
isLoading() {
|
|
2142
|
+
return this.#loading;
|
|
2143
|
+
}
|
|
2144
|
+
getServerTotal() {
|
|
2145
|
+
return this.#server_total;
|
|
2146
|
+
}
|
|
2147
|
+
getPageSize() {
|
|
2148
|
+
return this.#page_size;
|
|
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.#column_reorder;
|
|
2182
|
+
}
|
|
2183
|
+
getColumnOrder() {
|
|
2184
|
+
return this.#columns.map((column) => column.key);
|
|
2185
|
+
}
|
|
2186
|
+
getGroupBy() {
|
|
2187
|
+
return this.#group_by;
|
|
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.#group_label === "function"
|
|
2195
|
+
? String(this.#group_label(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()) {
|
|
@@ -2140,7 +2295,10 @@ class DataGrid {
|
|
|
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,7 +2309,7 @@ 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};
|
|
2312
|
+
this.#sort = { key: known, direction: known === undefined ? DataGrid.SORT.NONE : direction };
|
|
2155
2313
|
if (this.#data_source !== undefined) return this.reload();
|
|
2156
2314
|
this.#applySort();
|
|
2157
2315
|
return this;
|
|
@@ -2174,7 +2332,8 @@ class DataGrid {
|
|
|
2174
2332
|
|
|
2175
2333
|
setContextMenu(ctx = undefined) {
|
|
2176
2334
|
if (this.#context_menu === ctx) return this;
|
|
2177
|
-
if (this.#context_menu !== undefined && typeof this.#context_menu.detach === "function")
|
|
2335
|
+
if (this.#context_menu !== undefined && typeof this.#context_menu.detach === "function")
|
|
2336
|
+
this.#context_menu.detach();
|
|
2178
2337
|
this.#context_menu = ctx;
|
|
2179
2338
|
if (ctx !== undefined && typeof ctx.attach === "function") ctx.attach(this.#root);
|
|
2180
2339
|
return this;
|
|
@@ -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();
|
|
@@ -2334,7 +2495,8 @@ class DataGrid {
|
|
|
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.#context_menu !== undefined && typeof this.#context_menu.detach === "function")
|
|
2498
|
+
if (this.#context_menu !== undefined && typeof this.#context_menu.detach === "function")
|
|
2499
|
+
this.#context_menu.detach();
|
|
2338
2500
|
this.#list.destroy();
|
|
2339
2501
|
if (this.#chooser_dialog !== undefined) {
|
|
2340
2502
|
// У диалога нет своего «удали себя»: узел снимаем с документа сами
|