@rt-tools/ui-kit-v2 0.12.0 → 0.13.1
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/fesm2022/rt-tools-ui-kit-v2.mjs +1640 -1572
- package/fesm2022/rt-tools-ui-kit-v2.mjs.map +1 -1
- package/package.json +1 -1
- package/rt-tools-ui-kit-v2-0.13.1.tgz +0 -0
- package/src/lib/components/CONTEXT.md +16 -16
- package/src/lib/components/accordion/CONTEXT.md +31 -0
- package/src/lib/components/accordion/rt-accordion.component.scss +61 -0
- package/src/lib/components/checkbox/rt-checkbox.component.scss +6 -0
- package/types/rt-tools-ui-kit-v2.d.ts +28 -2
- package/rt-tools-ui-kit-v2-0.12.0.tgz +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { DOCUMENT, NgTemplateOutlet, TitleCasePipe, DecimalPipe, formatDate } from '@angular/common';
|
|
2
1
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { InjectionToken,
|
|
2
|
+
import { InjectionToken, inject, Injectable, ElementRef, signal, computed, input, booleanAttribute, numberAttribute, effect, afterNextRender, ViewEncapsulation, ChangeDetectionStrategy, Component, makeEnvironmentProviders, linkedSignal, Pipe, Renderer2, DestroyRef, Directive, output, Injector, untracked, HostBinding, ViewContainerRef, RendererFactory2, viewChild, afterRenderEffect, forwardRef, contentChild, TemplateRef, isSignal, LOCALE_ID, HostListener, contentChildren, ViewChild, model } from '@angular/core';
|
|
4
3
|
import { PlatformService, BlockDirective, ElemDirective, ModDirective, setRtComponentInputs, StorageService, WINDOW, BreakStringPipe, EmptyToDashPipe, ConcatClassesPipe, IDBStorageService } from '@rt-tools/core';
|
|
4
|
+
import { DOCUMENT, NgTemplateOutlet, TitleCasePipe, DecimalPipe, formatDate } from '@angular/common';
|
|
5
5
|
import { HttpClient } from '@angular/common/http';
|
|
6
6
|
import { takeUntilDestroyed, outputToObservable, toSignal, toObservable } from '@angular/core/rxjs-interop';
|
|
7
7
|
import { Subject, mergeMap, EMPTY, tap, map, catchError, merge, takeUntil, switchMap, filter, of, finalize, shareReplay, take, startWith, exhaustMap, defer, timer, distinctUntilChanged, debounce as debounce$1, fromEvent, debounceTime } from 'rxjs';
|
|
@@ -21,1568 +21,107 @@ import { DomSanitizer } from '@angular/platform-browser';
|
|
|
21
21
|
import { CdkTable, CdkColumnDef, CdkCell, CdkCellDef, CdkHeaderCell, CdkHeaderCellDef, DataRowOutlet, FooterRowOutlet, HeaderRowOutlet, NoDataRowOutlet, CDK_TABLE } from '@angular/cdk/table';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* иконок для скринридера. Своего языка у него нет и быть не может — его знает
|
|
28
|
-
* приложение и отдаёт функцией-переводчиком (`provideRtKitLabels`). Этот набор
|
|
29
|
-
* стоит под ней умолчанием, чтобы кит без единой настройки не рисовал пустоты:
|
|
30
|
-
* среди ключей много `aria`-подписей, и пустая там означает кнопку без имени
|
|
31
|
-
* для скринридера.
|
|
32
|
-
*
|
|
33
|
-
* Набор задаёт и **перечень ключей**: `TRtKitLabelKey` выводится отсюда, поэтому
|
|
34
|
-
* опечатка в ключе не доживает до рантайма.
|
|
35
|
-
*
|
|
36
|
-
* Места вида `{{name}}` заполняются параметрами — их принимает `rtKitLabel()`.
|
|
37
|
-
*/
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/typedef -- аннотация стёрла бы литеральный тип, на котором стоит TRtKitLabelKey
|
|
39
|
-
const RT_KIT_LABELS_EN = {
|
|
40
|
-
bottomSheetClose: 'Close',
|
|
41
|
-
chatAttachAria: 'Attach a file',
|
|
42
|
-
chatAttachTooltip: 'Attach a file',
|
|
43
|
-
chatAttachmentFallback: 'Attachment',
|
|
44
|
-
chatCollapse: 'Collapse chat',
|
|
45
|
-
chatComposerAria: 'Message input',
|
|
46
|
-
chatDeleteAria: 'Delete message',
|
|
47
|
-
chatDeleteTooltip: 'Delete',
|
|
48
|
-
chatDropOverlay: 'Drop the files to attach them to your message',
|
|
49
|
-
chatEmptyHint: 'Chat is not available yet.',
|
|
50
|
-
chatEmptyThread: 'No messages yet.',
|
|
51
|
-
chatExpand: 'Expand chat to full screen',
|
|
52
|
-
chatMessageDeleted: 'Message deleted',
|
|
53
|
-
chatPlaceholder: 'Type a message',
|
|
54
|
-
chatRefreshAria: 'Refresh messages',
|
|
55
|
-
chatRefreshTooltip: 'Refresh messages',
|
|
56
|
-
chatRetryAria: 'Send again',
|
|
57
|
-
chatRetryTooltip: 'Retry',
|
|
58
|
-
chatSendAria: 'Send message',
|
|
59
|
-
chatSendLabel: 'Send',
|
|
60
|
-
chatStatusFailed: 'Not sent',
|
|
61
|
-
chatStatusRead: 'Read',
|
|
62
|
-
chatStatusSending: 'Sending',
|
|
63
|
-
chatStatusSent: 'Delivered',
|
|
64
|
-
chatTitle: 'Chat',
|
|
65
|
-
counterDecrease: 'Decrease',
|
|
66
|
-
counterIncrease: 'Increase',
|
|
67
|
-
dataListClearFilters: 'Clear filters',
|
|
68
|
-
dataListHorizontalScrollbar: 'Horizontal scrollbar shown',
|
|
69
|
-
dataListPlaceholder: 'No Data Found',
|
|
70
|
-
dataListRefresh: 'Refresh',
|
|
71
|
-
dataListSearchPlaceholder: 'Search...',
|
|
72
|
-
dataListSelectAll: 'Select all',
|
|
73
|
-
dataListSelected: 'Selected: {{count}}',
|
|
74
|
-
dataListSettingsHint: 'Change columns visibility and order, show(hide) scrollbar',
|
|
75
|
-
dataListSettingsTitle: 'Edit table configuration',
|
|
76
|
-
dataListTableConfig: 'Table configuration',
|
|
77
|
-
dataListVerticalScrollbar: 'Vertical scrollbar shown',
|
|
78
|
-
dataTableActions: 'Actions',
|
|
79
|
-
dataTableCopied: 'Copied!',
|
|
80
|
-
dataTableCopy: 'Copy',
|
|
81
|
-
dataTableFilterClear: 'Clear filter',
|
|
82
|
-
dataTableFilterOperatorContains: 'Contains',
|
|
83
|
-
dataTableFilterOperatorEquals: 'Equal',
|
|
84
|
-
dataTableFilterOperatorGreaterThan: 'More than',
|
|
85
|
-
dataTableFilterOperatorLessThan: 'Less than',
|
|
86
|
-
dataTableFilterOperatorNotEquals: 'Not equal',
|
|
87
|
-
dataTableFilterSelectPlaceholder: 'Chose filter',
|
|
88
|
-
dataTableFilterValuePlaceholder: 'Type in filter value',
|
|
89
|
-
dataTableSelectPage: 'Select the rows of this page',
|
|
90
|
-
dataTableSelectRow: 'Select row',
|
|
91
|
-
fieldErrorEmail: 'Invalid email',
|
|
92
|
-
fieldErrorMax: 'Value is too large',
|
|
93
|
-
fieldErrorMaxLength: 'Value is too long',
|
|
94
|
-
fieldErrorMin: 'Value is too small',
|
|
95
|
-
fieldErrorMinLength: 'Value is too short',
|
|
96
|
-
fieldErrorPattern: 'Invalid format',
|
|
97
|
-
fieldErrorRequired: 'Required field',
|
|
98
|
-
photoViewerClose: 'Close viewer',
|
|
99
|
-
photoViewerNext: 'Next photo',
|
|
100
|
-
photoViewerPrev: 'Previous photo',
|
|
101
|
-
themeToggleLabel: 'Switch theme',
|
|
102
|
-
uiActions: 'Actions',
|
|
103
|
-
uiAuditEmptyHint: 'Deal events will appear here — stage changes and other actions.',
|
|
104
|
-
uiBack: 'Back',
|
|
105
|
-
uiBackToList: 'Back to list',
|
|
106
|
-
uiBaseline: 'was {{value}}',
|
|
107
|
-
uiCancel: 'Cancel',
|
|
108
|
-
uiChangeAgent: 'Change agent',
|
|
109
|
-
uiChooseFile: 'Choose file',
|
|
110
|
-
uiClear: 'Clear',
|
|
111
|
-
uiClearFilters: 'Clear filters',
|
|
112
|
-
uiClose: 'Close',
|
|
113
|
-
uiCollapse: 'Collapse',
|
|
114
|
-
uiColumnLocked: 'Column is pinned',
|
|
115
|
-
uiColumnSettings: 'Column settings',
|
|
116
|
-
uiColumnSettingsTitle: 'Configure columns',
|
|
117
|
-
uiCommentOptional: 'Comment (optional)',
|
|
118
|
-
uiConfirm: 'Confirm',
|
|
119
|
-
uiConfirmAction: 'Confirm action',
|
|
120
|
-
uiContinue: 'Continue',
|
|
121
|
-
uiCopied: 'Copied',
|
|
122
|
-
uiCopy: 'Copy',
|
|
123
|
-
uiCurrentStage: 'Current stage',
|
|
124
|
-
uiDetails: 'Details',
|
|
125
|
-
uiDiscardChanges: 'Discard Changes',
|
|
126
|
-
uiDownload: 'Download',
|
|
127
|
-
uiDownloadFile: 'Download: {{name}}',
|
|
128
|
-
uiDragColumn: 'Drag column',
|
|
129
|
-
uiFileDropOverlay: 'Drop the files to attach them',
|
|
130
|
-
uiFilterChoose: 'Choose',
|
|
131
|
-
uiFilterContains: 'Contains',
|
|
132
|
-
uiFilterEndsWith: 'Ends with',
|
|
133
|
-
uiFilterEquals: 'Equals',
|
|
134
|
-
uiFilterGreaterThan: 'Greater than',
|
|
135
|
-
uiFilterLessThan: 'Less than',
|
|
136
|
-
uiFilterNotEquals: 'Does not equal',
|
|
137
|
-
uiFilterStartsWith: 'Starts with',
|
|
138
|
-
uiFilterValue: 'Filter value',
|
|
139
|
-
uiFilters: 'Filters',
|
|
140
|
-
uiForward: 'Forward',
|
|
141
|
-
uiHide: 'Hide',
|
|
142
|
-
uiHideColumn: 'Hide column',
|
|
143
|
-
uiHidePassword: 'Hide password',
|
|
144
|
-
uiHint: 'Hint',
|
|
145
|
-
uiHistory: 'History',
|
|
146
|
-
uiInviteUser: 'Invite user',
|
|
147
|
-
uiItemsPerPage: 'Items per page',
|
|
148
|
-
uiLiveOnSite: 'On the site now',
|
|
149
|
-
uiLoadMore: 'Load more',
|
|
150
|
-
uiMainNav: 'Main navigation',
|
|
151
|
-
uiMore: 'More',
|
|
152
|
-
uiMoreActions: 'More actions',
|
|
153
|
-
uiMoveStage: 'Move',
|
|
154
|
-
uiMoveStageAria: 'Move stage',
|
|
155
|
-
uiNavMenu: 'Navigation menu',
|
|
156
|
-
uiNavUnread: 'Unread',
|
|
157
|
-
uiNextPage: 'Next page',
|
|
158
|
-
uiNightGridAria: 'Occupancy grid',
|
|
159
|
-
uiNoData: 'No data yet',
|
|
160
|
-
uiNoOptions: 'No options',
|
|
161
|
-
uiNoRows: 'No data',
|
|
162
|
-
uiNothingFound: 'Nothing found',
|
|
163
|
-
uiNotifications: 'Notifications',
|
|
164
|
-
uiPageOf: 'Page {{page}} of {{last}}',
|
|
165
|
-
uiPagination: 'Pagination',
|
|
166
|
-
uiPerPage: 'Items per page:',
|
|
167
|
-
uiPickAgent: 'Select an agent',
|
|
168
|
-
uiPickAnotherAgent: 'Select a different agent',
|
|
169
|
-
uiPickTransition: 'Select a transition',
|
|
170
|
-
uiPrevPage: 'Previous page',
|
|
171
|
-
uiProfile: 'Profile',
|
|
172
|
-
uiRangeEmpty: '0 of 0',
|
|
173
|
-
uiRangeOf: '{{from}}–{{to}} of {{total}}',
|
|
174
|
-
uiRemove: 'Delete',
|
|
175
|
-
uiRename: 'Rename',
|
|
176
|
-
uiReplace: 'Replace',
|
|
177
|
-
uiReplaceAgentTitle: 'Replace agent',
|
|
178
|
-
uiResetDefaults: 'Reset to defaults',
|
|
179
|
-
uiResizeDetails: 'Resize details pane',
|
|
180
|
-
uiResizeList: 'Resize list pane',
|
|
181
|
-
uiRefresh: 'Refresh',
|
|
182
|
-
uiRevert: 'Revert',
|
|
183
|
-
uiSave: 'Save',
|
|
184
|
-
uiSaveNewAgent: 'Save the new agent',
|
|
185
|
-
uiScrollDown: 'Scroll down',
|
|
186
|
-
uiSearch: 'Search',
|
|
187
|
-
uiSectionInProgress: '{{label}} — section under development',
|
|
188
|
-
uiSelectAll: 'Select all',
|
|
189
|
-
uiSelectValues: 'Select values',
|
|
190
|
-
uiSelectedCount: 'Selected: {{count}}',
|
|
191
|
-
uiSelectedOf: 'Selected: {{count}} of {{total}}',
|
|
192
|
-
uiShow: 'Show',
|
|
193
|
-
uiShowColumn: 'Show column',
|
|
194
|
-
uiShowDetails: 'Show details',
|
|
195
|
-
uiShowPassword: 'Show password',
|
|
196
|
-
uiStageNoteHint: 'Internal note — the client will not see it.',
|
|
197
|
-
uiStageTransition: 'Stage transition',
|
|
198
|
-
uiSubscribeNotifications: 'Subscribe to notifications',
|
|
199
|
-
uiTabInvalid: 'This tab contains errors',
|
|
200
|
-
uiTargetStage: 'Target stage',
|
|
201
|
-
uiTargetStageRequired: 'Select a target stage.',
|
|
202
|
-
uiThreadListEmptyHint: 'New threads will appear here.',
|
|
203
|
-
uiToList: 'To list',
|
|
204
|
-
uiUnsavedDiscard: 'Close without saving',
|
|
205
|
-
uiUnsavedMessage: 'The panel has edits that have not been saved yet. Choose what to do with them.',
|
|
206
|
-
uiUnsavedSave: 'Save and close',
|
|
207
|
-
uiUnsavedStay: 'Stay',
|
|
208
|
-
uiUnsavedTitle: 'Unsaved changes',
|
|
209
|
-
uiWelcomeAria: 'Welcome message for a new user',
|
|
210
|
-
uiYearOverYear: 'YoY',
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
/** Места вида `{{name}}` — их заполняет `interpolate`. */
|
|
214
|
-
const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}/g;
|
|
215
|
-
/**
|
|
216
|
-
* Подставляет параметры в подпись.
|
|
24
|
+
* Категории иконок rt-icon. 11 buckets: 8 prefix-based + Social (префикс `social-`) + Custom
|
|
25
|
+
* (префикс `ico-`) + Misc (fallback).
|
|
26
|
+
* Порядок фиксирован — соответствует порядку отображения в Catalog story (rt-icon.stories.ts).
|
|
217
27
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* параметра не дали, остаётся как есть: пустота на его месте прочиталась бы как
|
|
221
|
-
* законченная фраза, а `{{name}}` виден и чинится.
|
|
28
|
+
* Аннотация типа намеренно не указывается: явный `readonly string[]` потерял бы узкий
|
|
29
|
+
* литеральный тип, нужный для {@link TIconCategory}. `as const` сохраняет литеральные ключи.
|
|
222
30
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/typedef -- аннотация стёрла бы литеральный тип, на котором стоит TIconCategory
|
|
32
|
+
const CATEGORY_ORDER = [
|
|
33
|
+
'Navigation',
|
|
34
|
+
'Alignment & Sorting',
|
|
35
|
+
'Status',
|
|
36
|
+
'Time & Calendar',
|
|
37
|
+
'Communication',
|
|
38
|
+
'Documents',
|
|
39
|
+
'People',
|
|
40
|
+
'Commerce',
|
|
41
|
+
'Social',
|
|
42
|
+
'Custom',
|
|
43
|
+
'Misc',
|
|
44
|
+
];
|
|
45
|
+
function startsWithAny(name, prefixes) {
|
|
46
|
+
return prefixes.some((p) => name.startsWith(p));
|
|
47
|
+
}
|
|
48
|
+
function isOneOf(name, names) {
|
|
49
|
+
return names.includes(name);
|
|
50
|
+
}
|
|
51
|
+
function matchPrefixOrExact(prefixes, exact) {
|
|
52
|
+
return (name) => startsWithAny(name, prefixes) || isOneOf(name, exact);
|
|
228
53
|
}
|
|
229
|
-
/** Английское умолчание: кит без единой настройки рисует текст, а не пустоты. */
|
|
230
|
-
const defaultTranslator = (key, params) => interpolate(RT_KIT_LABELS_EN[key], params);
|
|
231
|
-
/**
|
|
232
|
-
* Функция, которой кит получает подписи. Её отдаёт приложение —
|
|
233
|
-
* `provideRtKitLabels()`.
|
|
234
|
-
*
|
|
235
|
-
* Токен объявлен **сигналом**, а не просто функцией: приложение, меняющее язык
|
|
236
|
-
* на ходу, кладёт в него новую функцию, и всё, что от неё считано,
|
|
237
|
-
* пересчитывается само.
|
|
238
|
-
*/
|
|
239
|
-
const RT_KIT_TRANSLATOR = new InjectionToken('RT_KIT_TRANSLATOR', {
|
|
240
|
-
providedIn: 'root',
|
|
241
|
-
factory: () => signal(defaultTranslator).asReadonly(),
|
|
242
|
-
});
|
|
243
54
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* `Missing locale data`, и кит за это не отвечает.
|
|
55
|
+
* Упорядоченный список правил категоризации.
|
|
56
|
+
* `Custom` — единственная категория по строгому префиксу `ico-` и проверяется первой,
|
|
57
|
+
* чтобы исключить конфликты с другими prefix-based категориями.
|
|
58
|
+
* `Misc` — fallback в {@link categoryOf}, отдельным правилом не описывается.
|
|
249
59
|
*/
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
* строится один раз на приложение (`providedIn: 'root'`) и пересчитывается,
|
|
259
|
-
* только когда приложение сменило функцию-переводчик.
|
|
260
|
-
*
|
|
261
|
-
* Подпись, которой переводчик не дал (вернул пустое или упал), берётся
|
|
262
|
-
* английской: пустая `aria`-подпись означает кнопку без имени для скринридера.
|
|
263
|
-
*/
|
|
264
|
-
const RT_KIT_LABELS = new InjectionToken('RT_KIT_LABELS', {
|
|
265
|
-
providedIn: 'root',
|
|
266
|
-
factory: () => {
|
|
267
|
-
const translator = inject(RT_KIT_TRANSLATOR);
|
|
268
|
-
return computed(() => {
|
|
269
|
-
const translate = translator();
|
|
270
|
-
const map = {};
|
|
271
|
-
for (const key of Object.keys(RT_KIT_LABELS_EN)) {
|
|
272
|
-
map[key] = translate(key) || RT_KIT_LABELS_EN[key];
|
|
273
|
-
}
|
|
274
|
-
return map;
|
|
275
|
-
});
|
|
60
|
+
const ICON_CATEGORY_RULES = [
|
|
61
|
+
// Знаки соцсетей — цветные, со своей заливкой в файле; в каталоге они стоят своим рядом,
|
|
62
|
+
// чтобы не смешиваться с одноцветными, которые красит тема.
|
|
63
|
+
{ category: 'Social', match: (n) => n.startsWith('social-') },
|
|
64
|
+
{ category: 'Custom', match: (n) => n.startsWith('ico-') },
|
|
65
|
+
{
|
|
66
|
+
category: 'Navigation',
|
|
67
|
+
match: matchPrefixOrExact(['arrow', 'arrows', 'angle', 'chevron', 'directions'], []),
|
|
276
68
|
},
|
|
277
|
-
});
|
|
278
|
-
/**
|
|
279
|
-
* Одна подпись, а не вся карта.
|
|
280
|
-
*
|
|
281
|
-
* Карте не годятся две вещи: подпись с подстановками (параметры приходят из
|
|
282
|
-
* состояния компонента) и подпись, ключ которой выбирается на ходу — кнопка
|
|
283
|
-
* «свернуть ↔ развернуть», текст ошибки поля по сработавшему валидатору. Обе
|
|
284
|
-
* берутся отсюда. Зовётся в поле класса, в инъекционном контексте.
|
|
285
|
-
*
|
|
286
|
-
* ```typescript
|
|
287
|
-
* readonly #range: Signal<string> = rtKitLabel('uiRangeOf', this.#rangeParams);
|
|
288
|
-
* readonly #expand: Signal<string> = rtKitLabel(computed(() => (this.expanded() ? 'chatCollapse' : 'chatExpand')));
|
|
289
|
-
* ```
|
|
290
|
-
*
|
|
291
|
-
* @param key Ключ подписи — значением или сигналом, если он меняется. Пустой
|
|
292
|
-
* ключ даёт пустую строку: у поля без сработавшего валидатора подписи нет, и
|
|
293
|
-
* умолчание тут показало бы ошибку там, где её не случилось.
|
|
294
|
-
* @param params Подстановки — значением или сигналом, если они меняются.
|
|
295
|
-
*/
|
|
296
|
-
function rtKitLabel(key, params) {
|
|
297
|
-
const translator = inject(RT_KIT_TRANSLATOR);
|
|
298
|
-
return computed(() => {
|
|
299
|
-
const resolvedKey = typeof key === 'function' ? key() : key;
|
|
300
|
-
if (resolvedKey === '') {
|
|
301
|
-
return '';
|
|
302
|
-
}
|
|
303
|
-
const resolved = typeof params === 'function' ? params() : params;
|
|
304
|
-
return translator()(resolvedKey, resolved) || defaultTranslator(resolvedKey, resolved);
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Отдаёт киту подписи приложения.
|
|
309
|
-
*
|
|
310
|
-
* Без него кит работает на английском умолчании — оно вшито и покрывает все
|
|
311
|
-
* ключи, поэтому пустой кнопки на экране не бывает никогда.
|
|
312
|
-
*
|
|
313
|
-
* ```typescript
|
|
314
|
-
* export const appConfig: ApplicationConfig = {
|
|
315
|
-
* providers: [
|
|
316
|
-
* provideRtKitLabels({
|
|
317
|
-
* translator: computed(() => (key, params) => transloco.translate(`rtKit.${key}`, params)),
|
|
318
|
-
* locale: toSignal(transloco.langChanges$, { initialValue: transloco.getActiveLang() }),
|
|
319
|
-
* }),
|
|
320
|
-
* ],
|
|
321
|
-
* };
|
|
322
|
-
* ```
|
|
323
|
-
*
|
|
324
|
-
* Способ доставки — дело приложения: Transloco, `$localize`, собственный
|
|
325
|
-
* словарь. Кит знает только ключи и то, что на них отвечают строкой.
|
|
326
|
-
*
|
|
327
|
-
* @param source Функция-переводчик и активная локаль. Локаль необязательна —
|
|
328
|
-
* без неё кит форматирует даты по-английски.
|
|
329
|
-
*/
|
|
330
|
-
function provideRtKitLabels(source) {
|
|
331
|
-
const providers = [{ provide: RT_KIT_TRANSLATOR, useValue: source.translator }];
|
|
332
|
-
if (source.locale !== undefined) {
|
|
333
|
-
providers.push({ provide: RT_KIT_LOCALE, useValue: source.locale });
|
|
334
|
-
}
|
|
335
|
-
return makeEnvironmentProviders(providers);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Подпись кита по ключу — для значений, приходящих из контекста шаблона.
|
|
340
|
-
*
|
|
341
|
-
* Шаблон не зовёт методов: имя раздела приходит элементом цикла, и посчитать подпись заранее
|
|
342
|
-
* нечем. Пайп чистый, поэтому функция-переводчик передаётся ему вторым доводом — сменившись,
|
|
343
|
-
* она пересчитывает подпись сама.
|
|
344
|
-
*/
|
|
345
|
-
class RtKitLabelPipe {
|
|
346
|
-
transform(key, translator, params) {
|
|
347
|
-
return translator(key, params);
|
|
348
|
-
}
|
|
349
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
350
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, isStandalone: true, name: "rtKitLabel" }); }
|
|
351
|
-
}
|
|
352
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, decorators: [{
|
|
353
|
-
type: Pipe,
|
|
354
|
-
args: [{
|
|
355
|
-
name: 'rtKitLabel',
|
|
356
|
-
pure: true,
|
|
357
|
-
}]
|
|
358
|
-
}] });
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Соответствие значков первого кита значкам этого набора.
|
|
362
|
-
*
|
|
363
|
-
* Первый кит рисует значки шрифтом Material: имя стоит прямо в шаблоне. Здесь у каждого такого
|
|
364
|
-
* имени ровно одна пара — иначе один и тот же смысл приезжает на разные экраны разными
|
|
365
|
-
* рисунками, а переезжающий выбирает значок заново на каждом экране.
|
|
366
|
-
*
|
|
367
|
-
* Перечень закрывает имена, которые видны поиском: голое слово в шаблоне, слово в тернарнике и
|
|
368
|
-
* слово внутри разбора по виду фильтра. Имена, подставляемые выражением, сюда не попадают: они
|
|
369
|
-
* приезжают из данных приложения и деревом кита не видны. Это граница перечня, а не его пробел.
|
|
370
|
-
*
|
|
371
|
-
* Пара ведёт на имя из фрейма, а не на своё с приставкой `ico-`, там где есть оба. Имён из
|
|
372
|
-
* фрейма 229 против 118, и они одной рисовки: смешанный набор дал бы разнобой на экране. Восемь
|
|
373
|
-
* таких пар помечены доводом — их можно переложить на свои одним местом.
|
|
374
|
-
*
|
|
375
|
-
* Имя без пары стоит здесь же, с причиной: молчание об имени неотличимо от того, что о нём
|
|
376
|
-
* забыли. Дорисовка недостающего — работа рисующего, и она вынесена за границу задачи.
|
|
377
|
-
*
|
|
378
|
-
* Держит перечень `tools/check-icon-map.mjs`: каждая пара ведёт на имя союза и на существующий
|
|
379
|
-
* файл, имена не повторяются, причина у имени без пары не пуста.
|
|
380
|
-
*/
|
|
381
|
-
/** Довод самой частой пары: имя первого кита и имя набора совпали после замены подчёркиваний. */
|
|
382
|
-
const SAME_NAME = 'совпадает именем';
|
|
383
|
-
const iconMaterialMap = [
|
|
384
|
-
{ from: 'add', to: 'ico-plus', why: 'простой плюс есть только в своей рисовке — во фрейме плюс лишь в круге и в составных' },
|
|
385
|
-
{ from: 'arrow_back', to: 'arrow-left', why: 'стрелка влево, один в один' },
|
|
386
|
-
{ from: 'arrow_forward', to: 'arrow-right', why: 'стрелка вправо, один в один' },
|
|
387
|
-
{ from: 'backup', to: 'cloud-upload', why: 'у Material backup — облако со стрелкой вверх, это оно и есть' },
|
|
388
|
-
{ from: 'block', to: 'ban', why: 'круг с перечёркиванием, один в один' },
|
|
389
|
-
{ from: 'chevron_left', to: 'chevron-left', why: SAME_NAME },
|
|
390
|
-
{ from: 'chevron_right', to: 'chevron-right', why: SAME_NAME },
|
|
391
|
-
{ from: 'close', to: 'close', why: `${SAME_NAME}; есть двойник ico-close` },
|
|
392
|
-
{ from: 'content_copy', to: 'copy', why: 'два листа, один в один; есть двойник ico-copy' },
|
|
393
|
-
{ from: 'delete', to: 'trash', why: 'корзина, один в один; есть двойник ico-trash' },
|
|
394
|
-
{ from: 'delete_forever', to: null, why: 'корзины с крестом в наборе нет — нужен рисунок' },
|
|
395
|
-
{ from: 'done', to: 'check', why: 'галочка, один в один; есть двойник ico-check' },
|
|
396
|
-
{ from: 'email', to: 'email', why: 'конверт, один в один' },
|
|
397
|
-
{ from: 'download', to: 'ico-download', why: 'простая стрелка вниз есть только в своей рисовке — во фрейме лишь облако' },
|
|
398
69
|
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
why: 'две горизонтальные полосы; рисунок дорисован по слову владельца — им таблица первого кита показывает сравнение «равно»',
|
|
70
|
+
category: 'Alignment & Sorting',
|
|
71
|
+
match: matchPrefixOrExact(['align', 'sort', 'bars', 'ellipsis', 'sliders', 'filter', 'th'], []),
|
|
402
72
|
},
|
|
403
|
-
{ from: 'edit', to: 'pencil', why: 'карандаш, один в один' },
|
|
404
|
-
{ from: 'info', to: 'info-circle', why: 'круг с буквой i — так рисует Material; голое info в наборе другое' },
|
|
405
|
-
{ from: 'keyboard_arrow_down', to: 'chevron-down', why: 'шеврон вниз, один в один; есть двойник ico-chevron-down' },
|
|
406
|
-
{ from: 'local_offer', to: 'tag', why: 'ярлык, один в один' },
|
|
407
|
-
{ from: 'menu', to: 'bars', why: 'три полосы, один в один' },
|
|
408
|
-
{ from: 'more_horiz', to: 'ellipsis-h', why: 'три точки по горизонтали' },
|
|
409
|
-
{ from: 'more_vert', to: 'ellipsis-v', why: 'три точки по вертикали' },
|
|
410
|
-
{ from: 'open_in_new', to: 'external-link', why: 'квадрат со стрелкой наружу' },
|
|
411
73
|
{
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
74
|
+
category: 'Status',
|
|
75
|
+
match: matchPrefixOrExact(['check', 'times', 'ban', 'exclamation', 'info', 'eye', 'lock', 'unlock', 'shield'], ['spinner', 'stop']),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
category: 'Time & Calendar',
|
|
79
|
+
match: matchPrefixOrExact(['alarm', 'calendar'], ['clock']),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
category: 'Communication',
|
|
83
|
+
match: matchPrefixOrExact(['comment', 'share'], ['bell', 'bot', 'telegram', 'at', 'email', 'send', 'phone', 'mobile']),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
category: 'Documents',
|
|
87
|
+
match: matchPrefixOrExact(['attach', 'file', 'folder', 'bookmark'], ['book', 'copy', 'clone', 'tag', 'tags']),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
category: 'People',
|
|
91
|
+
match: matchPrefixOrExact(['user'], ['users', 'id-card']),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
category: 'Commerce',
|
|
95
|
+
match: (n) => isOneOf(n, ['wallet', 'money-bill', 'ticket', 'credit-card']),
|
|
415
96
|
},
|
|
416
|
-
{ from: 'person', to: 'user', why: 'силуэт человека, один в один; есть двойник ico-user' },
|
|
417
|
-
{ from: 'person_off', to: 'user-off', why: 'силуэт человека, перечёркнутый наискось; рисунок дорисован по слову владельца' },
|
|
418
|
-
{ from: 'refresh', to: 'refresh', why: SAME_NAME },
|
|
419
|
-
{ from: 'search', to: 'search', why: `${SAME_NAME}; есть двойник ico-search` },
|
|
420
|
-
{ from: 'settings', to: 'cog', why: 'шестерёнка, один в один' },
|
|
421
|
-
{ from: 'star', to: 'star', why: SAME_NAME },
|
|
422
|
-
{ from: 'sync', to: 'sync', why: SAME_NAME },
|
|
423
|
-
{ from: 'view_column', to: 'table', why: 'ближний: у Material это колонки, здесь таблица — решение владельца' },
|
|
424
|
-
{ from: 'visibility', to: 'eye', why: 'глаз, один в один; есть двойник ico-eye' },
|
|
425
|
-
{ from: 'visibility_off', to: 'eye-slash', why: 'перечёркнутый глаз, один в один' },
|
|
426
97
|
];
|
|
427
98
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* Считается из перечня, а не пишется списком: свой список разошёлся бы с перечнем молча — реестр
|
|
431
|
-
* ходил бы за файлом, которого нет, или не ходил бы за лежащим.
|
|
99
|
+
* Возвращает категорию для имени иконки.
|
|
100
|
+
* Первое совпавшее правило в {@link ICON_CATEGORY_RULES} выигрывает; иначе — `Misc`.
|
|
432
101
|
*/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
102
|
+
function categoryOf(name) {
|
|
103
|
+
for (const rule of ICON_CATEGORY_RULES) {
|
|
104
|
+
if (rule.match(name)) {
|
|
105
|
+
return rule.category;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return 'Misc';
|
|
109
|
+
}
|
|
436
110
|
|
|
437
111
|
/**
|
|
438
|
-
*
|
|
112
|
+
* Имена иконок rt-icon. Набор лежит в `assets/icons` пакета, откуда сборка
|
|
113
|
+
* приложения кладёт его туда, куда указывает `provideRtIcons()`. Большинство
|
|
114
|
+
* файлов пришло из фрейма iconsLib
|
|
115
|
+
* (https://www.figma.com/design/x1rxhwomHt1pQRUcM3Zqjb?node-id=2922-7969).
|
|
439
116
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const RT_ICON_SYMBOL_ID_PREFIX = 'rt-icon-';
|
|
449
|
-
/**
|
|
450
|
-
* Префикс `id` символов материального набора. Свой, а не общий: наборов на странице бывает два —
|
|
451
|
-
* признак набора стоит и на корне, и на контейнере, — а один `id` держит один рисунок. С общим
|
|
452
|
-
* префиксом второй набор молча забирал бы символ первого.
|
|
453
|
-
*/
|
|
454
|
-
const RT_ICON_MATERIAL_SYMBOL_ID_PREFIX = 'rt-icon-material-';
|
|
455
|
-
/** Префикс `id` залитых рисунков материального набора: у имени их два, и `id` у каждого свой. */
|
|
456
|
-
const RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX = 'rt-icon-material-fill-';
|
|
457
|
-
/** Селекторы, которыми страница объявляет материальный набор. */
|
|
458
|
-
const RT_ICON_MATERIAL_PRESET_SELECTOR = "[data-preset='material'],.rt-preset-material";
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Адрес, по которому приложение публикует набор из `assets/icons` пакета.
|
|
462
|
-
* Умолчание годится, когда сборка кладёт набор в корень статики; свой адрес
|
|
463
|
-
* задаётся аргументом `provideRtIcons()`.
|
|
464
|
-
*/
|
|
465
|
-
const RT_ICONS_BASE_URL = new InjectionToken('RT_ICONS_BASE_URL', {
|
|
466
|
-
providedIn: 'root',
|
|
467
|
-
factory: () => '/icons',
|
|
468
|
-
});
|
|
469
|
-
/**
|
|
470
|
-
* Адрес материального набора значков. Отдельный от своего: два набора стоят рядом, и приложение
|
|
471
|
-
* кладёт их двумя папками.
|
|
472
|
-
*/
|
|
473
|
-
const RT_ICONS_MATERIAL_BASE_URL = new InjectionToken('RT_ICONS_MATERIAL_BASE_URL', {
|
|
474
|
-
providedIn: 'root',
|
|
475
|
-
factory: () => '/icons-material',
|
|
476
|
-
});
|
|
477
|
-
class RtIconRegistry {
|
|
478
|
-
#http = inject(HttpClient);
|
|
479
|
-
#doc = inject(DOCUMENT);
|
|
480
|
-
#baseUrl = inject(RT_ICONS_BASE_URL);
|
|
481
|
-
#materialBaseUrl = inject(RT_ICONS_MATERIAL_BASE_URL);
|
|
482
|
-
#platform = inject(PlatformService);
|
|
483
|
-
/**
|
|
484
|
-
* Имена, за которыми уже сходили: второй раз в сеть за ними не ходят. Ключ несёт набор —
|
|
485
|
-
* одно имя приезжает двумя разными рисунками, и общий ключ отдал бы второму первый.
|
|
486
|
-
*/
|
|
487
|
-
#requested = new Set();
|
|
488
|
-
/** Имена, которые попросила разметка. Загрузку по ним ведёт подписка из конструктора. */
|
|
489
|
-
#requestSource = new Subject();
|
|
490
|
-
constructor() {
|
|
491
|
-
// Потоки складываются, а не вытесняют друг друга: странице нужны все спрошенные
|
|
492
|
-
// значки, а не последний из них.
|
|
493
|
-
this.#requestSource
|
|
494
|
-
.pipe(mergeMap((asked) => this.#load(asked)), takeUntilDestroyed())
|
|
495
|
-
.subscribe();
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Имя symbol для `<use href="#X">`. Возвращает строку с ведущим `#`.
|
|
499
|
-
*
|
|
500
|
-
* Набор назван явно, а не выведен из страницы: одна и та же страница держит оба набора
|
|
501
|
-
* рядом — признак стоит и на корне, и на контейнере, — и вывод по странице дал бы обоим
|
|
502
|
-
* один ответ.
|
|
503
|
-
*/
|
|
504
|
-
symbolHref(name, drawing = 'base') {
|
|
505
|
-
return `#${this.#symbolId(name, drawing)}`;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Просит значок по имени: грузит файл и дописывает `<symbol>` в спрайт страницы.
|
|
509
|
-
*
|
|
510
|
-
* Ответа зовущий не ждёт и потока не получает: разметка рисует ссылку на символ сразу, а
|
|
511
|
-
* браузер дорисовывает её, когда символ приезжает. Подписка живёт здесь и только здесь —
|
|
512
|
-
* иначе каждая разметка гасила бы её сама, а таких мест у значка два.
|
|
513
|
-
*/
|
|
514
|
-
request(name, drawing = 'base') {
|
|
515
|
-
const id = this.#symbolId(name, drawing);
|
|
516
|
-
// На сервере относительных запросов к статике нет вовсе: значок дорисуется после
|
|
517
|
-
// гидрации, когда разметка попросит его снова уже в браузере.
|
|
518
|
-
if (!this.#platform.isPlatformBrowser || this.#requested.has(id)) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
this.#requested.add(id);
|
|
522
|
-
// Символ уже лежит в спрайте страницы: его положила соседняя история витрины или
|
|
523
|
-
// сама страница до старта приложения. Запроса не уходит вовсе.
|
|
524
|
-
if (this.#doc.getElementById(id)) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
this.#requestSource.next({ name, drawing });
|
|
528
|
-
}
|
|
529
|
-
#load(asked) {
|
|
530
|
-
if (asked.drawing === 'base') {
|
|
531
|
-
return this.#loadInto(asked, `${this.#baseUrl}/${asked.name}.svg`, EMPTY);
|
|
532
|
-
}
|
|
533
|
-
// Материальный рисунок, который не приехал, закрывается своим. Материальный набор —
|
|
534
|
-
// слой переопределений: имени, которого в нём нет, кит и так рисует свой рисунок, и
|
|
535
|
-
// не приехавший файл обязан вести себя так же. Иначе страница, объявившая набор, но
|
|
536
|
-
// не опубликовавшая его папку, показывает пустое место там, где обещан значок, —
|
|
537
|
-
// разметка при этом верна, и промах виден только глазами. Залитый, которого нет,
|
|
538
|
-
// сперва закрывается контурным того же набора.
|
|
539
|
-
const own = this.#loadInto(asked, `${this.#baseUrl}/${asked.name}.svg`, EMPTY);
|
|
540
|
-
const outlined = asked.drawing === 'material-fill' ? this.#loadInto(asked, `${this.#materialBaseUrl}/${asked.name}.svg`, own) : own;
|
|
541
|
-
const file = asked.drawing === 'material-fill' ? `${asked.name}.fill.svg` : `${asked.name}.svg`;
|
|
542
|
-
return this.#loadInto(asked, `${this.#materialBaseUrl}/${file}`, outlined);
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Рисунок по адресу под именем символа, которое просила разметка: подмена ссылки разметке
|
|
546
|
-
* не нужна. Отказ одного имени гасит только его значок — иначе один промах в наборе ронял бы
|
|
547
|
-
* всю страницу, а соседние значки уже приехали.
|
|
548
|
-
*/
|
|
549
|
-
#loadInto(asked, url, fallback) {
|
|
550
|
-
return this.#http.get(url, { responseType: 'text' }).pipe(tap((raw) => this.#mountSymbol(asked, raw)), map(() => undefined), catchError(() => fallback));
|
|
551
|
-
}
|
|
552
|
-
#symbolId(name, drawing) {
|
|
553
|
-
const prefix = {
|
|
554
|
-
base: RT_ICON_SYMBOL_ID_PREFIX,
|
|
555
|
-
material: RT_ICON_MATERIAL_SYMBOL_ID_PREFIX,
|
|
556
|
-
'material-fill': RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX,
|
|
557
|
-
};
|
|
558
|
-
return `${prefix[drawing]}${name}`;
|
|
559
|
-
}
|
|
560
|
-
#mountSymbol(asked, raw) {
|
|
561
|
-
const symbol = this.#doc.createElementNS('http://www.w3.org/2000/svg', 'symbol');
|
|
562
|
-
symbol.id = this.#symbolId(asked.name, asked.drawing);
|
|
563
|
-
symbol.setAttribute('viewBox', this.#viewBox(raw));
|
|
564
|
-
symbol.innerHTML = this.#inner(raw);
|
|
565
|
-
this.#sprite().appendChild(symbol);
|
|
566
|
-
}
|
|
567
|
-
#viewBox(raw) {
|
|
568
|
-
const viewBoxMatch = /viewBox="([^"]+)"/.exec(raw);
|
|
569
|
-
return viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
|
|
570
|
-
}
|
|
571
|
-
#inner(raw) {
|
|
572
|
-
// Тело корневого `<svg>` без него самого: оно и становится телом `<symbol>`.
|
|
573
|
-
return raw
|
|
574
|
-
.replace(/<\?xml[\s\S]*?\?>/g, '')
|
|
575
|
-
.replace(/<!DOCTYPE[\s\S]*?>/g, '')
|
|
576
|
-
.replace(/<svg\b[^>]*>/i, '')
|
|
577
|
-
.replace(/<\/svg>\s*$/i, '')
|
|
578
|
-
.trim();
|
|
579
|
-
}
|
|
580
|
-
#sprite() {
|
|
581
|
-
const existing = this.#doc.querySelector(`svg#${RT_ICON_SPRITE_ID}`);
|
|
582
|
-
if (existing) {
|
|
583
|
-
return existing;
|
|
584
|
-
}
|
|
585
|
-
const sprite = this.#doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
586
|
-
sprite.id = RT_ICON_SPRITE_ID;
|
|
587
|
-
sprite.setAttribute('aria-hidden', 'true');
|
|
588
|
-
sprite.setAttribute('style', 'position:absolute;width:0;height:0;overflow:hidden;');
|
|
589
|
-
this.#doc.body.insertBefore(sprite, this.#doc.body.firstChild);
|
|
590
|
-
return sprite;
|
|
591
|
-
}
|
|
592
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
593
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, providedIn: 'root' }); }
|
|
594
|
-
}
|
|
595
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, decorators: [{
|
|
596
|
-
type: Injectable,
|
|
597
|
-
args: [{ providedIn: 'root' }]
|
|
598
|
-
}], ctorParameters: () => [] });
|
|
599
|
-
|
|
600
|
-
const SIZES = Object.freeze({
|
|
601
|
-
xs: 12,
|
|
602
|
-
sm: 16,
|
|
603
|
-
md: 20,
|
|
604
|
-
lg: 24,
|
|
605
|
-
xl: 32,
|
|
606
|
-
'2xl': 40,
|
|
607
|
-
});
|
|
608
|
-
/* Без переданного цвета значок цвета не пишет: `currentColor` строкой стиля перебивал правило
|
|
609
|
-
стилей, которое красит значок снаружи, — значок опасного пункта меню оставался цвета текста. */
|
|
610
|
-
const COLORS = Object.freeze({
|
|
611
|
-
current: null,
|
|
612
|
-
// Приглушённый тон можно переназначить свойством сверху: поле ввода называет так тон своего
|
|
613
|
-
// значка в наборе оформления. Цвет стоит встроенным стилем, и правилом его не перебить.
|
|
614
|
-
muted: 'var(--rt-icon-color-muted, var(--rt-neutral-600))',
|
|
615
|
-
info: 'var(--rt-color-state-info)',
|
|
616
|
-
success: 'var(--rt-color-state-success)',
|
|
617
|
-
warning: 'var(--rt-color-state-warning)',
|
|
618
|
-
danger: 'var(--rt-color-state-danger)',
|
|
619
|
-
inverse: 'var(--rt-color-text-inverse)',
|
|
620
|
-
});
|
|
621
|
-
const BEM_BLOCK$1v = 'rt-icon';
|
|
622
|
-
class RtIconComponent {
|
|
623
|
-
#registry;
|
|
624
|
-
#host;
|
|
625
|
-
/**
|
|
626
|
-
* Набор, объявленный разметкой над этим значком.
|
|
627
|
-
*
|
|
628
|
-
* Читается у разметки, а не выбирается стилями: ссылку на символ спрайта CSS подменить нечем,
|
|
629
|
-
* и второй `<use>` рядом с первым тоже не годится — он вечно указывает на символ, за которым
|
|
630
|
-
* никто не ходил, а обвязка снимков ждёт, пока нарисуется каждый.
|
|
631
|
-
*
|
|
632
|
-
* Спрашивается ближайший предок, а не корень страницы: признак набора стоит и на контейнере,
|
|
633
|
-
* и на одной странице законно живут оба набора рядом.
|
|
634
|
-
*
|
|
635
|
-
* Один раз после первой отрисовки: на сервере разметки нет вовсе, а признак набора страница
|
|
636
|
-
* по ходу жизни не переставляет — его ставит приложение своей разметкой.
|
|
637
|
-
*/
|
|
638
|
-
#preset;
|
|
639
|
-
constructor() {
|
|
640
|
-
this.#registry = inject(RtIconRegistry);
|
|
641
|
-
this.#host = inject(ElementRef);
|
|
642
|
-
/**
|
|
643
|
-
* Набор, объявленный разметкой над этим значком.
|
|
644
|
-
*
|
|
645
|
-
* Читается у разметки, а не выбирается стилями: ссылку на символ спрайта CSS подменить нечем,
|
|
646
|
-
* и второй `<use>` рядом с первым тоже не годится — он вечно указывает на символ, за которым
|
|
647
|
-
* никто не ходил, а обвязка снимков ждёт, пока нарисуется каждый.
|
|
648
|
-
*
|
|
649
|
-
* Спрашивается ближайший предок, а не корень страницы: признак набора стоит и на контейнере,
|
|
650
|
-
* и на одной странице законно живут оба набора рядом.
|
|
651
|
-
*
|
|
652
|
-
* Один раз после первой отрисовки: на сервере разметки нет вовсе, а признак набора страница
|
|
653
|
-
* по ходу жизни не переставляет — его ставит приложение своей разметкой.
|
|
654
|
-
*/
|
|
655
|
-
this.#preset = signal('base', /* @ts-ignore */
|
|
656
|
-
...(ngDevMode ? [{ debugName: "#preset" }] : /* istanbul ignore next */ []));
|
|
657
|
-
this.href = computed(() => this.#registry.symbolHref(this.name(), this.drawing()), /* @ts-ignore */
|
|
658
|
-
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
659
|
-
/**
|
|
660
|
-
* Набор, которым рисуется этот значок. Материальный закрывает не все имена кита — он слой
|
|
661
|
-
* переопределений, как набор оформления: имя без материального рисунка рисуется своим, и это
|
|
662
|
-
* не пробел.
|
|
663
|
-
*/
|
|
664
|
-
this.preset = computed(() => this.#preset() === 'material' && iconMaterialDrawn.has(this.name()) ? 'material' : 'base', /* @ts-ignore */
|
|
665
|
-
...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
|
|
666
|
-
/** Рисунок значка: залитый бывает только у материального набора, свой набор заливки не знает. */
|
|
667
|
-
this.drawing = computed(() => {
|
|
668
|
-
if (this.preset() === 'base') {
|
|
669
|
-
return 'base';
|
|
670
|
-
}
|
|
671
|
-
return this.fill() ? 'material-fill' : 'material';
|
|
672
|
-
}, /* @ts-ignore */
|
|
673
|
-
...(ngDevMode ? [{ debugName: "drawing" }] : /* istanbul ignore next */ []));
|
|
674
|
-
this.sizePx = computed(() => SIZES[this.size()], /* @ts-ignore */
|
|
675
|
-
...(ngDevMode ? [{ debugName: "sizePx" }] : /* istanbul ignore next */ []));
|
|
676
|
-
this.colorValue = computed(() => COLORS[this.color()], /* @ts-ignore */
|
|
677
|
-
...(ngDevMode ? [{ debugName: "colorValue" }] : /* istanbul ignore next */ []));
|
|
678
|
-
this.rotateStyle = computed(() => {
|
|
679
|
-
const r = this.rotate();
|
|
680
|
-
return r !== null && r !== 0 ? `rotate(${r}deg)` : null;
|
|
681
|
-
}, /* @ts-ignore */
|
|
682
|
-
...(ngDevMode ? [{ debugName: "rotateStyle" }] : /* istanbul ignore next */ []));
|
|
683
|
-
this.name = input.required(/* @ts-ignore */
|
|
684
|
-
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
685
|
-
this.size = input('md', /* @ts-ignore */
|
|
686
|
-
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
687
|
-
this.color = input('current', /* @ts-ignore */
|
|
688
|
-
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
689
|
-
/**
|
|
690
|
-
* Залитый рисунок вместо контурного — как `FILL 1` у значка первого кита. Действует в
|
|
691
|
-
* материальном наборе; свой набор рисует значок одним рисунком. Дефолт `false`.
|
|
692
|
-
*/
|
|
693
|
-
this.fill = input(false, { ...(ngDevMode ? { debugName: "fill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
694
|
-
this.rotate = input(null, { ...(ngDevMode ? { debugName: "rotate" } : /* istanbul ignore next */ {}), transform: (v) => {
|
|
695
|
-
if (v === null || v === '') {
|
|
696
|
-
return null;
|
|
697
|
-
}
|
|
698
|
-
return numberAttribute(v, 0);
|
|
699
|
-
} });
|
|
700
|
-
// Значок едет по запросу имени, а не вперёд всем набором: страница платит за то, что
|
|
701
|
-
// нарисовала. Смена имени просит новое — прежний символ остаётся в спрайте.
|
|
702
|
-
effect(() => {
|
|
703
|
-
this.#registry.request(this.name(), this.drawing());
|
|
704
|
-
});
|
|
705
|
-
// Разметка над значком видна только в браузере и только после первой отрисовки:
|
|
706
|
-
// контейнер с признаком набора рисует то же приложение.
|
|
707
|
-
afterNextRender(() => {
|
|
708
|
-
if (this.#host.nativeElement.closest(RT_ICON_MATERIAL_PRESET_SELECTOR)) {
|
|
709
|
-
this.#preset.set('material');
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
714
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: RtIconComponent, isStandalone: true, selector: "rt-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, rotate: { classPropertyName: "rotate", publicName: "rotate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "'true'", "style.width.px": "sizePx()", "style.height.px": "sizePx()", "style.color": "colorValue()", "style.transform": "rotateStyle()" }, classAttribute: "rt-icon" }, ngImport: i0, template: "<svg focusable=\"false\" aria-hidden=\"true\" [attr.viewBox]=\"'0 0 24 24'\">\n <use [attr.href]=\"href()\" />\n</svg>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-icon{display:inline-flex;flex-shrink:0;line-height:0}.rt-icon svg{display:block;width:100%;height:100%;fill:currentcolor}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
715
|
-
}
|
|
716
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconComponent, decorators: [{
|
|
717
|
-
type: Component,
|
|
718
|
-
args: [{ selector: 'rt-icon', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
719
|
-
class: BEM_BLOCK$1v,
|
|
720
|
-
'[attr.aria-hidden]': "'true'",
|
|
721
|
-
'[style.width.px]': 'sizePx()',
|
|
722
|
-
'[style.height.px]': 'sizePx()',
|
|
723
|
-
'[style.color]': 'colorValue()',
|
|
724
|
-
'[style.transform]': 'rotateStyle()',
|
|
725
|
-
}, template: "<svg focusable=\"false\" aria-hidden=\"true\" [attr.viewBox]=\"'0 0 24 24'\">\n <use [attr.href]=\"href()\" />\n</svg>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-icon{display:inline-flex;flex-shrink:0;line-height:0}.rt-icon svg{display:block;width:100%;height:100%;fill:currentcolor}}\n"] }]
|
|
726
|
-
}], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], rotate: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotate", required: false }] }] } });
|
|
727
|
-
|
|
728
|
-
const BEM_BLOCK$1u = 'rt-ripple';
|
|
729
|
-
/** Сколько живёт один слой волны. Совпадает с длительностью в стилях блока. */
|
|
730
|
-
const RIPPLE_LIFETIME_MS = 550;
|
|
731
|
-
/**
|
|
732
|
-
* Волна нажатия. Ставится на элемент, который нажимают, и рисует расходящийся
|
|
733
|
-
* от точки касания круг.
|
|
734
|
-
*
|
|
735
|
-
* Волна живёт в своём обрезающем слое внутри элемента, а не прямо в нём:
|
|
736
|
-
* `overflow: hidden` на самой кнопке срезал бы всё, что торчит за её край, —
|
|
737
|
-
* у кнопки-значка так стоит метка, вынесенная на два пикселя наружу.
|
|
738
|
-
*
|
|
739
|
-
* @example
|
|
740
|
-
* ```html
|
|
741
|
-
* <button rtRipple>Сохранить</button>
|
|
742
|
-
* <button rtRipple [rippleDisabled]="loading()">Сохранить</button>
|
|
743
|
-
* ```
|
|
744
|
-
*/
|
|
745
|
-
class RtRippleDirective {
|
|
746
|
-
#el;
|
|
747
|
-
#renderer;
|
|
748
|
-
#destroyRef;
|
|
749
|
-
/** Слои, которые ещё гаснут: снимаются, если элемент умрёт раньше их. */
|
|
750
|
-
#live;
|
|
751
|
-
/** Обрезающий слой заводится при первом нажатии: у кнопки, которую не нажали, его нет. */
|
|
752
|
-
#area;
|
|
753
|
-
/** Гасит незакончившиеся волны: иначе таймер трогает снятый из дерева узел. */
|
|
754
|
-
constructor() {
|
|
755
|
-
this.#el = inject(ElementRef);
|
|
756
|
-
this.#renderer = inject(Renderer2);
|
|
757
|
-
this.#destroyRef = inject(DestroyRef);
|
|
758
|
-
/** Слои, которые ещё гаснут: снимаются, если элемент умрёт раньше их. */
|
|
759
|
-
this.#live = new Set();
|
|
760
|
-
/** Обрезающий слой заводится при первом нажатии: у кнопки, которую не нажали, его нет. */
|
|
761
|
-
this.#area = null;
|
|
762
|
-
/**
|
|
763
|
-
* Отключает волну, не трогая остальное поведение элемента.
|
|
764
|
-
* Отключённая и загружающаяся кнопка волны не даёт.
|
|
765
|
-
*/
|
|
766
|
-
this.rippleDisabled = input(false, { ...(ngDevMode ? { debugName: "rippleDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
767
|
-
this.#destroyRef.onDestroy(() => {
|
|
768
|
-
for (const timer of this.#live) {
|
|
769
|
-
clearTimeout(timer);
|
|
770
|
-
}
|
|
771
|
-
this.#live.clear();
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Ставит один слой волны от точки нажатия и снимает его, когда тот отгорит.
|
|
776
|
-
*
|
|
777
|
-
* Радиус считается по дальнему углу элемента: волна из угла должна накрыть
|
|
778
|
-
* противоположный, иначе на широкой кнопке она обрывается на полпути.
|
|
779
|
-
*/
|
|
780
|
-
launch(event) {
|
|
781
|
-
const host = this.#el.nativeElement;
|
|
782
|
-
if (this.rippleDisabled() || host.disabled) {
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
const box = host.getBoundingClientRect();
|
|
786
|
-
const x = event.clientX - box.left;
|
|
787
|
-
const y = event.clientY - box.top;
|
|
788
|
-
const radius = Math.hypot(Math.max(x, box.width - x), Math.max(y, box.height - y));
|
|
789
|
-
const area = this.#ensureArea(host);
|
|
790
|
-
const layer = this.#renderer.createElement('span');
|
|
791
|
-
this.#renderer.addClass(layer, `${BEM_BLOCK$1u}__wave`);
|
|
792
|
-
this.#renderer.setStyle(layer, 'left', `${x - radius}px`);
|
|
793
|
-
this.#renderer.setStyle(layer, 'top', `${y - radius}px`);
|
|
794
|
-
this.#renderer.setStyle(layer, 'width', `${radius * 2}px`);
|
|
795
|
-
this.#renderer.setStyle(layer, 'height', `${radius * 2}px`);
|
|
796
|
-
this.#renderer.appendChild(area, layer);
|
|
797
|
-
const timer = setTimeout(() => {
|
|
798
|
-
this.#live.delete(timer);
|
|
799
|
-
if (layer.parentNode) {
|
|
800
|
-
this.#renderer.removeChild(area, layer);
|
|
801
|
-
}
|
|
802
|
-
}, RIPPLE_LIFETIME_MS);
|
|
803
|
-
this.#live.add(timer);
|
|
804
|
-
}
|
|
805
|
-
/** Обрезающий слой один на элемент: он повторяет его скругление и не ловит указатель. */
|
|
806
|
-
#ensureArea(host) {
|
|
807
|
-
if (this.#area?.parentNode) {
|
|
808
|
-
return this.#area;
|
|
809
|
-
}
|
|
810
|
-
/* Обрезающий слой стоит absolute, и ему нужен позиционированный предок. Позиция
|
|
811
|
-
ставится здесь, а не правилом блока: правило висело бы на каждой кнопке кита и
|
|
812
|
-
меняло растеризацию кадров витрины, ничего не двигая в раскладке. */
|
|
813
|
-
if (getComputedStyle(host).position === 'static') {
|
|
814
|
-
this.#renderer.setStyle(host, 'position', 'relative');
|
|
815
|
-
}
|
|
816
|
-
const area = this.#renderer.createElement('span');
|
|
817
|
-
this.#renderer.addClass(area, `${BEM_BLOCK$1u}__area`);
|
|
818
|
-
this.#renderer.setAttribute(area, 'aria-hidden', 'true');
|
|
819
|
-
this.#renderer.appendChild(host, area);
|
|
820
|
-
this.#area = area;
|
|
821
|
-
return area;
|
|
822
|
-
}
|
|
823
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRippleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
824
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: RtRippleDirective, isStandalone: true, selector: "[rtRipple]", inputs: { rippleDisabled: { classPropertyName: "rippleDisabled", publicName: "rippleDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "launch($event)" }, classAttribute: "rt-ripple" }, ngImport: i0 }); }
|
|
825
|
-
}
|
|
826
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRippleDirective, decorators: [{
|
|
827
|
-
type: Directive,
|
|
828
|
-
args: [{
|
|
829
|
-
selector: '[rtRipple]',
|
|
830
|
-
host: {
|
|
831
|
-
class: BEM_BLOCK$1u,
|
|
832
|
-
'(pointerdown)': 'launch($event)',
|
|
833
|
-
},
|
|
834
|
-
}]
|
|
835
|
-
}], ctorParameters: () => [], propDecorators: { rippleDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "rippleDisabled", required: false }] }] } });
|
|
836
|
-
|
|
837
|
-
/** Признак материального набора в разметке: атрибут или класс, которые ставит приложение. */
|
|
838
|
-
const RT_MATERIAL_PRESET_SELECTOR = "[data-preset='material'],.rt-preset-material";
|
|
839
|
-
/** Класс набора, который несёт узел вне поддерева, где набор объявлен. */
|
|
840
|
-
const RT_MATERIAL_PRESET_CLASS = 'rt-preset-material';
|
|
841
|
-
/**
|
|
842
|
-
* Классы набора для панели наложения, открытой от `origin`. Панель лежит в конце страницы, вне
|
|
843
|
-
* контейнера с признаком набора, и назначения набора до неё не доходят: она берёт класс набора
|
|
844
|
-
* сама, если признак стоит над узлом, от которого она открыта. Кит читает признак приложения, а не
|
|
845
|
-
* ставит свой.
|
|
846
|
-
*/
|
|
847
|
-
function materialPresetClassesOf(origin) {
|
|
848
|
-
return origin.closest(RT_MATERIAL_PRESET_SELECTOR) ? [RT_MATERIAL_PRESET_CLASS] : [];
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Классы набора для панели, которую открывает сервис без узла-источника: диалог, боковая панель.
|
|
852
|
-
* Источником служит узел в фокусе в момент открытия — нажатая кнопка. Панель, открытая не по
|
|
853
|
-
* нажатию, набор от фокуса не берёт; для неё остаётся `panelClass` приложения.
|
|
854
|
-
*/
|
|
855
|
-
function materialPresetClassesOfFocus(document) {
|
|
856
|
-
const focused = document.activeElement;
|
|
857
|
-
return focused ? materialPresetClassesOf(focused) : [];
|
|
858
|
-
}
|
|
859
|
-
/** Атрибут местного куска темы — его ставит `rtTheme` и читают правила куска в стилях кита. */
|
|
860
|
-
const RT_THEME_SCOPE_ATTRIBUTE = 'data-theme';
|
|
861
|
-
/**
|
|
862
|
-
* Тема местного куска, от которого открыта панель, — значение ближайшего `data-theme` над `origin`.
|
|
863
|
-
* Панель лежит в конце страницы, вне куска, и без этого рисуется темой страницы: список тёмной
|
|
864
|
-
* карточки на светлой странице выходил светлым. Тема корня страницы не возвращается: её панель
|
|
865
|
-
* наследует и так, а признак на панели повторил бы весь набор назначений ещё раз.
|
|
866
|
-
*/
|
|
867
|
-
function themeScopeOf(origin) {
|
|
868
|
-
const scope = origin.closest(`[${RT_THEME_SCOPE_ATTRIBUTE}]`);
|
|
869
|
-
if (scope === null || scope === origin.ownerDocument.documentElement) {
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
return scope.getAttribute(RT_THEME_SCOPE_ATTRIBUTE) || null;
|
|
873
|
-
}
|
|
874
|
-
function applyThemeScope(pane, theme) {
|
|
875
|
-
if (theme === null) {
|
|
876
|
-
pane.removeAttribute(RT_THEME_SCOPE_ATTRIBUTE);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
pane.setAttribute(RT_THEME_SCOPE_ATTRIBUTE, theme);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Переносит тему куска `origin` на коробку панели или снимает её, если кусок ушёл. Зовётся на
|
|
884
|
-
* каждом открытии: коробка переживает закрытие, а кусок вокруг источника мог смениться.
|
|
885
|
-
*/
|
|
886
|
-
function carryThemeScope(pane, origin) {
|
|
887
|
-
applyThemeScope(pane, themeScopeOf(origin));
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Тема местного куска для панели, которую открывает сервис без узла-источника: диалог, боковая
|
|
891
|
-
* панель. Источником служит узел в фокусе в момент открытия — нажатая кнопка, как у
|
|
892
|
-
* `materialPresetClassesOfFocus`. Без фокуса, как и под темой корня, возвращает `null`.
|
|
893
|
-
*/
|
|
894
|
-
function themeScopeOfFocus(document) {
|
|
895
|
-
const focused = document.activeElement;
|
|
896
|
-
return focused ? themeScopeOf(focused) : null;
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Переносит на коробку панели тему куска, где стоит фокус. Зовётся до того, как панель заберёт
|
|
900
|
-
* фокус себе: после этого `activeElement` уже внутри панели, в конце страницы, вне куска.
|
|
901
|
-
*/
|
|
902
|
-
function carryThemeScopeOfFocus(pane, document) {
|
|
903
|
-
applyThemeScope(pane, themeScopeOfFocus(document));
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const BEM_BLOCK$1t = 'rt-tooltip';
|
|
907
|
-
/**
|
|
908
|
-
* Презентационная панель tooltip'а. Рендерится директивой `[rtTooltip]` через
|
|
909
|
-
* CDK `ComponentPortal` в overlay; текст выставляется императивно
|
|
910
|
-
* (`ref.instance.text.set(...)`) — отдельного `input()` нет, т.к. компонент
|
|
911
|
-
* создаётся вручную, а не в шаблоне.
|
|
912
|
-
*
|
|
913
|
-
* `ViewEncapsulation.None` — стили префиксованы `.rt-tooltip` (единообразно с
|
|
914
|
-
* остальным common/ui). `role="tooltip"` — вспомогательный ярлык для AT.
|
|
915
|
-
*/
|
|
916
|
-
class RtTooltipComponent {
|
|
917
|
-
constructor() {
|
|
918
|
-
this.text = signal('', /* @ts-ignore */
|
|
919
|
-
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
920
|
-
}
|
|
921
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
922
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: RtTooltipComponent, isStandalone: true, selector: "rt-tooltip", host: { attributes: { "role": "tooltip" }, classAttribute: "rt-tooltip" }, ngImport: i0, template: '{{ text() }}', isInline: true, styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-tooltip{--rt-tooltip-radius: var(--rt-overlay-tooltip-radius);--rt-tooltip-shadow: var(--rt-overlay-tooltip-shadow);display:block;max-width:240px;padding:var(--rt-space-xs) var(--rt-space-sm);border-radius:var(--rt-tooltip-radius);background:var(--rt-color-bg-inverse);box-shadow:var(--rt-tooltip-shadow);color:var(--rt-color-text-inverse);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);overflow-wrap:anywhere;pointer-events:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
923
|
-
}
|
|
924
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipComponent, decorators: [{
|
|
925
|
-
type: Component,
|
|
926
|
-
args: [{ selector: 'rt-tooltip', template: '{{ text() }}', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
927
|
-
class: BEM_BLOCK$1t,
|
|
928
|
-
role: 'tooltip',
|
|
929
|
-
}, styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-tooltip{--rt-tooltip-radius: var(--rt-overlay-tooltip-radius);--rt-tooltip-shadow: var(--rt-overlay-tooltip-shadow);display:block;max-width:240px;padding:var(--rt-space-xs) var(--rt-space-sm);border-radius:var(--rt-tooltip-radius);background:var(--rt-color-bg-inverse);box-shadow:var(--rt-tooltip-shadow);color:var(--rt-color-text-inverse);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);overflow-wrap:anywhere;pointer-events:none}}\n"] }]
|
|
930
|
-
}] });
|
|
931
|
-
|
|
932
|
-
const SHOW_DELAY_MS = 300;
|
|
933
|
-
const VIEWPORT_MARGIN$1 = 8;
|
|
934
|
-
/**
|
|
935
|
-
* Tooltip-директива поверх CDK Overlay. Показывает короткую подсказку
|
|
936
|
-
* (`rtTooltip` = строка) на hover/focus host'а. Движок — CDK `flexibleConnectedTo`
|
|
937
|
-
* (авто-flip у края viewport, reposition при скролле), так что подсказка не
|
|
938
|
-
* режется `overflow: hidden` предками (таблицы, aside, карточки).
|
|
939
|
-
*
|
|
940
|
-
* Триггеры: `mouseenter`/`focusin` показывают (с задержкой `SHOW_DELAY_MS`),
|
|
941
|
-
* `mouseleave`/`focusout`/`click` прячут. Пустой текст → no-op (директива
|
|
942
|
-
* выключена), поэтому её можно безусловно вешать на icon-кнопки и включать
|
|
943
|
-
* выставлением строки.
|
|
944
|
-
*
|
|
945
|
-
* @example
|
|
946
|
-
* ```html
|
|
947
|
-
* <rt-icon-button icon="ico-trash" ariaLabel="Удалить" tooltip="Удалить" />
|
|
948
|
-
* <button rtButton [rtTooltip]="'Подсказка'">…</button>
|
|
949
|
-
* ```
|
|
950
|
-
*/
|
|
951
|
-
class RtTooltipDirective {
|
|
952
|
-
constructor() {
|
|
953
|
-
this.#overlay = inject(Overlay);
|
|
954
|
-
this.#elementRef = inject(ElementRef);
|
|
955
|
-
this.#scrollStrategies = inject(ScrollStrategyOptions);
|
|
956
|
-
this.#overlayRef = null;
|
|
957
|
-
this.#tooltipRef = null;
|
|
958
|
-
this.#showTimer = null;
|
|
959
|
-
/**
|
|
960
|
-
* Текст подсказки. Принимает `null`/`undefined` (частый результат date-пайпа
|
|
961
|
-
* или nullable-полей модели) и нормализует их в пустую строку — иначе
|
|
962
|
-
* биндинги вида `value | date` ломали бы строгую типизацию шаблона. Пустая
|
|
963
|
-
* строка → no-op (директива выключена).
|
|
964
|
-
*/
|
|
965
|
-
this.text = input('', { ...(ngDevMode ? { debugName: "text" } : /* istanbul ignore next */ {}), alias: 'rtTooltip',
|
|
966
|
-
transform: (value) => value ?? '' });
|
|
967
|
-
/** Сторона подсказки. Не задана — сверху, а под материальным набором снизу, как у первого кита. */
|
|
968
|
-
this.placement = input(null, { ...(ngDevMode ? { debugName: "placement" } : /* istanbul ignore next */ {}), alias: 'rtTooltipPlacement' });
|
|
969
|
-
}
|
|
970
|
-
#overlay;
|
|
971
|
-
#elementRef;
|
|
972
|
-
#scrollStrategies;
|
|
973
|
-
#overlayRef;
|
|
974
|
-
#tooltipRef;
|
|
975
|
-
#showTimer;
|
|
976
|
-
ngOnDestroy() {
|
|
977
|
-
this.#clearTimer();
|
|
978
|
-
this.#disposeOverlay();
|
|
979
|
-
}
|
|
980
|
-
show() {
|
|
981
|
-
if (this.text().trim() === '' || this.#tooltipRef !== null) {
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
this.#clearTimer();
|
|
985
|
-
this.#showTimer = setTimeout(() => this.#attach(), SHOW_DELAY_MS);
|
|
986
|
-
}
|
|
987
|
-
hide() {
|
|
988
|
-
this.#clearTimer();
|
|
989
|
-
this.#overlayRef?.detach();
|
|
990
|
-
this.#tooltipRef = null;
|
|
991
|
-
}
|
|
992
|
-
#attach() {
|
|
993
|
-
if (this.text().trim() === '') {
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
const overlayRef = this.#ensureOverlay();
|
|
997
|
-
// Коробка переживает закрытие, а кусок темы вокруг host'а мог смениться — переносим на каждом показе.
|
|
998
|
-
carryThemeScope(overlayRef.overlayElement, this.#elementRef.nativeElement);
|
|
999
|
-
const portal = new ComponentPortal(RtTooltipComponent);
|
|
1000
|
-
this.#tooltipRef = overlayRef.attach(portal);
|
|
1001
|
-
this.#tooltipRef.instance.text.set(this.text());
|
|
1002
|
-
this.#tooltipRef.changeDetectorRef.markForCheck();
|
|
1003
|
-
}
|
|
1004
|
-
#ensureOverlay() {
|
|
1005
|
-
if (this.#overlayRef !== null) {
|
|
1006
|
-
return this.#overlayRef;
|
|
1007
|
-
}
|
|
1008
|
-
const above = {
|
|
1009
|
-
originX: 'center',
|
|
1010
|
-
originY: 'top',
|
|
1011
|
-
overlayX: 'center',
|
|
1012
|
-
overlayY: 'bottom',
|
|
1013
|
-
offsetY: -6,
|
|
1014
|
-
};
|
|
1015
|
-
const below = {
|
|
1016
|
-
originX: 'center',
|
|
1017
|
-
originY: 'bottom',
|
|
1018
|
-
overlayX: 'center',
|
|
1019
|
-
overlayY: 'top',
|
|
1020
|
-
offsetY: 6,
|
|
1021
|
-
};
|
|
1022
|
-
const presetClasses = materialPresetClassesOf(this.#elementRef.nativeElement);
|
|
1023
|
-
const placement = this.placement() ?? (presetClasses.length > 0 ? 'bottom' : 'top');
|
|
1024
|
-
const positions = placement === 'top' ? [above, below] : [below, above];
|
|
1025
|
-
const positionStrategy = this.#overlay
|
|
1026
|
-
.position()
|
|
1027
|
-
.flexibleConnectedTo(this.#elementRef)
|
|
1028
|
-
.withPush(true)
|
|
1029
|
-
.withViewportMargin(VIEWPORT_MARGIN$1)
|
|
1030
|
-
.withFlexibleDimensions(false)
|
|
1031
|
-
.withPositions(positions);
|
|
1032
|
-
const config = new OverlayConfig({
|
|
1033
|
-
positionStrategy,
|
|
1034
|
-
scrollStrategy: this.#scrollStrategies.reposition(),
|
|
1035
|
-
hasBackdrop: false,
|
|
1036
|
-
panelClass: ['rt-tooltip-panel', ...presetClasses],
|
|
1037
|
-
});
|
|
1038
|
-
this.#overlayRef = this.#overlay.create(config);
|
|
1039
|
-
return this.#overlayRef;
|
|
1040
|
-
}
|
|
1041
|
-
#clearTimer() {
|
|
1042
|
-
if (this.#showTimer !== null) {
|
|
1043
|
-
clearTimeout(this.#showTimer);
|
|
1044
|
-
this.#showTimer = null;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
#disposeOverlay() {
|
|
1048
|
-
this.#overlayRef?.dispose();
|
|
1049
|
-
this.#overlayRef = null;
|
|
1050
|
-
this.#tooltipRef = null;
|
|
1051
|
-
}
|
|
1052
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1053
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: RtTooltipDirective, isStandalone: true, selector: "[rtTooltip]", inputs: { text: { classPropertyName: "text", publicName: "rtTooltip", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "rtTooltipPlacement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "show()", "mouseleave": "hide()", "focusin": "show()", "focusout": "hide()", "click": "hide()" } }, exportAs: ["rtTooltip"], ngImport: i0 }); }
|
|
1054
|
-
}
|
|
1055
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipDirective, decorators: [{
|
|
1056
|
-
type: Directive,
|
|
1057
|
-
args: [{
|
|
1058
|
-
selector: '[rtTooltip]',
|
|
1059
|
-
exportAs: 'rtTooltip',
|
|
1060
|
-
host: {
|
|
1061
|
-
'(mouseenter)': 'show()',
|
|
1062
|
-
'(mouseleave)': 'hide()',
|
|
1063
|
-
'(focusin)': 'show()',
|
|
1064
|
-
'(focusout)': 'hide()',
|
|
1065
|
-
'(click)': 'hide()',
|
|
1066
|
-
},
|
|
1067
|
-
}]
|
|
1068
|
-
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtTooltip", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtTooltipPlacement", required: false }] }] } });
|
|
1069
|
-
|
|
1070
|
-
const BEM_BLOCK$1s = 'rt-icon-button';
|
|
1071
|
-
/**
|
|
1072
|
-
* Универсальная icon-only кнопка кита.
|
|
1073
|
-
*
|
|
1074
|
-
* Рендерит свой собственный `<button type="...">` внутри template (НЕ host-button).
|
|
1075
|
-
* Все цвета и размеры — через семантические `--rt-*` токены; геометрия квадрата
|
|
1076
|
-
* выставляется CSS-custom-property `--rt-icon-button-size` (overridable inline
|
|
1077
|
-
* через `style="--rt-icon-button-size: 35px"` — нужно для миграции rt-header 35×35).
|
|
1078
|
-
*
|
|
1079
|
-
* Loading state — заменяет иконку на `<rt-icon name="spinner">` и блокирует
|
|
1080
|
-
* клики (`button[disabled]`). Active state управляет `aria-pressed` (для toggle-кнопок).
|
|
1081
|
-
* Indicator — небольшой dot top-right через BEM-элемент `__indicator`.
|
|
1082
|
-
*
|
|
1083
|
-
* BEM-разметка — через `rtBlock="rt-icon-button"` + `[rtMod]` объект; камелкейс
|
|
1084
|
-
* модификаторов автоматически конвертируется директивой в kebab-case
|
|
1085
|
-
* (`hasIndicator` → `rt-icon-button--has-indicator`).
|
|
1086
|
-
*/
|
|
1087
|
-
class RtIconButtonComponent {
|
|
1088
|
-
constructor() {
|
|
1089
|
-
/**
|
|
1090
|
-
* Размер дочерней `<rt-icon>`. По умолчанию маппится 1-к-1 от размера кнопки,
|
|
1091
|
-
* но потребитель может задать свой: у крупной кнопки-действия (плавающая
|
|
1092
|
-
* кнопка чата) иконка во весь диаметр выглядит тяжеловесно, а сама кнопка
|
|
1093
|
-
* должна остаться большой — в неё целятся пальцем.
|
|
1094
|
-
*/
|
|
1095
|
-
this.resolvedIconSize = computed(() => {
|
|
1096
|
-
const map = {
|
|
1097
|
-
sm: 'sm',
|
|
1098
|
-
md: 'md',
|
|
1099
|
-
lg: 'lg',
|
|
1100
|
-
xl: 'xl',
|
|
1101
|
-
'2xl': '2xl',
|
|
1102
|
-
};
|
|
1103
|
-
return this.iconSize() ?? map[this.size()];
|
|
1104
|
-
}, /* @ts-ignore */
|
|
1105
|
-
...(ngDevMode ? [{ debugName: "resolvedIconSize" }] : /* istanbul ignore next */ []));
|
|
1106
|
-
/** Кнопка отключена, если установлен disabled ИЛИ loading. */
|
|
1107
|
-
this.isDisabled = computed(() => this.disabled() || this.loading(), /* @ts-ignore */
|
|
1108
|
-
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1109
|
-
/**
|
|
1110
|
-
* BEM-модификаторы для `[rtMod]`. Собираем в .ts (а не в шаблоне), потому что
|
|
1111
|
-
* Angular template-parser не поддерживает computed property keys в inline-объектах.
|
|
1112
|
-
* camelCase ключи `hasIndicator` директива автоматически конвертирует в `has-indicator`.
|
|
1113
|
-
*/
|
|
1114
|
-
this.bemMods = computed(() => ({
|
|
1115
|
-
[this.variant()]: true,
|
|
1116
|
-
[this.size()]: true,
|
|
1117
|
-
[this.shape()]: true,
|
|
1118
|
-
loading: this.loading(),
|
|
1119
|
-
active: this.active(),
|
|
1120
|
-
hasIndicator: this.indicator(),
|
|
1121
|
-
}), /* @ts-ignore */
|
|
1122
|
-
...(ngDevMode ? [{ debugName: "bemMods" }] : /* istanbul ignore next */ []));
|
|
1123
|
-
/** Имя иконки — обязательно. */
|
|
1124
|
-
this.icon = input.required(/* @ts-ignore */
|
|
1125
|
-
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
1126
|
-
/** Доступное имя для screen-reader'ов — обязательно (icon-only кнопка). */
|
|
1127
|
-
this.ariaLabel = input.required(/* @ts-ignore */
|
|
1128
|
-
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
1129
|
-
/** Семантическая палитра. */
|
|
1130
|
-
this.variant = input('ghost', /* @ts-ignore */
|
|
1131
|
-
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
1132
|
-
/**
|
|
1133
|
-
* Цвет самой иконки. Default `current` — иконка наследует цвет текста кнопки
|
|
1134
|
-
* (поведение по умолчанию). Явное значение нужно, когда иконка должна
|
|
1135
|
-
* отличаться от текста кнопки — например крестик-очистка внутри поля,
|
|
1136
|
-
* который должен совпадать по цвету с иконками-аффордансами самого поля.
|
|
1137
|
-
*/
|
|
1138
|
-
this.iconColor = input('current', /* @ts-ignore */
|
|
1139
|
-
...(ngDevMode ? [{ debugName: "iconColor" }] : /* istanbul ignore next */ []));
|
|
1140
|
-
/**
|
|
1141
|
-
* Залитый рисунок значка вместо контурного — как `fill` у `rt-icon`. Рисует его материальный
|
|
1142
|
-
* набор; значок своего набора один и от признака не меняется.
|
|
1143
|
-
*/
|
|
1144
|
-
this.iconFill = input(false, { ...(ngDevMode ? { debugName: "iconFill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1145
|
-
/** Размер квадрата. */
|
|
1146
|
-
this.size = input('md', /* @ts-ignore */
|
|
1147
|
-
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1148
|
-
/** Размер иконки; пусто — берётся от размера кнопки. */
|
|
1149
|
-
this.iconSize = input(null, /* @ts-ignore */
|
|
1150
|
-
...(ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []));
|
|
1151
|
-
/** Форма (круг или скруглённый квадрат). */
|
|
1152
|
-
this.shape = input('square', /* @ts-ignore */
|
|
1153
|
-
...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
|
|
1154
|
-
/** HTML-type для нативного `<button>`. */
|
|
1155
|
-
this.type = input('button', /* @ts-ignore */
|
|
1156
|
-
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
1157
|
-
/**
|
|
1158
|
-
* Текст styled-tooltip'а (`rtTooltip`), показывается на hover/focus. Пустая
|
|
1159
|
-
* строка → tooltip выключен. Для icon-only кнопок обычно дублирует `ariaLabel`.
|
|
1160
|
-
*/
|
|
1161
|
-
this.tooltip = input('', /* @ts-ignore */
|
|
1162
|
-
...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
|
|
1163
|
-
/**
|
|
1164
|
-
* Tabindex внутренней `<button>`. Default `0` (в обычном таб-порядке).
|
|
1165
|
-
* Значение `-1` исключает кнопку из таб-навигации, оставляя клик мышью —
|
|
1166
|
-
* нужно для вспомогательных аффордансов вроде крестика-очистки внутри поля.
|
|
1167
|
-
*/
|
|
1168
|
-
this.tabIndex = input(0, { ...(ngDevMode ? { debugName: "tabIndex" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1169
|
-
/** Состояние загрузки: показывает spinner вместо иконки + disabled. */
|
|
1170
|
-
this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1171
|
-
/** Нативное отключение кнопки. */
|
|
1172
|
-
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1173
|
-
/** Toggle-состояние — превращается в `aria-pressed="true"`. */
|
|
1174
|
-
this.active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1175
|
-
/** Бейдж-индикатор (dot top-right через BEM-модификатор). */
|
|
1176
|
-
this.indicator = input(false, { ...(ngDevMode ? { debugName: "indicator" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1177
|
-
/** Эмиттит MouseEvent при клике (только когда не disabled и не loading). */
|
|
1178
|
-
this.clicked = output();
|
|
1179
|
-
}
|
|
1180
|
-
onClick(event) {
|
|
1181
|
-
if (this.isDisabled()) {
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
this.clicked.emit(event);
|
|
1185
|
-
}
|
|
1186
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1187
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: RtIconButtonComponent, isStandalone: true, selector: "rt-icon-button", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, iconColor: { classPropertyName: "iconColor", publicName: "iconColor", isSignal: true, isRequired: false, transformFunction: null }, iconFill: { classPropertyName: "iconFill", publicName: "iconFill", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, indicator: { classPropertyName: "indicator", publicName: "indicator", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, host: { classAttribute: "rt-icon-button" }, ngImport: i0, template: "<button\n rtBlock=\"rt-icon-button\"\n qa-dataid=\"icon-button-control\"\n rtRipple\n [rippleDisabled]=\"isDisabled() || loading()\"\n [rtTooltip]=\"tooltip()\"\n [rtMod]=\"bemMods()\"\n [type]=\"type()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"active() ? 'true' : null\"\n [attr.tabindex]=\"tabIndex()\"\n (click)=\"onClick($event)\">\n @if (loading()) {\n <rt-icon rtElem=\"spinner\" name=\"spinner\" color=\"current\" [size]=\"resolvedIconSize()\" />\n } @else {\n <rt-icon rtElem=\"icon\" [color]=\"iconColor()\" [name]=\"icon()\" [size]=\"resolvedIconSize()\" [fill]=\"iconFill()\" />\n }\n @if (indicator()) {\n <span rtElem=\"indicator\" aria-hidden=\"true\"></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-icon-button{display:inline-flex;--rt-icon-button-size: var(--rt-control-height-md);--rt-icon-button-color: var(--rt-color-text-primary)}rt-icon-button .rt-icon-button{--rt-icon-button-circle-radius: var(--rt-radius-full);--rt-icon-button-square-radius: var(--rt-radius-md);--rt-icon-button-rounded-sm-radius: calc(var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) / 3);--rt-icon-button-rounded-lg-radius: calc( var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) * 2 / 3 );--rt-icon-button-indicator-radius: var(--rt-radius-full);--rt-icon-button-focus-shadow: var(--rt-shadow-focus-ring);--rt-icon-button-shadow: var(--rt-shadow-none);--rt-icon-button-shadow-hover: var(--rt-icon-button-shadow);--rt-icon-button-color-hover: var(--rt-icon-button-color);--rt-icon-button-transition-duration: var(--rt-duration-fast);--rt-icon-button-bg: transparent;position:relative;display:inline-flex;width:var(--rt-icon-button-size);height:var(--rt-icon-button-size);align-items:center;justify-content:center;padding:0;border:0;background-color:var(--rt-icon-button-bg);box-shadow:var(--rt-icon-button-shadow);color:var(--rt-icon-button-color);cursor:pointer;transition:background-color var(--rt-icon-button-transition-duration) ease,box-shadow var(--rt-icon-button-transition-duration) ease}rt-icon-button .rt-icon-button--sm{--rt-icon-button-size: var(--rt-control-height-sm)}rt-icon-button .rt-icon-button--md{--rt-icon-button-size: var(--rt-control-height-md)}rt-icon-button .rt-icon-button--lg{--rt-icon-button-size: var(--rt-control-height-lg)}rt-icon-button .rt-icon-button--xl{--rt-icon-button-size: var(--rt-control-height-xl)}rt-icon-button .rt-icon-button--2xl{--rt-icon-button-size: var(--rt-control-height-2xl)}rt-icon-button .rt-icon-button--circle{border-radius:var(--rt-icon-button-circle-radius)}rt-icon-button .rt-icon-button--rounded-sm{border-radius:var(--rt-icon-button-rounded-sm-radius)}rt-icon-button .rt-icon-button--rounded-lg{border-radius:var(--rt-icon-button-rounded-lg-radius)}rt-icon-button .rt-icon-button--square{border-radius:var(--rt-icon-button-square-radius)}rt-icon-button .rt-icon-button:hover:not(:disabled){box-shadow:var(--rt-icon-button-shadow-hover)}rt-icon-button .rt-icon-button--ghost:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover);color:var(--rt-icon-button-color-hover)}rt-icon-button .rt-icon-button--ghost.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--primary{--rt-icon-button-bg: var(--rt-color-action-primary);color:var(--rt-color-action-on-primary)}rt-icon-button .rt-icon-button--primary:hover:not(:disabled){background-color:var(--rt-color-action-primary-hover)}rt-icon-button .rt-icon-button--secondary{--rt-icon-button-bg: var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary)}rt-icon-button .rt-icon-button--secondary:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger{color:var(--rt-color-state-danger)}rt-icon-button .rt-icon-button--danger:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success{color:var(--rt-color-state-success)}rt-icon-button .rt-icon-button--success:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning{color:var(--rt-color-state-warning)}rt-icon-button .rt-icon-button--warning:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button:disabled{background-color:color-mix(in srgb,var(--rt-icon-button-bg) var(--rt-icon-button-disabled-fill),transparent);cursor:not-allowed;opacity:var(--rt-icon-button-disabled-opacity)}rt-icon-button .rt-icon-button:disabled .rt-icon svg{color:var(--rt-icon-button-disabled-color)}rt-icon-button .rt-icon-button:focus-visible{box-shadow:var(--rt-icon-button-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-icon-button .rt-icon-button__spinner{--rt-icon-button-spin-duration: 1s;animation:rt-icon-button-spin var(--rt-icon-button-spin-duration) linear infinite}rt-icon-button .rt-icon-button__indicator{position:absolute;width:8px;height:8px;border-radius:var(--rt-icon-button-indicator-radius);background:var(--rt-color-state-danger);inset-block-start:-2px;inset-inline-end:-2px;pointer-events:none}@keyframes rt-icon-button-spin{to{transform:rotate(360deg)}}}\n"], dependencies: [{ kind: "component", type:
|
|
1188
|
-
// standalone components / directives
|
|
1189
|
-
RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "fill", "rotate"] }, { kind: "directive", type: RtRippleDirective, selector: "[rtRipple]", inputs: ["rippleDisabled"] }, { kind: "directive", type: RtTooltipDirective, selector: "[rtTooltip]", inputs: ["rtTooltip", "rtTooltipPlacement"], exportAs: ["rtTooltip"] }, { kind: "directive", type: BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1190
|
-
}
|
|
1191
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconButtonComponent, decorators: [{
|
|
1192
|
-
type: Component,
|
|
1193
|
-
args: [{ selector: 'rt-icon-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
|
|
1194
|
-
// standalone components / directives
|
|
1195
|
-
RtIconComponent,
|
|
1196
|
-
RtRippleDirective,
|
|
1197
|
-
RtTooltipDirective,
|
|
1198
|
-
BlockDirective,
|
|
1199
|
-
ElemDirective,
|
|
1200
|
-
ModDirective,
|
|
1201
|
-
], host: {
|
|
1202
|
-
class: BEM_BLOCK$1s,
|
|
1203
|
-
}, template: "<button\n rtBlock=\"rt-icon-button\"\n qa-dataid=\"icon-button-control\"\n rtRipple\n [rippleDisabled]=\"isDisabled() || loading()\"\n [rtTooltip]=\"tooltip()\"\n [rtMod]=\"bemMods()\"\n [type]=\"type()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"active() ? 'true' : null\"\n [attr.tabindex]=\"tabIndex()\"\n (click)=\"onClick($event)\">\n @if (loading()) {\n <rt-icon rtElem=\"spinner\" name=\"spinner\" color=\"current\" [size]=\"resolvedIconSize()\" />\n } @else {\n <rt-icon rtElem=\"icon\" [color]=\"iconColor()\" [name]=\"icon()\" [size]=\"resolvedIconSize()\" [fill]=\"iconFill()\" />\n }\n @if (indicator()) {\n <span rtElem=\"indicator\" aria-hidden=\"true\"></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-icon-button{display:inline-flex;--rt-icon-button-size: var(--rt-control-height-md);--rt-icon-button-color: var(--rt-color-text-primary)}rt-icon-button .rt-icon-button{--rt-icon-button-circle-radius: var(--rt-radius-full);--rt-icon-button-square-radius: var(--rt-radius-md);--rt-icon-button-rounded-sm-radius: calc(var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) / 3);--rt-icon-button-rounded-lg-radius: calc( var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) * 2 / 3 );--rt-icon-button-indicator-radius: var(--rt-radius-full);--rt-icon-button-focus-shadow: var(--rt-shadow-focus-ring);--rt-icon-button-shadow: var(--rt-shadow-none);--rt-icon-button-shadow-hover: var(--rt-icon-button-shadow);--rt-icon-button-color-hover: var(--rt-icon-button-color);--rt-icon-button-transition-duration: var(--rt-duration-fast);--rt-icon-button-bg: transparent;position:relative;display:inline-flex;width:var(--rt-icon-button-size);height:var(--rt-icon-button-size);align-items:center;justify-content:center;padding:0;border:0;background-color:var(--rt-icon-button-bg);box-shadow:var(--rt-icon-button-shadow);color:var(--rt-icon-button-color);cursor:pointer;transition:background-color var(--rt-icon-button-transition-duration) ease,box-shadow var(--rt-icon-button-transition-duration) ease}rt-icon-button .rt-icon-button--sm{--rt-icon-button-size: var(--rt-control-height-sm)}rt-icon-button .rt-icon-button--md{--rt-icon-button-size: var(--rt-control-height-md)}rt-icon-button .rt-icon-button--lg{--rt-icon-button-size: var(--rt-control-height-lg)}rt-icon-button .rt-icon-button--xl{--rt-icon-button-size: var(--rt-control-height-xl)}rt-icon-button .rt-icon-button--2xl{--rt-icon-button-size: var(--rt-control-height-2xl)}rt-icon-button .rt-icon-button--circle{border-radius:var(--rt-icon-button-circle-radius)}rt-icon-button .rt-icon-button--rounded-sm{border-radius:var(--rt-icon-button-rounded-sm-radius)}rt-icon-button .rt-icon-button--rounded-lg{border-radius:var(--rt-icon-button-rounded-lg-radius)}rt-icon-button .rt-icon-button--square{border-radius:var(--rt-icon-button-square-radius)}rt-icon-button .rt-icon-button:hover:not(:disabled){box-shadow:var(--rt-icon-button-shadow-hover)}rt-icon-button .rt-icon-button--ghost:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover);color:var(--rt-icon-button-color-hover)}rt-icon-button .rt-icon-button--ghost.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--primary{--rt-icon-button-bg: var(--rt-color-action-primary);color:var(--rt-color-action-on-primary)}rt-icon-button .rt-icon-button--primary:hover:not(:disabled){background-color:var(--rt-color-action-primary-hover)}rt-icon-button .rt-icon-button--secondary{--rt-icon-button-bg: var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary)}rt-icon-button .rt-icon-button--secondary:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger{color:var(--rt-color-state-danger)}rt-icon-button .rt-icon-button--danger:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success{color:var(--rt-color-state-success)}rt-icon-button .rt-icon-button--success:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning{color:var(--rt-color-state-warning)}rt-icon-button .rt-icon-button--warning:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button:disabled{background-color:color-mix(in srgb,var(--rt-icon-button-bg) var(--rt-icon-button-disabled-fill),transparent);cursor:not-allowed;opacity:var(--rt-icon-button-disabled-opacity)}rt-icon-button .rt-icon-button:disabled .rt-icon svg{color:var(--rt-icon-button-disabled-color)}rt-icon-button .rt-icon-button:focus-visible{box-shadow:var(--rt-icon-button-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-icon-button .rt-icon-button__spinner{--rt-icon-button-spin-duration: 1s;animation:rt-icon-button-spin var(--rt-icon-button-spin-duration) linear infinite}rt-icon-button .rt-icon-button__indicator{position:absolute;width:8px;height:8px;border-radius:var(--rt-icon-button-indicator-radius);background:var(--rt-color-state-danger);inset-block-start:-2px;inset-inline-end:-2px;pointer-events:none}@keyframes rt-icon-button-spin{to{transform:rotate(360deg)}}}\n"] }]
|
|
1204
|
-
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], iconColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconColor", required: false }] }], iconFill: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconFill", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], iconSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconSize", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], indicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicator", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
1205
|
-
|
|
1206
|
-
/**
|
|
1207
|
-
* Хэндл к программно открытой модалке (через `RtDialogService.open()`).
|
|
1208
|
-
*
|
|
1209
|
-
* Контент-компонент инжектит `RtDialogRef` и закрывает модалку через `.close(result?)`.
|
|
1210
|
-
* Родитель, открывший модалку, ждёт результата через `.afterClosed()`.
|
|
1211
|
-
*
|
|
1212
|
-
* Generic `<T>` — тип результата `close(result)`. Дефолт `unknown` — если результат
|
|
1213
|
-
* не нужен, оставляй generic пустым.
|
|
1214
|
-
*
|
|
1215
|
-
* @example
|
|
1216
|
-
* \`\`\`ts
|
|
1217
|
-
* // в контент-компоненте
|
|
1218
|
-
* private readonly dialogRef = inject(RtDialogRef);
|
|
1219
|
-
* close(): void { this.dialogRef.close({ confirmed: true }); }
|
|
1220
|
-
*
|
|
1221
|
-
* // в родителе
|
|
1222
|
-
* dialogService.open(MyDialogComponent).afterClosed().subscribe((result) => {...});
|
|
1223
|
-
* \`\`\`
|
|
1224
|
-
*/
|
|
1225
|
-
class RtDialogRef {
|
|
1226
|
-
#overlayRef;
|
|
1227
|
-
#afterClosedSource;
|
|
1228
|
-
constructor(overlayRef) {
|
|
1229
|
-
/**
|
|
1230
|
-
* Runtime-флаг, блокирующий close() через ESC / backdrop click.
|
|
1231
|
-
*
|
|
1232
|
-
* Default `false`. Включай (`dialogRef.disableClose.set(true)`) на
|
|
1233
|
-
* время in-flight submit'а, чтобы юзер не потерял введённые в форму
|
|
1234
|
-
* данные на случайном ESC. После next/error subscribe ставь обратно
|
|
1235
|
-
* в `false`. Не влияет на программный `close(result)` — он всегда
|
|
1236
|
-
* закрывает overlay.
|
|
1237
|
-
*
|
|
1238
|
-
* Статические config-флаги `closeOnEscape` / `closeOnBackdropClick`
|
|
1239
|
-
* (см. `IRtDialogConfig`) определяют, подписан ли вообще handler на
|
|
1240
|
-
* соответствующее событие; `disableClose` — это runtime-override,
|
|
1241
|
-
* который гейтит dispatch внутри уже-подписанного handler'а.
|
|
1242
|
-
*/
|
|
1243
|
-
this.disableClose = signal(false, /* @ts-ignore */
|
|
1244
|
-
...(ngDevMode ? [{ debugName: "disableClose" }] : /* istanbul ignore next */ []));
|
|
1245
|
-
this.#afterClosedSource = new Subject();
|
|
1246
|
-
this.#overlayRef = overlayRef;
|
|
1247
|
-
}
|
|
1248
|
-
/** Закрывает overlay; опционально передаёт результат подписчикам `afterClosed()`. */
|
|
1249
|
-
close(result) {
|
|
1250
|
-
this.#overlayRef.dispose();
|
|
1251
|
-
this.#afterClosedSource.next(result);
|
|
1252
|
-
this.#afterClosedSource.complete();
|
|
1253
|
-
}
|
|
1254
|
-
/** Эмиттит один раз — результат `close(result)` — и завершается. */
|
|
1255
|
-
afterClosed() {
|
|
1256
|
-
return this.#afterClosedSource.asObservable();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Injection token для передачи данных в модалку, открытую через `RtDialogService.open()`.
|
|
1262
|
-
*
|
|
1263
|
-
* Пользовательский компонент-контент модалки инжектит токен и получает данные,
|
|
1264
|
-
* переданные в `DialogConfig.data`. Тип значения задаётся generic'ом — `inject(RT_DIALOG_DATA)`
|
|
1265
|
-
* вернёт `unknown`, прокастуй через `inject(RT_DIALOG_DATA) as MyDataType` ИЛИ
|
|
1266
|
-
* объяви токен с конкретным generic'ом в feature-либе.
|
|
1267
|
-
*
|
|
1268
|
-
* @example
|
|
1269
|
-
* \`\`\`ts
|
|
1270
|
-
* @Component({...})
|
|
1271
|
-
* export class WelcomeModalComponent {
|
|
1272
|
-
* readonly data = inject(RT_DIALOG_DATA) as { userName: string };
|
|
1273
|
-
* }
|
|
1274
|
-
*
|
|
1275
|
-
* // parent
|
|
1276
|
-
* dialogService.open(WelcomeModalComponent, { data: { userName: "Иван" } });
|
|
1277
|
-
* \`\`\`
|
|
1278
|
-
*/
|
|
1279
|
-
const RT_DIALOG_DATA = new InjectionToken('RT_DIALOG_DATA');
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Программный API для открытия модалок поверх CDK Overlay.
|
|
1283
|
-
*
|
|
1284
|
-
* Устройство сервиса:
|
|
1285
|
-
* - `Overlay.position().global().centerHorizontally().centerVertically()` — центрирование.
|
|
1286
|
-
* - `scrollStrategies.block()` — фиксирует background-scroll.
|
|
1287
|
-
* - `hasBackdrop: true` + кастомный класс — затемнение.
|
|
1288
|
-
* - `ComponentPortal` + custom `Injector` подкидывает `RtDialogRef` + `RT_DIALOG_DATA` в DI
|
|
1289
|
-
* контент-компонента.
|
|
1290
|
-
*
|
|
1291
|
-
* Альтернатива: legacy inline `<rt-dialog [visible]>` форма продолжает работать через
|
|
1292
|
-
* native `<dialog>.showModal()`. Новые экраны — на программный API.
|
|
1293
|
-
*
|
|
1294
|
-
* @example
|
|
1295
|
-
* \`\`\`ts
|
|
1296
|
-
* // открытие
|
|
1297
|
-
* private readonly dialogService = inject(RtDialogService);
|
|
1298
|
-
*
|
|
1299
|
-
* openConfirm(): void {
|
|
1300
|
-
* const ref = this.dialogService.open<ConfirmDialogComponent, { question: string }, boolean>(
|
|
1301
|
-
* ConfirmDialogComponent,
|
|
1302
|
-
* { data: { question: "Удалить?" } }
|
|
1303
|
-
* );
|
|
1304
|
-
* ref.afterClosed().subscribe((confirmed) => {
|
|
1305
|
-
* if (confirmed) this.delete();
|
|
1306
|
-
* });
|
|
1307
|
-
* }
|
|
1308
|
-
*
|
|
1309
|
-
* // контент-компонент
|
|
1310
|
-
* @Component({
|
|
1311
|
-
* selector: "rt-confirm-dialog",
|
|
1312
|
-
* template: \`
|
|
1313
|
-
* <rt-dialog size="sm">
|
|
1314
|
-
* <rt-dialog-header [title]="data.question" />
|
|
1315
|
-
* <div>Действие необратимо.</div>
|
|
1316
|
-
* <rt-dialog-footer>
|
|
1317
|
-
* <button rtButton (click)="close(false)">Отмена</button>
|
|
1318
|
-
* <button rtButton theme="danger" (click)="close(true)">Удалить</button>
|
|
1319
|
-
* </rt-dialog-footer>
|
|
1320
|
-
* </rt-dialog>
|
|
1321
|
-
* \`,
|
|
1322
|
-
* })
|
|
1323
|
-
* export class ConfirmDialogComponent {
|
|
1324
|
-
* readonly data = inject(RT_DIALOG_DATA) as { question: string };
|
|
1325
|
-
* readonly #ref = inject(RtDialogRef<boolean>);
|
|
1326
|
-
* close(result: boolean): void { this.#ref.close(result); }
|
|
1327
|
-
* }
|
|
1328
|
-
* \`\`\`
|
|
1329
|
-
*/
|
|
1330
|
-
class RtDialogService {
|
|
1331
|
-
#overlay = inject(Overlay);
|
|
1332
|
-
#injector = inject(Injector);
|
|
1333
|
-
#document = inject(DOCUMENT);
|
|
1334
|
-
#openSource = new Subject();
|
|
1335
|
-
constructor() {
|
|
1336
|
-
// Per-open close-подписки (backdrop / ESC) живут в одном постоянном
|
|
1337
|
-
// конструкторном стриме: open() эмитит контекст открытия, mergeMap
|
|
1338
|
-
// подписывает event-стримы конкретного OverlayRef. `detachments()`
|
|
1339
|
-
// терминирует внутренний поток при overlay.dispose() (дёргается из
|
|
1340
|
-
// RtDialogRef.close()). mergeMap, а не switchMap — одновременно может
|
|
1341
|
-
// быть открыто несколько модалок (стек).
|
|
1342
|
-
//
|
|
1343
|
-
// Внутри обоих handler'ов — runtime-guard через `dialogRef.disableClose()`:
|
|
1344
|
-
// если контент-компонент выставил `disableClose.set(true)` (например, на
|
|
1345
|
-
// время in-flight submit'а), ESC / backdrop НЕ закрывают overlay.
|
|
1346
|
-
// Программный `dialogRef.close(result)` гейтом не задет — он всегда работает.
|
|
1347
|
-
this.#openSource
|
|
1348
|
-
.pipe(mergeMap((openContext) => {
|
|
1349
|
-
const backdropClose$ = openContext.closeOnBackdropClick
|
|
1350
|
-
? openContext.overlayRef.backdropClick().pipe(map(() => () => {
|
|
1351
|
-
if (openContext.dialogRef.disableClose()) {
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
openContext.dialogRef.close();
|
|
1355
|
-
}))
|
|
1356
|
-
: EMPTY;
|
|
1357
|
-
const escapeClose$ = openContext.closeOnEscape
|
|
1358
|
-
? openContext.overlayRef.keydownEvents().pipe(map((event) => () => {
|
|
1359
|
-
if (event.key !== 'Escape') {
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
event.preventDefault();
|
|
1363
|
-
if (openContext.dialogRef.disableClose()) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
openContext.dialogRef.close();
|
|
1367
|
-
}))
|
|
1368
|
-
: EMPTY;
|
|
1369
|
-
return merge(backdropClose$, escapeClose$).pipe(takeUntil(openContext.overlayRef.detachments()));
|
|
1370
|
-
}), takeUntilDestroyed())
|
|
1371
|
-
.subscribe((handleCloseIntent) => handleCloseIntent());
|
|
1372
|
-
}
|
|
1373
|
-
open(component, config) {
|
|
1374
|
-
const overlayRef = this.#overlay.create({
|
|
1375
|
-
positionStrategy: this.#overlay.position().global().centerHorizontally().centerVertically(),
|
|
1376
|
-
scrollStrategy: this.#overlay.scrollStrategies.block(),
|
|
1377
|
-
hasBackdrop: true,
|
|
1378
|
-
backdropClass: config?.backdropClass ?? 'rt-dialog-backdrop',
|
|
1379
|
-
panelClass: [config?.panelClass ?? 'rt-dialog-overlay', materialPresetClassesOfFocus(this.#document)].flat(),
|
|
1380
|
-
disposeOnNavigation: true,
|
|
1381
|
-
});
|
|
1382
|
-
// До attach: содержимое диалога может забрать фокус, и кусок темы у кнопки будет потерян.
|
|
1383
|
-
carryThemeScopeOfFocus(overlayRef.overlayElement, this.#document);
|
|
1384
|
-
const dialogRef = new RtDialogRef(overlayRef);
|
|
1385
|
-
// Сами close-подписки объявлены один раз в конструкторе — здесь только
|
|
1386
|
-
// эмит контекста открытия (см. #openSource-стрим).
|
|
1387
|
-
this.#openSource.next({
|
|
1388
|
-
overlayRef,
|
|
1389
|
-
dialogRef,
|
|
1390
|
-
closeOnBackdropClick: config?.closeOnBackdropClick !== false,
|
|
1391
|
-
closeOnEscape: config?.closeOnEscape !== false,
|
|
1392
|
-
});
|
|
1393
|
-
const injector = Injector.create({
|
|
1394
|
-
parent: this.#injector,
|
|
1395
|
-
providers: [
|
|
1396
|
-
{ provide: RtDialogRef, useValue: dialogRef },
|
|
1397
|
-
{ provide: RT_DIALOG_DATA, useValue: config?.data ?? null },
|
|
1398
|
-
],
|
|
1399
|
-
});
|
|
1400
|
-
const portal = new ComponentPortal(component, null, injector);
|
|
1401
|
-
overlayRef.attach(portal);
|
|
1402
|
-
return dialogRef;
|
|
1403
|
-
}
|
|
1404
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1405
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, providedIn: 'root' }); }
|
|
1406
|
-
}
|
|
1407
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, decorators: [{
|
|
1408
|
-
type: Injectable,
|
|
1409
|
-
args: [{ providedIn: 'root' }]
|
|
1410
|
-
}], ctorParameters: () => [] });
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* Настройки кита, которые приложение дало на старте.
|
|
1414
|
-
*
|
|
1415
|
-
* Токен объявлен **значением**, а не сигналом, и этим отличается от подписей и
|
|
1416
|
-
* от темы: настройки — снимок старта. Их читают один раз, когда узел создан, и
|
|
1417
|
-
* поздняя подмена на уже нарисованное не действует. Тому, что меняется на живой
|
|
1418
|
-
* странице, место во входе узла или в службе темы.
|
|
1419
|
-
*
|
|
1420
|
-
* Умолчание — пустой объект: кит без единой настройки рисует то же, что рисовал
|
|
1421
|
-
* до их появления.
|
|
1422
|
-
*/
|
|
1423
|
-
const RT_KIT_CONFIG = new InjectionToken('RT_KIT_CONFIG', {
|
|
1424
|
-
providedIn: 'root',
|
|
1425
|
-
factory: () => ({}),
|
|
1426
|
-
});
|
|
1427
|
-
/**
|
|
1428
|
-
* Разрешение умолчания — одно место на весь кит, а не повтор в каждом узле.
|
|
1429
|
-
*
|
|
1430
|
-
* Порядок от частного к общему: вход на месте вызова, умолчание узла, общее
|
|
1431
|
-
* умолчание, умолчание кита. Первый найденный ответ выигрывает, поиск на нём
|
|
1432
|
-
* останавливается. Вход на месте сюда не приходит вовсе — он перебивает всё
|
|
1433
|
-
* тем, что значение отсюда становится лишь тем, **с чего вход стартует**.
|
|
1434
|
-
*
|
|
1435
|
-
* Зовётся в поле класса: там есть контекст внедрения.
|
|
1436
|
-
*
|
|
1437
|
-
* ```typescript
|
|
1438
|
-
* readonly #size: IButton.Size = rtKitDefault('button', (it: IRtKitConfig.Button): IButton.Size | undefined => it.size, 'md');
|
|
1439
|
-
* public readonly size: InputSignal<IButton.Size> = input<IButton.Size>(this.#size);
|
|
1440
|
-
* ```
|
|
1441
|
-
*
|
|
1442
|
-
* @param node Имя узла — ключ раздела умолчаний по узлам.
|
|
1443
|
-
* @param pick Что взять из умолчаний этого узла.
|
|
1444
|
-
* @param fallback Умолчание кита: то, что рисуется, когда не сказано ничего.
|
|
1445
|
-
* @param pickCommon Что взять из общих умолчаний, если поле живёт и там. Поля,
|
|
1446
|
-
* стоящего на обоих уровнях, сегодня нет ни одного — ступень объявлена под
|
|
1447
|
-
* поле, которое назовёт владелец, и до тех пор не зовётся.
|
|
1448
|
-
*/
|
|
1449
|
-
function rtKitDefault(node, pick, fallback, pickCommon) {
|
|
1450
|
-
const config = inject(RT_KIT_CONFIG);
|
|
1451
|
-
const nodeSettings = config.components?.[node];
|
|
1452
|
-
const fromNode = nodeSettings === undefined ? undefined : pick(nodeSettings);
|
|
1453
|
-
if (fromNode !== undefined) {
|
|
1454
|
-
return fromNode;
|
|
1455
|
-
}
|
|
1456
|
-
const global = config.global;
|
|
1457
|
-
const fromCommon = pickCommon === undefined || global === undefined ? undefined : pickCommon(global);
|
|
1458
|
-
return fromCommon ?? fallback;
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Отдаёт киту настройки приложения.
|
|
1462
|
-
*
|
|
1463
|
-
* Раздача необязательна: без неё работают умолчания кита. Частично заданный
|
|
1464
|
-
* объект не обнуляет остальное — неназванное поле проваливается на уровень
|
|
1465
|
-
* выше.
|
|
1466
|
-
*
|
|
1467
|
-
* ```typescript
|
|
1468
|
-
* export const appConfig: ApplicationConfig = {
|
|
1469
|
-
* providers: [
|
|
1470
|
-
* provideRtKit({
|
|
1471
|
-
* global: { theme: 'auto' },
|
|
1472
|
-
* components: { button: { size: 'lg' }, aside: { closeOnEscape: false } },
|
|
1473
|
-
* }),
|
|
1474
|
-
* ],
|
|
1475
|
-
* };
|
|
1476
|
-
* ```
|
|
1477
|
-
*
|
|
1478
|
-
* @param config Настройки целиком. Всё в них необязательно.
|
|
1479
|
-
*/
|
|
1480
|
-
function provideRtKit(config) {
|
|
1481
|
-
return makeEnvironmentProviders([{ provide: RT_KIT_CONFIG, useValue: config }]);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
/**
|
|
1485
|
-
* Категории иконок rt-icon. 11 buckets: 8 prefix-based + Social (префикс `social-`) + Custom
|
|
1486
|
-
* (префикс `ico-`) + Misc (fallback).
|
|
1487
|
-
* Порядок фиксирован — соответствует порядку отображения в Catalog story (rt-icon.stories.ts).
|
|
1488
|
-
*
|
|
1489
|
-
* Аннотация типа намеренно не указывается: явный `readonly string[]` потерял бы узкий
|
|
1490
|
-
* литеральный тип, нужный для {@link TIconCategory}. `as const` сохраняет литеральные ключи.
|
|
1491
|
-
*/
|
|
1492
|
-
// eslint-disable-next-line @typescript-eslint/typedef -- аннотация стёрла бы литеральный тип, на котором стоит TIconCategory
|
|
1493
|
-
const CATEGORY_ORDER = [
|
|
1494
|
-
'Navigation',
|
|
1495
|
-
'Alignment & Sorting',
|
|
1496
|
-
'Status',
|
|
1497
|
-
'Time & Calendar',
|
|
1498
|
-
'Communication',
|
|
1499
|
-
'Documents',
|
|
1500
|
-
'People',
|
|
1501
|
-
'Commerce',
|
|
1502
|
-
'Social',
|
|
1503
|
-
'Custom',
|
|
1504
|
-
'Misc',
|
|
1505
|
-
];
|
|
1506
|
-
function startsWithAny(name, prefixes) {
|
|
1507
|
-
return prefixes.some((p) => name.startsWith(p));
|
|
1508
|
-
}
|
|
1509
|
-
function isOneOf(name, names) {
|
|
1510
|
-
return names.includes(name);
|
|
1511
|
-
}
|
|
1512
|
-
function matchPrefixOrExact(prefixes, exact) {
|
|
1513
|
-
return (name) => startsWithAny(name, prefixes) || isOneOf(name, exact);
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Упорядоченный список правил категоризации.
|
|
1517
|
-
* `Custom` — единственная категория по строгому префиксу `ico-` и проверяется первой,
|
|
1518
|
-
* чтобы исключить конфликты с другими prefix-based категориями.
|
|
1519
|
-
* `Misc` — fallback в {@link categoryOf}, отдельным правилом не описывается.
|
|
1520
|
-
*/
|
|
1521
|
-
const ICON_CATEGORY_RULES = [
|
|
1522
|
-
// Знаки соцсетей — цветные, со своей заливкой в файле; в каталоге они стоят своим рядом,
|
|
1523
|
-
// чтобы не смешиваться с одноцветными, которые красит тема.
|
|
1524
|
-
{ category: 'Social', match: (n) => n.startsWith('social-') },
|
|
1525
|
-
{ category: 'Custom', match: (n) => n.startsWith('ico-') },
|
|
1526
|
-
{
|
|
1527
|
-
category: 'Navigation',
|
|
1528
|
-
match: matchPrefixOrExact(['arrow', 'arrows', 'angle', 'chevron', 'directions'], []),
|
|
1529
|
-
},
|
|
1530
|
-
{
|
|
1531
|
-
category: 'Alignment & Sorting',
|
|
1532
|
-
match: matchPrefixOrExact(['align', 'sort', 'bars', 'ellipsis', 'sliders', 'filter', 'th'], []),
|
|
1533
|
-
},
|
|
1534
|
-
{
|
|
1535
|
-
category: 'Status',
|
|
1536
|
-
match: matchPrefixOrExact(['check', 'times', 'ban', 'exclamation', 'info', 'eye', 'lock', 'unlock', 'shield'], ['spinner', 'stop']),
|
|
1537
|
-
},
|
|
1538
|
-
{
|
|
1539
|
-
category: 'Time & Calendar',
|
|
1540
|
-
match: matchPrefixOrExact(['alarm', 'calendar'], ['clock']),
|
|
1541
|
-
},
|
|
1542
|
-
{
|
|
1543
|
-
category: 'Communication',
|
|
1544
|
-
match: matchPrefixOrExact(['comment', 'share'], ['bell', 'bot', 'telegram', 'at', 'email', 'send', 'phone', 'mobile']),
|
|
1545
|
-
},
|
|
1546
|
-
{
|
|
1547
|
-
category: 'Documents',
|
|
1548
|
-
match: matchPrefixOrExact(['attach', 'file', 'folder', 'bookmark'], ['book', 'copy', 'clone', 'tag', 'tags']),
|
|
1549
|
-
},
|
|
1550
|
-
{
|
|
1551
|
-
category: 'People',
|
|
1552
|
-
match: matchPrefixOrExact(['user'], ['users', 'id-card']),
|
|
1553
|
-
},
|
|
1554
|
-
{
|
|
1555
|
-
category: 'Commerce',
|
|
1556
|
-
match: (n) => isOneOf(n, ['wallet', 'money-bill', 'ticket', 'credit-card']),
|
|
1557
|
-
},
|
|
1558
|
-
];
|
|
1559
|
-
/**
|
|
1560
|
-
* Возвращает категорию для имени иконки.
|
|
1561
|
-
* Первое совпавшее правило в {@link ICON_CATEGORY_RULES} выигрывает; иначе — `Misc`.
|
|
1562
|
-
*/
|
|
1563
|
-
function categoryOf(name) {
|
|
1564
|
-
for (const rule of ICON_CATEGORY_RULES) {
|
|
1565
|
-
if (rule.match(name)) {
|
|
1566
|
-
return rule.category;
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
return 'Misc';
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
/**
|
|
1573
|
-
* Имена иконок rt-icon. Набор лежит в `assets/icons` пакета, откуда сборка
|
|
1574
|
-
* приложения кладёт его туда, куда указывает `provideRtIcons()`. Большинство
|
|
1575
|
-
* файлов пришло из фрейма iconsLib
|
|
1576
|
-
* (https://www.figma.com/design/x1rxhwomHt1pQRUcM3Zqjb?node-id=2922-7969).
|
|
1577
|
-
*
|
|
1578
|
-
* При добавлении новой иконки: положить SVG в `assets/icons` пакета, добавить
|
|
1579
|
-
* slug сюда, прогнать pnpm exec eslint --fix + pnpm exec prettier --write по этому файлу.
|
|
1580
|
-
* Имя без файла гасит только свой значок: реестр ходит за каждым именем отдельно, и промах
|
|
1581
|
-
* виден пустым местом на экране, а не отказом в прогоне.
|
|
1582
|
-
*
|
|
1583
|
-
* Аннотация типа намеренно не указывается: явный `readonly string[]` потерял бы узкий
|
|
1584
|
-
* литеральный тип, нужный для `IRtIcon.Name` (см. `rt-icon.model.ts`). `as const`
|
|
1585
|
-
* сохраняет литеральные ключи.
|
|
117
|
+
* При добавлении новой иконки: положить SVG в `assets/icons` пакета, добавить
|
|
118
|
+
* slug сюда, прогнать pnpm exec eslint --fix + pnpm exec prettier --write по этому файлу.
|
|
119
|
+
* Имя без файла гасит только свой значок: реестр ходит за каждым именем отдельно, и промах
|
|
120
|
+
* виден пустым местом на экране, а не отказом в прогоне.
|
|
121
|
+
*
|
|
122
|
+
* Аннотация типа намеренно не указывается: явный `readonly string[]` потерял бы узкий
|
|
123
|
+
* литеральный тип, нужный для `IRtIcon.Name` (см. `rt-icon.model.ts`). `as const`
|
|
124
|
+
* сохраняет литеральные ключи.
|
|
1586
125
|
*/
|
|
1587
126
|
// eslint-disable-next-line @typescript-eslint/typedef -- аннотация стёрла бы литеральный тип, на котором стоит IRtIcon.Name
|
|
1588
127
|
const iconsName = [
|
|
@@ -1939,35 +478,1564 @@ const iconsName = [
|
|
|
1939
478
|
];
|
|
1940
479
|
|
|
1941
480
|
/**
|
|
1942
|
-
*
|
|
481
|
+
* Соответствие значков первого кита значкам этого набора.
|
|
482
|
+
*
|
|
483
|
+
* Первый кит рисует значки шрифтом Material: имя стоит прямо в шаблоне. Здесь у каждого такого
|
|
484
|
+
* имени ровно одна пара — иначе один и тот же смысл приезжает на разные экраны разными
|
|
485
|
+
* рисунками, а переезжающий выбирает значок заново на каждом экране.
|
|
486
|
+
*
|
|
487
|
+
* Перечень закрывает имена, которые видны поиском: голое слово в шаблоне, слово в тернарнике и
|
|
488
|
+
* слово внутри разбора по виду фильтра. Имена, подставляемые выражением, сюда не попадают: они
|
|
489
|
+
* приезжают из данных приложения и деревом кита не видны. Это граница перечня, а не его пробел.
|
|
490
|
+
*
|
|
491
|
+
* Пара ведёт на имя из фрейма, а не на своё с приставкой `ico-`, там где есть оба. Имён из
|
|
492
|
+
* фрейма 229 против 118, и они одной рисовки: смешанный набор дал бы разнобой на экране. Восемь
|
|
493
|
+
* таких пар помечены доводом — их можно переложить на свои одним местом.
|
|
494
|
+
*
|
|
495
|
+
* Имя без пары стоит здесь же, с причиной: молчание об имени неотличимо от того, что о нём
|
|
496
|
+
* забыли. Дорисовка недостающего — работа рисующего, и она вынесена за границу задачи.
|
|
497
|
+
*
|
|
498
|
+
* Держит перечень `tools/check-icon-map.mjs`: каждая пара ведёт на имя союза и на существующий
|
|
499
|
+
* файл, имена не повторяются, причина у имени без пары не пуста.
|
|
500
|
+
*/
|
|
501
|
+
/** Довод самой частой пары: имя первого кита и имя набора совпали после замены подчёркиваний. */
|
|
502
|
+
const SAME_NAME = 'совпадает именем';
|
|
503
|
+
const iconMaterialMap = [
|
|
504
|
+
{ from: 'add', to: 'ico-plus', why: 'простой плюс есть только в своей рисовке — во фрейме плюс лишь в круге и в составных' },
|
|
505
|
+
{ from: 'arrow_back', to: 'arrow-left', why: 'стрелка влево, один в один' },
|
|
506
|
+
{ from: 'arrow_forward', to: 'arrow-right', why: 'стрелка вправо, один в один' },
|
|
507
|
+
{ from: 'backup', to: 'cloud-upload', why: 'у Material backup — облако со стрелкой вверх, это оно и есть' },
|
|
508
|
+
{ from: 'block', to: 'ban', why: 'круг с перечёркиванием, один в один' },
|
|
509
|
+
{ from: 'chevron_left', to: 'chevron-left', why: SAME_NAME },
|
|
510
|
+
{ from: 'chevron_right', to: 'chevron-right', why: SAME_NAME },
|
|
511
|
+
{ from: 'close', to: 'close', why: `${SAME_NAME}; есть двойник ico-close` },
|
|
512
|
+
{ from: 'content_copy', to: 'copy', why: 'два листа, один в один; есть двойник ico-copy' },
|
|
513
|
+
{ from: 'delete', to: 'trash', why: 'корзина, один в один; есть двойник ico-trash' },
|
|
514
|
+
{ from: 'delete_forever', to: null, why: 'корзины с крестом в наборе нет — нужен рисунок' },
|
|
515
|
+
{ from: 'done', to: 'check', why: 'галочка, один в один; есть двойник ico-check' },
|
|
516
|
+
{ from: 'email', to: 'email', why: 'конверт, один в один' },
|
|
517
|
+
{ from: 'download', to: 'ico-download', why: 'простая стрелка вниз есть только в своей рисовке — во фрейме лишь облако' },
|
|
518
|
+
{
|
|
519
|
+
from: 'drag_handle',
|
|
520
|
+
to: 'equals',
|
|
521
|
+
why: 'две горизонтальные полосы; рисунок дорисован по слову владельца — им таблица первого кита показывает сравнение «равно»',
|
|
522
|
+
},
|
|
523
|
+
{ from: 'edit', to: 'pencil', why: 'карандаш, один в один' },
|
|
524
|
+
{ from: 'info', to: 'info-circle', why: 'круг с буквой i — так рисует Material; голое info в наборе другое' },
|
|
525
|
+
{ from: 'keyboard_arrow_down', to: 'chevron-down', why: 'шеврон вниз, один в один; есть двойник ico-chevron-down' },
|
|
526
|
+
{ from: 'local_offer', to: 'tag', why: 'ярлык, один в один' },
|
|
527
|
+
{ from: 'menu', to: 'bars', why: 'три полосы, один в один' },
|
|
528
|
+
{ from: 'more_horiz', to: 'ellipsis-h', why: 'три точки по горизонтали' },
|
|
529
|
+
{ from: 'more_vert', to: 'ellipsis-v', why: 'три точки по вертикали' },
|
|
530
|
+
{ from: 'open_in_new', to: 'external-link', why: 'квадрат со стрелкой наружу' },
|
|
531
|
+
{
|
|
532
|
+
from: 'open_with',
|
|
533
|
+
to: 'arrows',
|
|
534
|
+
why: 'четыре стрелки из центра; рисунок дорисован — им панель колонок первого кита показывает ручку перетаскивания',
|
|
535
|
+
},
|
|
536
|
+
{ from: 'person', to: 'user', why: 'силуэт человека, один в один; есть двойник ico-user' },
|
|
537
|
+
{ from: 'person_off', to: 'user-off', why: 'силуэт человека, перечёркнутый наискось; рисунок дорисован по слову владельца' },
|
|
538
|
+
{ from: 'refresh', to: 'refresh', why: SAME_NAME },
|
|
539
|
+
{ from: 'search', to: 'search', why: `${SAME_NAME}; есть двойник ico-search` },
|
|
540
|
+
{ from: 'settings', to: 'cog', why: 'шестерёнка, один в один' },
|
|
541
|
+
{ from: 'star', to: 'star', why: SAME_NAME },
|
|
542
|
+
{ from: 'sync', to: 'sync', why: SAME_NAME },
|
|
543
|
+
{ from: 'view_column', to: 'table', why: 'ближний: у Material это колонки, здесь таблица — решение владельца' },
|
|
544
|
+
{ from: 'visibility', to: 'eye', why: 'глаз, один в один; есть двойник ico-eye' },
|
|
545
|
+
{ from: 'visibility_off', to: 'eye-slash', why: 'перечёркнутый глаз, один в один' },
|
|
546
|
+
];
|
|
547
|
+
/**
|
|
548
|
+
* Имена кита, у которых в материальном наборе лежит свой рисунок.
|
|
549
|
+
*
|
|
550
|
+
* Считается из перечня, а не пишется списком: свой список разошёлся бы с перечнем молча — реестр
|
|
551
|
+
* ходил бы за файлом, которого нет, или не ходил бы за лежащим.
|
|
552
|
+
*/
|
|
553
|
+
const iconMaterialDrawn = new Set(iconMaterialMap
|
|
554
|
+
.map((entry) => entry.to)
|
|
555
|
+
.filter((name) => name !== null));
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Имена, которыми набор значков виден со стороны страницы.
|
|
559
|
+
*
|
|
560
|
+
* Отдельный файл, а не константы реестра: те же имена читает обвязка снимков витрины, а она идёт
|
|
561
|
+
* под Playwright и ни одного объявления Angular поднять не может. Своя копия там уже стояла —
|
|
562
|
+
* второе объявление того же значения расходится с первым молча: ожидание набора ищет один узел,
|
|
563
|
+
* а реестр создаёт другой, и кадр выходит без значков.
|
|
564
|
+
*/
|
|
565
|
+
/** Селектор inline-sprite, создаваемого один раз при первом обращении. */
|
|
566
|
+
const RT_ICON_SPRITE_ID = 'rt-icon-sprite';
|
|
567
|
+
/** Префикс `id` для inline-sprite символов. Изолирует от внешних `id` на странице. */
|
|
568
|
+
const RT_ICON_SYMBOL_ID_PREFIX = 'rt-icon-';
|
|
569
|
+
/**
|
|
570
|
+
* Префикс `id` символов материального набора. Свой, а не общий: наборов на странице бывает два —
|
|
571
|
+
* признак набора стоит и на корне, и на контейнере, — а один `id` держит один рисунок. С общим
|
|
572
|
+
* префиксом второй набор молча забирал бы символ первого.
|
|
573
|
+
*/
|
|
574
|
+
const RT_ICON_MATERIAL_SYMBOL_ID_PREFIX = 'rt-icon-material-';
|
|
575
|
+
/** Префикс `id` залитых рисунков материального набора: у имени их два, и `id` у каждого свой. */
|
|
576
|
+
const RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX = 'rt-icon-material-fill-';
|
|
577
|
+
/** Селекторы, которыми страница объявляет материальный набор. */
|
|
578
|
+
const RT_ICON_MATERIAL_PRESET_SELECTOR = "[data-preset='material'],.rt-preset-material";
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Адрес, по которому приложение публикует набор из `assets/icons` пакета.
|
|
582
|
+
* Умолчание годится, когда сборка кладёт набор в корень статики; свой адрес
|
|
583
|
+
* задаётся аргументом `provideRtIcons()`.
|
|
584
|
+
*/
|
|
585
|
+
const RT_ICONS_BASE_URL = new InjectionToken('RT_ICONS_BASE_URL', {
|
|
586
|
+
providedIn: 'root',
|
|
587
|
+
factory: () => '/icons',
|
|
588
|
+
});
|
|
589
|
+
/**
|
|
590
|
+
* Адрес материального набора значков. Отдельный от своего: два набора стоят рядом, и приложение
|
|
591
|
+
* кладёт их двумя папками.
|
|
592
|
+
*/
|
|
593
|
+
const RT_ICONS_MATERIAL_BASE_URL = new InjectionToken('RT_ICONS_MATERIAL_BASE_URL', {
|
|
594
|
+
providedIn: 'root',
|
|
595
|
+
factory: () => '/icons-material',
|
|
596
|
+
});
|
|
597
|
+
class RtIconRegistry {
|
|
598
|
+
#http = inject(HttpClient);
|
|
599
|
+
#doc = inject(DOCUMENT);
|
|
600
|
+
#baseUrl = inject(RT_ICONS_BASE_URL);
|
|
601
|
+
#materialBaseUrl = inject(RT_ICONS_MATERIAL_BASE_URL);
|
|
602
|
+
#platform = inject(PlatformService);
|
|
603
|
+
/**
|
|
604
|
+
* Имена, за которыми уже сходили: второй раз в сеть за ними не ходят. Ключ несёт набор —
|
|
605
|
+
* одно имя приезжает двумя разными рисунками, и общий ключ отдал бы второму первый.
|
|
606
|
+
*/
|
|
607
|
+
#requested = new Set();
|
|
608
|
+
/** Имена, которые попросила разметка. Загрузку по ним ведёт подписка из конструктора. */
|
|
609
|
+
#requestSource = new Subject();
|
|
610
|
+
constructor() {
|
|
611
|
+
// Потоки складываются, а не вытесняют друг друга: странице нужны все спрошенные
|
|
612
|
+
// значки, а не последний из них.
|
|
613
|
+
this.#requestSource
|
|
614
|
+
.pipe(mergeMap((asked) => this.#load(asked)), takeUntilDestroyed())
|
|
615
|
+
.subscribe();
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Имя symbol для `<use href="#X">`. Возвращает строку с ведущим `#`.
|
|
619
|
+
*
|
|
620
|
+
* Набор назван явно, а не выведен из страницы: одна и та же страница держит оба набора
|
|
621
|
+
* рядом — признак стоит и на корне, и на контейнере, — и вывод по странице дал бы обоим
|
|
622
|
+
* один ответ.
|
|
623
|
+
*/
|
|
624
|
+
symbolHref(name, drawing = 'base') {
|
|
625
|
+
return `#${this.#symbolId(name, drawing)}`;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Просит значок по имени: грузит файл и дописывает `<symbol>` в спрайт страницы.
|
|
629
|
+
*
|
|
630
|
+
* Ответа зовущий не ждёт и потока не получает: разметка рисует ссылку на символ сразу, а
|
|
631
|
+
* браузер дорисовывает её, когда символ приезжает. Подписка живёт здесь и только здесь —
|
|
632
|
+
* иначе каждая разметка гасила бы её сама, а таких мест у значка два.
|
|
633
|
+
*/
|
|
634
|
+
request(name, drawing = 'base') {
|
|
635
|
+
const id = this.#symbolId(name, drawing);
|
|
636
|
+
// На сервере относительных запросов к статике нет вовсе: значок дорисуется после
|
|
637
|
+
// гидрации, когда разметка попросит его снова уже в браузере.
|
|
638
|
+
if (!this.#platform.isPlatformBrowser || this.#requested.has(id)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.#requested.add(id);
|
|
642
|
+
// Символ уже лежит в спрайте страницы: его положила соседняя история витрины или
|
|
643
|
+
// сама страница до старта приложения. Запроса не уходит вовсе.
|
|
644
|
+
if (this.#doc.getElementById(id)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this.#requestSource.next({ name, drawing });
|
|
648
|
+
}
|
|
649
|
+
#load(asked) {
|
|
650
|
+
if (asked.drawing === 'base') {
|
|
651
|
+
return this.#loadInto(asked, `${this.#baseUrl}/${asked.name}.svg`, EMPTY);
|
|
652
|
+
}
|
|
653
|
+
// Материальный рисунок, который не приехал, закрывается своим. Материальный набор —
|
|
654
|
+
// слой переопределений: имени, которого в нём нет, кит и так рисует свой рисунок, и
|
|
655
|
+
// не приехавший файл обязан вести себя так же. Иначе страница, объявившая набор, но
|
|
656
|
+
// не опубликовавшая его папку, показывает пустое место там, где обещан значок, —
|
|
657
|
+
// разметка при этом верна, и промах виден только глазами. Залитый, которого нет,
|
|
658
|
+
// сперва закрывается контурным того же набора.
|
|
659
|
+
const own = this.#loadInto(asked, `${this.#baseUrl}/${asked.name}.svg`, EMPTY);
|
|
660
|
+
const outlined = asked.drawing === 'material-fill' ? this.#loadInto(asked, `${this.#materialBaseUrl}/${asked.name}.svg`, own) : own;
|
|
661
|
+
const file = asked.drawing === 'material-fill' ? `${asked.name}.fill.svg` : `${asked.name}.svg`;
|
|
662
|
+
return this.#loadInto(asked, `${this.#materialBaseUrl}/${file}`, outlined);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Рисунок по адресу под именем символа, которое просила разметка: подмена ссылки разметке
|
|
666
|
+
* не нужна. Отказ одного имени гасит только его значок — иначе один промах в наборе ронял бы
|
|
667
|
+
* всю страницу, а соседние значки уже приехали.
|
|
668
|
+
*/
|
|
669
|
+
#loadInto(asked, url, fallback) {
|
|
670
|
+
return this.#http.get(url, { responseType: 'text' }).pipe(tap((raw) => this.#mountSymbol(asked, raw)), map(() => undefined), catchError(() => fallback));
|
|
671
|
+
}
|
|
672
|
+
#symbolId(name, drawing) {
|
|
673
|
+
const prefix = {
|
|
674
|
+
base: RT_ICON_SYMBOL_ID_PREFIX,
|
|
675
|
+
material: RT_ICON_MATERIAL_SYMBOL_ID_PREFIX,
|
|
676
|
+
'material-fill': RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX,
|
|
677
|
+
};
|
|
678
|
+
return `${prefix[drawing]}${name}`;
|
|
679
|
+
}
|
|
680
|
+
#mountSymbol(asked, raw) {
|
|
681
|
+
const symbol = this.#doc.createElementNS('http://www.w3.org/2000/svg', 'symbol');
|
|
682
|
+
symbol.id = this.#symbolId(asked.name, asked.drawing);
|
|
683
|
+
symbol.setAttribute('viewBox', this.#viewBox(raw));
|
|
684
|
+
symbol.innerHTML = this.#inner(raw);
|
|
685
|
+
this.#sprite().appendChild(symbol);
|
|
686
|
+
}
|
|
687
|
+
#viewBox(raw) {
|
|
688
|
+
const viewBoxMatch = /viewBox="([^"]+)"/.exec(raw);
|
|
689
|
+
return viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
|
|
690
|
+
}
|
|
691
|
+
#inner(raw) {
|
|
692
|
+
// Тело корневого `<svg>` без него самого: оно и становится телом `<symbol>`.
|
|
693
|
+
return raw
|
|
694
|
+
.replace(/<\?xml[\s\S]*?\?>/g, '')
|
|
695
|
+
.replace(/<!DOCTYPE[\s\S]*?>/g, '')
|
|
696
|
+
.replace(/<svg\b[^>]*>/i, '')
|
|
697
|
+
.replace(/<\/svg>\s*$/i, '')
|
|
698
|
+
.trim();
|
|
699
|
+
}
|
|
700
|
+
#sprite() {
|
|
701
|
+
const existing = this.#doc.querySelector(`svg#${RT_ICON_SPRITE_ID}`);
|
|
702
|
+
if (existing) {
|
|
703
|
+
return existing;
|
|
704
|
+
}
|
|
705
|
+
const sprite = this.#doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
706
|
+
sprite.id = RT_ICON_SPRITE_ID;
|
|
707
|
+
sprite.setAttribute('aria-hidden', 'true');
|
|
708
|
+
sprite.setAttribute('style', 'position:absolute;width:0;height:0;overflow:hidden;');
|
|
709
|
+
this.#doc.body.insertBefore(sprite, this.#doc.body.firstChild);
|
|
710
|
+
return sprite;
|
|
711
|
+
}
|
|
712
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
713
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, providedIn: 'root' }); }
|
|
714
|
+
}
|
|
715
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconRegistry, decorators: [{
|
|
716
|
+
type: Injectable,
|
|
717
|
+
args: [{ providedIn: 'root' }]
|
|
718
|
+
}], ctorParameters: () => [] });
|
|
719
|
+
|
|
720
|
+
const SIZES = Object.freeze({
|
|
721
|
+
xs: 12,
|
|
722
|
+
sm: 16,
|
|
723
|
+
md: 20,
|
|
724
|
+
lg: 24,
|
|
725
|
+
xl: 32,
|
|
726
|
+
'2xl': 40,
|
|
727
|
+
});
|
|
728
|
+
/* Без переданного цвета значок цвета не пишет: `currentColor` строкой стиля перебивал правило
|
|
729
|
+
стилей, которое красит значок снаружи, — значок опасного пункта меню оставался цвета текста. */
|
|
730
|
+
const COLORS = Object.freeze({
|
|
731
|
+
current: null,
|
|
732
|
+
// Приглушённый тон можно переназначить свойством сверху: поле ввода называет так тон своего
|
|
733
|
+
// значка в наборе оформления. Цвет стоит встроенным стилем, и правилом его не перебить.
|
|
734
|
+
muted: 'var(--rt-icon-color-muted, var(--rt-neutral-600))',
|
|
735
|
+
info: 'var(--rt-color-state-info)',
|
|
736
|
+
success: 'var(--rt-color-state-success)',
|
|
737
|
+
warning: 'var(--rt-color-state-warning)',
|
|
738
|
+
danger: 'var(--rt-color-state-danger)',
|
|
739
|
+
inverse: 'var(--rt-color-text-inverse)',
|
|
740
|
+
});
|
|
741
|
+
const BEM_BLOCK$1w = 'rt-icon';
|
|
742
|
+
class RtIconComponent {
|
|
743
|
+
#registry;
|
|
744
|
+
#host;
|
|
745
|
+
/**
|
|
746
|
+
* Набор, объявленный разметкой над этим значком.
|
|
747
|
+
*
|
|
748
|
+
* Читается у разметки, а не выбирается стилями: ссылку на символ спрайта CSS подменить нечем,
|
|
749
|
+
* и второй `<use>` рядом с первым тоже не годится — он вечно указывает на символ, за которым
|
|
750
|
+
* никто не ходил, а обвязка снимков ждёт, пока нарисуется каждый.
|
|
751
|
+
*
|
|
752
|
+
* Спрашивается ближайший предок, а не корень страницы: признак набора стоит и на контейнере,
|
|
753
|
+
* и на одной странице законно живут оба набора рядом.
|
|
754
|
+
*
|
|
755
|
+
* Один раз после первой отрисовки: на сервере разметки нет вовсе, а признак набора страница
|
|
756
|
+
* по ходу жизни не переставляет — его ставит приложение своей разметкой.
|
|
757
|
+
*/
|
|
758
|
+
#preset;
|
|
759
|
+
constructor() {
|
|
760
|
+
this.#registry = inject(RtIconRegistry);
|
|
761
|
+
this.#host = inject(ElementRef);
|
|
762
|
+
/**
|
|
763
|
+
* Набор, объявленный разметкой над этим значком.
|
|
764
|
+
*
|
|
765
|
+
* Читается у разметки, а не выбирается стилями: ссылку на символ спрайта CSS подменить нечем,
|
|
766
|
+
* и второй `<use>` рядом с первым тоже не годится — он вечно указывает на символ, за которым
|
|
767
|
+
* никто не ходил, а обвязка снимков ждёт, пока нарисуется каждый.
|
|
768
|
+
*
|
|
769
|
+
* Спрашивается ближайший предок, а не корень страницы: признак набора стоит и на контейнере,
|
|
770
|
+
* и на одной странице законно живут оба набора рядом.
|
|
771
|
+
*
|
|
772
|
+
* Один раз после первой отрисовки: на сервере разметки нет вовсе, а признак набора страница
|
|
773
|
+
* по ходу жизни не переставляет — его ставит приложение своей разметкой.
|
|
774
|
+
*/
|
|
775
|
+
this.#preset = signal('base', /* @ts-ignore */
|
|
776
|
+
...(ngDevMode ? [{ debugName: "#preset" }] : /* istanbul ignore next */ []));
|
|
777
|
+
this.href = computed(() => this.#registry.symbolHref(this.name(), this.drawing()), /* @ts-ignore */
|
|
778
|
+
...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
779
|
+
/**
|
|
780
|
+
* Набор, которым рисуется этот значок. Материальный закрывает не все имена кита — он слой
|
|
781
|
+
* переопределений, как набор оформления: имя без материального рисунка рисуется своим, и это
|
|
782
|
+
* не пробел.
|
|
783
|
+
*/
|
|
784
|
+
this.preset = computed(() => this.#preset() === 'material' && iconMaterialDrawn.has(this.name()) ? 'material' : 'base', /* @ts-ignore */
|
|
785
|
+
...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
|
|
786
|
+
/** Рисунок значка: залитый бывает только у материального набора, свой набор заливки не знает. */
|
|
787
|
+
this.drawing = computed(() => {
|
|
788
|
+
if (this.preset() === 'base') {
|
|
789
|
+
return 'base';
|
|
790
|
+
}
|
|
791
|
+
return this.fill() ? 'material-fill' : 'material';
|
|
792
|
+
}, /* @ts-ignore */
|
|
793
|
+
...(ngDevMode ? [{ debugName: "drawing" }] : /* istanbul ignore next */ []));
|
|
794
|
+
this.sizePx = computed(() => SIZES[this.size()], /* @ts-ignore */
|
|
795
|
+
...(ngDevMode ? [{ debugName: "sizePx" }] : /* istanbul ignore next */ []));
|
|
796
|
+
this.colorValue = computed(() => COLORS[this.color()], /* @ts-ignore */
|
|
797
|
+
...(ngDevMode ? [{ debugName: "colorValue" }] : /* istanbul ignore next */ []));
|
|
798
|
+
this.rotateStyle = computed(() => {
|
|
799
|
+
const r = this.rotate();
|
|
800
|
+
return r !== null && r !== 0 ? `rotate(${r}deg)` : null;
|
|
801
|
+
}, /* @ts-ignore */
|
|
802
|
+
...(ngDevMode ? [{ debugName: "rotateStyle" }] : /* istanbul ignore next */ []));
|
|
803
|
+
this.name = input.required(/* @ts-ignore */
|
|
804
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
805
|
+
this.size = input('md', /* @ts-ignore */
|
|
806
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
807
|
+
this.color = input('current', /* @ts-ignore */
|
|
808
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
809
|
+
/**
|
|
810
|
+
* Залитый рисунок вместо контурного — как `FILL 1` у значка первого кита. Действует в
|
|
811
|
+
* материальном наборе; свой набор рисует значок одним рисунком. Дефолт `false`.
|
|
812
|
+
*/
|
|
813
|
+
this.fill = input(false, { ...(ngDevMode ? { debugName: "fill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
814
|
+
this.rotate = input(null, { ...(ngDevMode ? { debugName: "rotate" } : /* istanbul ignore next */ {}), transform: (v) => {
|
|
815
|
+
if (v === null || v === '') {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
return numberAttribute(v, 0);
|
|
819
|
+
} });
|
|
820
|
+
// Значок едет по запросу имени, а не вперёд всем набором: страница платит за то, что
|
|
821
|
+
// нарисовала. Смена имени просит новое — прежний символ остаётся в спрайте.
|
|
822
|
+
effect(() => {
|
|
823
|
+
this.#registry.request(this.name(), this.drawing());
|
|
824
|
+
});
|
|
825
|
+
// Разметка над значком видна только в браузере и только после первой отрисовки:
|
|
826
|
+
// контейнер с признаком набора рисует то же приложение.
|
|
827
|
+
afterNextRender(() => {
|
|
828
|
+
if (this.#host.nativeElement.closest(RT_ICON_MATERIAL_PRESET_SELECTOR)) {
|
|
829
|
+
this.#preset.set('material');
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
834
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: RtIconComponent, isStandalone: true, selector: "rt-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, rotate: { classPropertyName: "rotate", publicName: "rotate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "'true'", "style.width.px": "sizePx()", "style.height.px": "sizePx()", "style.color": "colorValue()", "style.transform": "rotateStyle()" }, classAttribute: "rt-icon" }, ngImport: i0, template: "<svg focusable=\"false\" aria-hidden=\"true\" [attr.viewBox]=\"'0 0 24 24'\">\n <use [attr.href]=\"href()\" />\n</svg>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-icon{display:inline-flex;flex-shrink:0;line-height:0}.rt-icon svg{display:block;width:100%;height:100%;fill:currentcolor}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
835
|
+
}
|
|
836
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconComponent, decorators: [{
|
|
837
|
+
type: Component,
|
|
838
|
+
args: [{ selector: 'rt-icon', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
839
|
+
class: BEM_BLOCK$1w,
|
|
840
|
+
'[attr.aria-hidden]': "'true'",
|
|
841
|
+
'[style.width.px]': 'sizePx()',
|
|
842
|
+
'[style.height.px]': 'sizePx()',
|
|
843
|
+
'[style.color]': 'colorValue()',
|
|
844
|
+
'[style.transform]': 'rotateStyle()',
|
|
845
|
+
}, template: "<svg focusable=\"false\" aria-hidden=\"true\" [attr.viewBox]=\"'0 0 24 24'\">\n <use [attr.href]=\"href()\" />\n</svg>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-icon{display:inline-flex;flex-shrink:0;line-height:0}.rt-icon svg{display:block;width:100%;height:100%;fill:currentcolor}}\n"] }]
|
|
846
|
+
}], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], rotate: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotate", required: false }] }] } });
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Называет адрес, по которому приложение опубликовало набор значков `rt-icon`.
|
|
850
|
+
*
|
|
851
|
+
* ```typescript
|
|
852
|
+
* export function withCoreProviders(): EnvironmentProviders {
|
|
853
|
+
* return makeEnvironmentProviders([
|
|
854
|
+
* provideHttpClient(...),
|
|
855
|
+
* provideRtStorage(),
|
|
856
|
+
* provideRtIcons(),
|
|
857
|
+
* ]);
|
|
858
|
+
* }
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* Вперёд отсюда не грузится ничего: значок едет тогда, когда его попросила разметка, и платит
|
|
862
|
+
* за него та страница, которая его нарисовала. Сборка спрайта и загрузка — дело `RtIconRegistry`.
|
|
863
|
+
*
|
|
864
|
+
* Сами файлы приложение публикует само — набор лежит в `assets/icons` пакета, и сборка копирует
|
|
865
|
+
* его в свою статику.
|
|
866
|
+
*
|
|
867
|
+
* Наборов значков два: свой и материальный. Второй нужен только приложению, которое объявляет
|
|
868
|
+
* материальный набор оформления, и адрес у него свой — набор лежит в `assets/icons-material`.
|
|
869
|
+
*
|
|
870
|
+
* @param baseUrl Адрес опубликованного набора. По умолчанию `/icons`.
|
|
871
|
+
* @param materialBaseUrl Адрес материального набора. По умолчанию `/icons-material`.
|
|
872
|
+
*/
|
|
873
|
+
function provideRtIcons(baseUrl, materialBaseUrl) {
|
|
874
|
+
return makeEnvironmentProviders([
|
|
875
|
+
...(baseUrl === undefined ? [] : [{ provide: RT_ICONS_BASE_URL, useValue: baseUrl }]),
|
|
876
|
+
...(materialBaseUrl === undefined ? [] : [{ provide: RT_ICONS_MATERIAL_BASE_URL, useValue: materialBaseUrl }]),
|
|
877
|
+
]);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Раскрытые пункты после нажатия на пункт `index`: раскрытый сворачивается, свёрнутый
|
|
882
|
+
* раскрывается, остальные остаются как были. Читающий сравнивает два ответа, и свернуть первый
|
|
883
|
+
* при раскрытии второго значило бы отнять у него то, что он читал.
|
|
884
|
+
*/
|
|
885
|
+
function toggleAccordionItem(open, index) {
|
|
886
|
+
const next = new Set(open);
|
|
887
|
+
if (next.has(index)) {
|
|
888
|
+
next.delete(index);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
next.add(index);
|
|
892
|
+
}
|
|
893
|
+
return next;
|
|
894
|
+
}
|
|
895
|
+
/** Раскрытые пункты при входе: пункт из входа, если такой есть в списке, иначе ни одного. */
|
|
896
|
+
function initialAccordionOpen(openIndex, count) {
|
|
897
|
+
if (openIndex === null || openIndex < 0 || openIndex >= count) {
|
|
898
|
+
return new Set();
|
|
899
|
+
}
|
|
900
|
+
return new Set([openIndex]);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const BEM_BLOCK$1v = 'rt-accordion';
|
|
904
|
+
let accordionIdSeed = 0;
|
|
905
|
+
function nextAccordionId() {
|
|
906
|
+
accordionIdSeed += 1;
|
|
907
|
+
return accordionIdSeed;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Список пунктов, у каждого заголовок-кнопка и текст под ней. Нажатие раскрывает или сворачивает
|
|
911
|
+
* пункт, соседи остаются как были. Предмета показа аккордеон не знает: пункты приходят входом уже
|
|
912
|
+
* переведёнными.
|
|
913
|
+
*/
|
|
914
|
+
class RtAccordionComponent {
|
|
915
|
+
constructor() {
|
|
916
|
+
/** Основа id кнопок и панелей: два аккордеона на одной странице не ссылаются на чужие панели. */
|
|
917
|
+
this.idBase = `rt-accordion-${nextAccordionId()}`;
|
|
918
|
+
/** Пришёл новый список — раскрытие начинается заново с пункта из входа. */
|
|
919
|
+
this.open = linkedSignal(() => initialAccordionOpen(this.openIndex(), this.items().length), /* @ts-ignore */
|
|
920
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
921
|
+
this.items = input.required(/* @ts-ignore */
|
|
922
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
923
|
+
/** Пункт, раскрытый при входе. `null` оставляет все свёрнутыми. */
|
|
924
|
+
this.openIndex = input(0, /* @ts-ignore */
|
|
925
|
+
...(ngDevMode ? [{ debugName: "openIndex" }] : /* istanbul ignore next */ []));
|
|
926
|
+
}
|
|
927
|
+
toggle(index) {
|
|
928
|
+
this.open.update((open) => toggleAccordionItem(open, index));
|
|
929
|
+
}
|
|
930
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
931
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: RtAccordionComponent, isStandalone: true, selector: "rt-accordion", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, openIndex: { classPropertyName: "openIndex", publicName: "openIndex", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "rt-accordion" }, ngImport: i0, template: "<ng-container rtBlock=\"rt-accordion\">\n @for (item of items(); track $index; let index = $index) {\n <div rtElem=\"item\" [rtMod]=\"{ open: open().has(index) }\">\n <h3 rtElem=\"heading\">\n <button\n rtElem=\"toggle\"\n qa-dataid=\"accordion-toggle\"\n type=\"button\"\n [id]=\"idBase + '-toggle-' + index\"\n [attr.aria-expanded]=\"open().has(index)\"\n [attr.aria-controls]=\"idBase + '-panel-' + index\"\n (click)=\"toggle(index)\">\n <span rtElem=\"title\">{{ item.title }}</span>\n <rt-icon rtElem=\"chevron\" name=\"chevron-down\" size=\"sm\" aria-hidden=\"true\" />\n </button>\n </h3>\n <div\n rtElem=\"panel\"\n qa-dataid=\"accordion-panel\"\n role=\"region\"\n [id]=\"idBase + '-panel-' + index\"\n [attr.aria-labelledby]=\"idBase + '-toggle-' + index\"\n [hidden]=\"!open().has(index)\">\n <p rtElem=\"text\">{{ item.text }}</p>\n </div>\n </div>\n }\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-accordion{--rt-accordion-border-width: var(--rt-border-width-thin);--rt-accordion-duration: var(--rt-duration-fast);--rt-accordion-focus-shadow: var(--rt-shadow-focus-ring);display:block;width:100%}.rt-accordion__item{border-bottom:var(--rt-accordion-border-width) solid var(--rt-color-border-subtle)}.rt-accordion__heading{margin:0;font-family:var(--rt-font-family-sans);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-snug)}.rt-accordion__toggle{display:flex;width:100%;align-items:center;justify-content:space-between;padding:var(--rt-space-md) 0;border:0;background:transparent;color:var(--rt-color-text-primary);cursor:pointer;font:inherit;gap:var(--rt-space-md);text-align:start}.rt-accordion__toggle:focus-visible{box-shadow:var(--rt-accordion-focus-shadow);outline:2px solid transparent;outline-offset:2px}.rt-accordion__chevron{flex:none;transition:transform var(--rt-accordion-duration)}.rt-accordion__text{padding-bottom:var(--rt-space-md);margin:0;color:var(--rt-color-text-muted);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-sm);line-height:var(--rt-leading-normal)}.rt-accordion__item--open .rt-accordion__chevron{transform:rotate(180deg)}}\n"], dependencies: [{ kind: "directive", type:
|
|
932
|
+
// standalone components / directives
|
|
933
|
+
BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }, { kind: "component", type: RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "fill", "rotate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
934
|
+
}
|
|
935
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtAccordionComponent, decorators: [{
|
|
936
|
+
type: Component,
|
|
937
|
+
args: [{ selector: 'rt-accordion', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
|
|
938
|
+
// standalone components / directives
|
|
939
|
+
BlockDirective,
|
|
940
|
+
ElemDirective,
|
|
941
|
+
ModDirective,
|
|
942
|
+
RtIconComponent,
|
|
943
|
+
], host: {
|
|
944
|
+
class: BEM_BLOCK$1v,
|
|
945
|
+
}, template: "<ng-container rtBlock=\"rt-accordion\">\n @for (item of items(); track $index; let index = $index) {\n <div rtElem=\"item\" [rtMod]=\"{ open: open().has(index) }\">\n <h3 rtElem=\"heading\">\n <button\n rtElem=\"toggle\"\n qa-dataid=\"accordion-toggle\"\n type=\"button\"\n [id]=\"idBase + '-toggle-' + index\"\n [attr.aria-expanded]=\"open().has(index)\"\n [attr.aria-controls]=\"idBase + '-panel-' + index\"\n (click)=\"toggle(index)\">\n <span rtElem=\"title\">{{ item.title }}</span>\n <rt-icon rtElem=\"chevron\" name=\"chevron-down\" size=\"sm\" aria-hidden=\"true\" />\n </button>\n </h3>\n <div\n rtElem=\"panel\"\n qa-dataid=\"accordion-panel\"\n role=\"region\"\n [id]=\"idBase + '-panel-' + index\"\n [attr.aria-labelledby]=\"idBase + '-toggle-' + index\"\n [hidden]=\"!open().has(index)\">\n <p rtElem=\"text\">{{ item.text }}</p>\n </div>\n </div>\n }\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-accordion{--rt-accordion-border-width: var(--rt-border-width-thin);--rt-accordion-duration: var(--rt-duration-fast);--rt-accordion-focus-shadow: var(--rt-shadow-focus-ring);display:block;width:100%}.rt-accordion__item{border-bottom:var(--rt-accordion-border-width) solid var(--rt-color-border-subtle)}.rt-accordion__heading{margin:0;font-family:var(--rt-font-family-sans);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-snug)}.rt-accordion__toggle{display:flex;width:100%;align-items:center;justify-content:space-between;padding:var(--rt-space-md) 0;border:0;background:transparent;color:var(--rt-color-text-primary);cursor:pointer;font:inherit;gap:var(--rt-space-md);text-align:start}.rt-accordion__toggle:focus-visible{box-shadow:var(--rt-accordion-focus-shadow);outline:2px solid transparent;outline-offset:2px}.rt-accordion__chevron{flex:none;transition:transform var(--rt-accordion-duration)}.rt-accordion__text{padding-bottom:var(--rt-space-md);margin:0;color:var(--rt-color-text-muted);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-sm);line-height:var(--rt-leading-normal)}.rt-accordion__item--open .rt-accordion__chevron{transform:rotate(180deg)}}\n"] }]
|
|
946
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], openIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "openIndex", required: false }] }] } });
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Английские подписи кита — умолчание, а не локализация.
|
|
950
|
+
*
|
|
951
|
+
* Кит рисует часть текста сам: кнопки диалогов, тексты ошибок поля, расшифровки
|
|
952
|
+
* иконок для скринридера. Своего языка у него нет и быть не может — его знает
|
|
953
|
+
* приложение и отдаёт функцией-переводчиком (`provideRtKitLabels`). Этот набор
|
|
954
|
+
* стоит под ней умолчанием, чтобы кит без единой настройки не рисовал пустоты:
|
|
955
|
+
* среди ключей много `aria`-подписей, и пустая там означает кнопку без имени
|
|
956
|
+
* для скринридера.
|
|
957
|
+
*
|
|
958
|
+
* Набор задаёт и **перечень ключей**: `TRtKitLabelKey` выводится отсюда, поэтому
|
|
959
|
+
* опечатка в ключе не доживает до рантайма.
|
|
960
|
+
*
|
|
961
|
+
* Места вида `{{name}}` заполняются параметрами — их принимает `rtKitLabel()`.
|
|
962
|
+
*/
|
|
963
|
+
// eslint-disable-next-line @typescript-eslint/typedef -- аннотация стёрла бы литеральный тип, на котором стоит TRtKitLabelKey
|
|
964
|
+
const RT_KIT_LABELS_EN = {
|
|
965
|
+
bottomSheetClose: 'Close',
|
|
966
|
+
chatAttachAria: 'Attach a file',
|
|
967
|
+
chatAttachTooltip: 'Attach a file',
|
|
968
|
+
chatAttachmentFallback: 'Attachment',
|
|
969
|
+
chatCollapse: 'Collapse chat',
|
|
970
|
+
chatComposerAria: 'Message input',
|
|
971
|
+
chatDeleteAria: 'Delete message',
|
|
972
|
+
chatDeleteTooltip: 'Delete',
|
|
973
|
+
chatDropOverlay: 'Drop the files to attach them to your message',
|
|
974
|
+
chatEmptyHint: 'Chat is not available yet.',
|
|
975
|
+
chatEmptyThread: 'No messages yet.',
|
|
976
|
+
chatExpand: 'Expand chat to full screen',
|
|
977
|
+
chatMessageDeleted: 'Message deleted',
|
|
978
|
+
chatPlaceholder: 'Type a message',
|
|
979
|
+
chatRefreshAria: 'Refresh messages',
|
|
980
|
+
chatRefreshTooltip: 'Refresh messages',
|
|
981
|
+
chatRetryAria: 'Send again',
|
|
982
|
+
chatRetryTooltip: 'Retry',
|
|
983
|
+
chatSendAria: 'Send message',
|
|
984
|
+
chatSendLabel: 'Send',
|
|
985
|
+
chatStatusFailed: 'Not sent',
|
|
986
|
+
chatStatusRead: 'Read',
|
|
987
|
+
chatStatusSending: 'Sending',
|
|
988
|
+
chatStatusSent: 'Delivered',
|
|
989
|
+
chatTitle: 'Chat',
|
|
990
|
+
counterDecrease: 'Decrease',
|
|
991
|
+
counterIncrease: 'Increase',
|
|
992
|
+
dataListClearFilters: 'Clear filters',
|
|
993
|
+
dataListHorizontalScrollbar: 'Horizontal scrollbar shown',
|
|
994
|
+
dataListPlaceholder: 'No Data Found',
|
|
995
|
+
dataListRefresh: 'Refresh',
|
|
996
|
+
dataListSearchPlaceholder: 'Search...',
|
|
997
|
+
dataListSelectAll: 'Select all',
|
|
998
|
+
dataListSelected: 'Selected: {{count}}',
|
|
999
|
+
dataListSettingsHint: 'Change columns visibility and order, show(hide) scrollbar',
|
|
1000
|
+
dataListSettingsTitle: 'Edit table configuration',
|
|
1001
|
+
dataListTableConfig: 'Table configuration',
|
|
1002
|
+
dataListVerticalScrollbar: 'Vertical scrollbar shown',
|
|
1003
|
+
dataTableActions: 'Actions',
|
|
1004
|
+
dataTableCopied: 'Copied!',
|
|
1005
|
+
dataTableCopy: 'Copy',
|
|
1006
|
+
dataTableFilterClear: 'Clear filter',
|
|
1007
|
+
dataTableFilterOperatorContains: 'Contains',
|
|
1008
|
+
dataTableFilterOperatorEquals: 'Equal',
|
|
1009
|
+
dataTableFilterOperatorGreaterThan: 'More than',
|
|
1010
|
+
dataTableFilterOperatorLessThan: 'Less than',
|
|
1011
|
+
dataTableFilterOperatorNotEquals: 'Not equal',
|
|
1012
|
+
dataTableFilterSelectPlaceholder: 'Chose filter',
|
|
1013
|
+
dataTableFilterValuePlaceholder: 'Type in filter value',
|
|
1014
|
+
dataTableSelectPage: 'Select the rows of this page',
|
|
1015
|
+
dataTableSelectRow: 'Select row',
|
|
1016
|
+
fieldErrorEmail: 'Invalid email',
|
|
1017
|
+
fieldErrorMax: 'Value is too large',
|
|
1018
|
+
fieldErrorMaxLength: 'Value is too long',
|
|
1019
|
+
fieldErrorMin: 'Value is too small',
|
|
1020
|
+
fieldErrorMinLength: 'Value is too short',
|
|
1021
|
+
fieldErrorPattern: 'Invalid format',
|
|
1022
|
+
fieldErrorRequired: 'Required field',
|
|
1023
|
+
photoViewerClose: 'Close viewer',
|
|
1024
|
+
photoViewerNext: 'Next photo',
|
|
1025
|
+
photoViewerPrev: 'Previous photo',
|
|
1026
|
+
themeToggleLabel: 'Switch theme',
|
|
1027
|
+
uiActions: 'Actions',
|
|
1028
|
+
uiAuditEmptyHint: 'Deal events will appear here — stage changes and other actions.',
|
|
1029
|
+
uiBack: 'Back',
|
|
1030
|
+
uiBackToList: 'Back to list',
|
|
1031
|
+
uiBaseline: 'was {{value}}',
|
|
1032
|
+
uiCancel: 'Cancel',
|
|
1033
|
+
uiChangeAgent: 'Change agent',
|
|
1034
|
+
uiChooseFile: 'Choose file',
|
|
1035
|
+
uiClear: 'Clear',
|
|
1036
|
+
uiClearFilters: 'Clear filters',
|
|
1037
|
+
uiClose: 'Close',
|
|
1038
|
+
uiCollapse: 'Collapse',
|
|
1039
|
+
uiColumnLocked: 'Column is pinned',
|
|
1040
|
+
uiColumnSettings: 'Column settings',
|
|
1041
|
+
uiColumnSettingsTitle: 'Configure columns',
|
|
1042
|
+
uiCommentOptional: 'Comment (optional)',
|
|
1043
|
+
uiConfirm: 'Confirm',
|
|
1044
|
+
uiConfirmAction: 'Confirm action',
|
|
1045
|
+
uiContinue: 'Continue',
|
|
1046
|
+
uiCopied: 'Copied',
|
|
1047
|
+
uiCopy: 'Copy',
|
|
1048
|
+
uiCurrentStage: 'Current stage',
|
|
1049
|
+
uiDetails: 'Details',
|
|
1050
|
+
uiDiscardChanges: 'Discard Changes',
|
|
1051
|
+
uiDownload: 'Download',
|
|
1052
|
+
uiDownloadFile: 'Download: {{name}}',
|
|
1053
|
+
uiDragColumn: 'Drag column',
|
|
1054
|
+
uiFileDropOverlay: 'Drop the files to attach them',
|
|
1055
|
+
uiFilterChoose: 'Choose',
|
|
1056
|
+
uiFilterContains: 'Contains',
|
|
1057
|
+
uiFilterEndsWith: 'Ends with',
|
|
1058
|
+
uiFilterEquals: 'Equals',
|
|
1059
|
+
uiFilterGreaterThan: 'Greater than',
|
|
1060
|
+
uiFilterLessThan: 'Less than',
|
|
1061
|
+
uiFilterNotEquals: 'Does not equal',
|
|
1062
|
+
uiFilterStartsWith: 'Starts with',
|
|
1063
|
+
uiFilterValue: 'Filter value',
|
|
1064
|
+
uiFilters: 'Filters',
|
|
1065
|
+
uiForward: 'Forward',
|
|
1066
|
+
uiHide: 'Hide',
|
|
1067
|
+
uiHideColumn: 'Hide column',
|
|
1068
|
+
uiHidePassword: 'Hide password',
|
|
1069
|
+
uiHint: 'Hint',
|
|
1070
|
+
uiHistory: 'History',
|
|
1071
|
+
uiInviteUser: 'Invite user',
|
|
1072
|
+
uiItemsPerPage: 'Items per page',
|
|
1073
|
+
uiLiveOnSite: 'On the site now',
|
|
1074
|
+
uiLoadMore: 'Load more',
|
|
1075
|
+
uiMainNav: 'Main navigation',
|
|
1076
|
+
uiMore: 'More',
|
|
1077
|
+
uiMoreActions: 'More actions',
|
|
1078
|
+
uiMoveStage: 'Move',
|
|
1079
|
+
uiMoveStageAria: 'Move stage',
|
|
1080
|
+
uiNavMenu: 'Navigation menu',
|
|
1081
|
+
uiNavUnread: 'Unread',
|
|
1082
|
+
uiNextPage: 'Next page',
|
|
1083
|
+
uiNightGridAria: 'Occupancy grid',
|
|
1084
|
+
uiNoData: 'No data yet',
|
|
1085
|
+
uiNoOptions: 'No options',
|
|
1086
|
+
uiNoRows: 'No data',
|
|
1087
|
+
uiNothingFound: 'Nothing found',
|
|
1088
|
+
uiNotifications: 'Notifications',
|
|
1089
|
+
uiPageOf: 'Page {{page}} of {{last}}',
|
|
1090
|
+
uiPagination: 'Pagination',
|
|
1091
|
+
uiPerPage: 'Items per page:',
|
|
1092
|
+
uiPickAgent: 'Select an agent',
|
|
1093
|
+
uiPickAnotherAgent: 'Select a different agent',
|
|
1094
|
+
uiPickTransition: 'Select a transition',
|
|
1095
|
+
uiPrevPage: 'Previous page',
|
|
1096
|
+
uiProfile: 'Profile',
|
|
1097
|
+
uiRangeEmpty: '0 of 0',
|
|
1098
|
+
uiRangeOf: '{{from}}–{{to}} of {{total}}',
|
|
1099
|
+
uiRemove: 'Delete',
|
|
1100
|
+
uiRename: 'Rename',
|
|
1101
|
+
uiReplace: 'Replace',
|
|
1102
|
+
uiReplaceAgentTitle: 'Replace agent',
|
|
1103
|
+
uiResetDefaults: 'Reset to defaults',
|
|
1104
|
+
uiResizeDetails: 'Resize details pane',
|
|
1105
|
+
uiResizeList: 'Resize list pane',
|
|
1106
|
+
uiRefresh: 'Refresh',
|
|
1107
|
+
uiRevert: 'Revert',
|
|
1108
|
+
uiSave: 'Save',
|
|
1109
|
+
uiSaveNewAgent: 'Save the new agent',
|
|
1110
|
+
uiScrollDown: 'Scroll down',
|
|
1111
|
+
uiSearch: 'Search',
|
|
1112
|
+
uiSectionInProgress: '{{label}} — section under development',
|
|
1113
|
+
uiSelectAll: 'Select all',
|
|
1114
|
+
uiSelectValues: 'Select values',
|
|
1115
|
+
uiSelectedCount: 'Selected: {{count}}',
|
|
1116
|
+
uiSelectedOf: 'Selected: {{count}} of {{total}}',
|
|
1117
|
+
uiShow: 'Show',
|
|
1118
|
+
uiShowColumn: 'Show column',
|
|
1119
|
+
uiShowDetails: 'Show details',
|
|
1120
|
+
uiShowPassword: 'Show password',
|
|
1121
|
+
uiStageNoteHint: 'Internal note — the client will not see it.',
|
|
1122
|
+
uiStageTransition: 'Stage transition',
|
|
1123
|
+
uiSubscribeNotifications: 'Subscribe to notifications',
|
|
1124
|
+
uiTabInvalid: 'This tab contains errors',
|
|
1125
|
+
uiTargetStage: 'Target stage',
|
|
1126
|
+
uiTargetStageRequired: 'Select a target stage.',
|
|
1127
|
+
uiThreadListEmptyHint: 'New threads will appear here.',
|
|
1128
|
+
uiToList: 'To list',
|
|
1129
|
+
uiUnsavedDiscard: 'Close without saving',
|
|
1130
|
+
uiUnsavedMessage: 'The panel has edits that have not been saved yet. Choose what to do with them.',
|
|
1131
|
+
uiUnsavedSave: 'Save and close',
|
|
1132
|
+
uiUnsavedStay: 'Stay',
|
|
1133
|
+
uiUnsavedTitle: 'Unsaved changes',
|
|
1134
|
+
uiWelcomeAria: 'Welcome message for a new user',
|
|
1135
|
+
uiYearOverYear: 'YoY',
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
/** Места вида `{{name}}` — их заполняет `interpolate`. */
|
|
1139
|
+
const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}/g;
|
|
1140
|
+
/**
|
|
1141
|
+
* Подставляет параметры в подпись.
|
|
1142
|
+
*
|
|
1143
|
+
* Нужна английскому умолчанию: приложение свои подстановки делает само —
|
|
1144
|
+
* функция-переводчик обязана вернуть готовую строку. Место, для которого
|
|
1145
|
+
* параметра не дали, остаётся как есть: пустота на его месте прочиталась бы как
|
|
1146
|
+
* законченная фраза, а `{{name}}` виден и чинится.
|
|
1147
|
+
*/
|
|
1148
|
+
function interpolate(text, params) {
|
|
1149
|
+
if (params === undefined) {
|
|
1150
|
+
return text;
|
|
1151
|
+
}
|
|
1152
|
+
return text.replace(PLACEHOLDER, (match, name) => (Object.hasOwn(params, name) ? String(params[name]) : match));
|
|
1153
|
+
}
|
|
1154
|
+
/** Английское умолчание: кит без единой настройки рисует текст, а не пустоты. */
|
|
1155
|
+
const defaultTranslator = (key, params) => interpolate(RT_KIT_LABELS_EN[key], params);
|
|
1156
|
+
/**
|
|
1157
|
+
* Функция, которой кит получает подписи. Её отдаёт приложение —
|
|
1158
|
+
* `provideRtKitLabels()`.
|
|
1159
|
+
*
|
|
1160
|
+
* Токен объявлен **сигналом**, а не просто функцией: приложение, меняющее язык
|
|
1161
|
+
* на ходу, кладёт в него новую функцию, и всё, что от неё считано,
|
|
1162
|
+
* пересчитывается само.
|
|
1163
|
+
*/
|
|
1164
|
+
const RT_KIT_TRANSLATOR = new InjectionToken('RT_KIT_TRANSLATOR', {
|
|
1165
|
+
providedIn: 'root',
|
|
1166
|
+
factory: () => signal(defaultTranslator).asReadonly(),
|
|
1167
|
+
});
|
|
1168
|
+
/**
|
|
1169
|
+
* Активная локаль. Ею кит форматирует даты — больше ни для чего она ему не
|
|
1170
|
+
* нужна.
|
|
1171
|
+
*
|
|
1172
|
+
* Данные локали для `DatePipe` регистрирует приложение: без них пайп падает
|
|
1173
|
+
* `Missing locale data`, и кит за это не отвечает.
|
|
1174
|
+
*/
|
|
1175
|
+
const RT_KIT_LOCALE = new InjectionToken('RT_KIT_LOCALE', {
|
|
1176
|
+
providedIn: 'root',
|
|
1177
|
+
factory: () => signal('en').asReadonly(),
|
|
1178
|
+
});
|
|
1179
|
+
/**
|
|
1180
|
+
* Все подписи кита разом, ключ → готовая строка.
|
|
1181
|
+
*
|
|
1182
|
+
* Этим пользуются компоненты: `t().uiClose` вместо ключа по одному. Карта
|
|
1183
|
+
* строится один раз на приложение (`providedIn: 'root'`) и пересчитывается,
|
|
1184
|
+
* только когда приложение сменило функцию-переводчик.
|
|
1185
|
+
*
|
|
1186
|
+
* Подпись, которой переводчик не дал (вернул пустое или упал), берётся
|
|
1187
|
+
* английской: пустая `aria`-подпись означает кнопку без имени для скринридера.
|
|
1188
|
+
*/
|
|
1189
|
+
const RT_KIT_LABELS = new InjectionToken('RT_KIT_LABELS', {
|
|
1190
|
+
providedIn: 'root',
|
|
1191
|
+
factory: () => {
|
|
1192
|
+
const translator = inject(RT_KIT_TRANSLATOR);
|
|
1193
|
+
return computed(() => {
|
|
1194
|
+
const translate = translator();
|
|
1195
|
+
const map = {};
|
|
1196
|
+
for (const key of Object.keys(RT_KIT_LABELS_EN)) {
|
|
1197
|
+
map[key] = translate(key) || RT_KIT_LABELS_EN[key];
|
|
1198
|
+
}
|
|
1199
|
+
return map;
|
|
1200
|
+
});
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
/**
|
|
1204
|
+
* Одна подпись, а не вся карта.
|
|
1205
|
+
*
|
|
1206
|
+
* Карте не годятся две вещи: подпись с подстановками (параметры приходят из
|
|
1207
|
+
* состояния компонента) и подпись, ключ которой выбирается на ходу — кнопка
|
|
1208
|
+
* «свернуть ↔ развернуть», текст ошибки поля по сработавшему валидатору. Обе
|
|
1209
|
+
* берутся отсюда. Зовётся в поле класса, в инъекционном контексте.
|
|
1210
|
+
*
|
|
1211
|
+
* ```typescript
|
|
1212
|
+
* readonly #range: Signal<string> = rtKitLabel('uiRangeOf', this.#rangeParams);
|
|
1213
|
+
* readonly #expand: Signal<string> = rtKitLabel(computed(() => (this.expanded() ? 'chatCollapse' : 'chatExpand')));
|
|
1214
|
+
* ```
|
|
1215
|
+
*
|
|
1216
|
+
* @param key Ключ подписи — значением или сигналом, если он меняется. Пустой
|
|
1217
|
+
* ключ даёт пустую строку: у поля без сработавшего валидатора подписи нет, и
|
|
1218
|
+
* умолчание тут показало бы ошибку там, где её не случилось.
|
|
1219
|
+
* @param params Подстановки — значением или сигналом, если они меняются.
|
|
1220
|
+
*/
|
|
1221
|
+
function rtKitLabel(key, params) {
|
|
1222
|
+
const translator = inject(RT_KIT_TRANSLATOR);
|
|
1223
|
+
return computed(() => {
|
|
1224
|
+
const resolvedKey = typeof key === 'function' ? key() : key;
|
|
1225
|
+
if (resolvedKey === '') {
|
|
1226
|
+
return '';
|
|
1227
|
+
}
|
|
1228
|
+
const resolved = typeof params === 'function' ? params() : params;
|
|
1229
|
+
return translator()(resolvedKey, resolved) || defaultTranslator(resolvedKey, resolved);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Отдаёт киту подписи приложения.
|
|
1234
|
+
*
|
|
1235
|
+
* Без него кит работает на английском умолчании — оно вшито и покрывает все
|
|
1236
|
+
* ключи, поэтому пустой кнопки на экране не бывает никогда.
|
|
1237
|
+
*
|
|
1238
|
+
* ```typescript
|
|
1239
|
+
* export const appConfig: ApplicationConfig = {
|
|
1240
|
+
* providers: [
|
|
1241
|
+
* provideRtKitLabels({
|
|
1242
|
+
* translator: computed(() => (key, params) => transloco.translate(`rtKit.${key}`, params)),
|
|
1243
|
+
* locale: toSignal(transloco.langChanges$, { initialValue: transloco.getActiveLang() }),
|
|
1244
|
+
* }),
|
|
1245
|
+
* ],
|
|
1246
|
+
* };
|
|
1247
|
+
* ```
|
|
1248
|
+
*
|
|
1249
|
+
* Способ доставки — дело приложения: Transloco, `$localize`, собственный
|
|
1250
|
+
* словарь. Кит знает только ключи и то, что на них отвечают строкой.
|
|
1251
|
+
*
|
|
1252
|
+
* @param source Функция-переводчик и активная локаль. Локаль необязательна —
|
|
1253
|
+
* без неё кит форматирует даты по-английски.
|
|
1254
|
+
*/
|
|
1255
|
+
function provideRtKitLabels(source) {
|
|
1256
|
+
const providers = [{ provide: RT_KIT_TRANSLATOR, useValue: source.translator }];
|
|
1257
|
+
if (source.locale !== undefined) {
|
|
1258
|
+
providers.push({ provide: RT_KIT_LOCALE, useValue: source.locale });
|
|
1259
|
+
}
|
|
1260
|
+
return makeEnvironmentProviders(providers);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Подпись кита по ключу — для значений, приходящих из контекста шаблона.
|
|
1265
|
+
*
|
|
1266
|
+
* Шаблон не зовёт методов: имя раздела приходит элементом цикла, и посчитать подпись заранее
|
|
1267
|
+
* нечем. Пайп чистый, поэтому функция-переводчик передаётся ему вторым доводом — сменившись,
|
|
1268
|
+
* она пересчитывает подпись сама.
|
|
1269
|
+
*/
|
|
1270
|
+
class RtKitLabelPipe {
|
|
1271
|
+
transform(key, translator, params) {
|
|
1272
|
+
return translator(key, params);
|
|
1273
|
+
}
|
|
1274
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1275
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, isStandalone: true, name: "rtKitLabel" }); }
|
|
1276
|
+
}
|
|
1277
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtKitLabelPipe, decorators: [{
|
|
1278
|
+
type: Pipe,
|
|
1279
|
+
args: [{
|
|
1280
|
+
name: 'rtKitLabel',
|
|
1281
|
+
pure: true,
|
|
1282
|
+
}]
|
|
1283
|
+
}] });
|
|
1284
|
+
|
|
1285
|
+
const BEM_BLOCK$1u = 'rt-ripple';
|
|
1286
|
+
/** Сколько живёт один слой волны. Совпадает с длительностью в стилях блока. */
|
|
1287
|
+
const RIPPLE_LIFETIME_MS = 550;
|
|
1288
|
+
/**
|
|
1289
|
+
* Волна нажатия. Ставится на элемент, который нажимают, и рисует расходящийся
|
|
1290
|
+
* от точки касания круг.
|
|
1291
|
+
*
|
|
1292
|
+
* Волна живёт в своём обрезающем слое внутри элемента, а не прямо в нём:
|
|
1293
|
+
* `overflow: hidden` на самой кнопке срезал бы всё, что торчит за её край, —
|
|
1294
|
+
* у кнопки-значка так стоит метка, вынесенная на два пикселя наружу.
|
|
1295
|
+
*
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```html
|
|
1298
|
+
* <button rtRipple>Сохранить</button>
|
|
1299
|
+
* <button rtRipple [rippleDisabled]="loading()">Сохранить</button>
|
|
1300
|
+
* ```
|
|
1301
|
+
*/
|
|
1302
|
+
class RtRippleDirective {
|
|
1303
|
+
#el;
|
|
1304
|
+
#renderer;
|
|
1305
|
+
#destroyRef;
|
|
1306
|
+
/** Слои, которые ещё гаснут: снимаются, если элемент умрёт раньше их. */
|
|
1307
|
+
#live;
|
|
1308
|
+
/** Обрезающий слой заводится при первом нажатии: у кнопки, которую не нажали, его нет. */
|
|
1309
|
+
#area;
|
|
1310
|
+
/** Гасит незакончившиеся волны: иначе таймер трогает снятый из дерева узел. */
|
|
1311
|
+
constructor() {
|
|
1312
|
+
this.#el = inject(ElementRef);
|
|
1313
|
+
this.#renderer = inject(Renderer2);
|
|
1314
|
+
this.#destroyRef = inject(DestroyRef);
|
|
1315
|
+
/** Слои, которые ещё гаснут: снимаются, если элемент умрёт раньше их. */
|
|
1316
|
+
this.#live = new Set();
|
|
1317
|
+
/** Обрезающий слой заводится при первом нажатии: у кнопки, которую не нажали, его нет. */
|
|
1318
|
+
this.#area = null;
|
|
1319
|
+
/**
|
|
1320
|
+
* Отключает волну, не трогая остальное поведение элемента.
|
|
1321
|
+
* Отключённая и загружающаяся кнопка волны не даёт.
|
|
1322
|
+
*/
|
|
1323
|
+
this.rippleDisabled = input(false, { ...(ngDevMode ? { debugName: "rippleDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1324
|
+
this.#destroyRef.onDestroy(() => {
|
|
1325
|
+
for (const timer of this.#live) {
|
|
1326
|
+
clearTimeout(timer);
|
|
1327
|
+
}
|
|
1328
|
+
this.#live.clear();
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Ставит один слой волны от точки нажатия и снимает его, когда тот отгорит.
|
|
1333
|
+
*
|
|
1334
|
+
* Радиус считается по дальнему углу элемента: волна из угла должна накрыть
|
|
1335
|
+
* противоположный, иначе на широкой кнопке она обрывается на полпути.
|
|
1336
|
+
*/
|
|
1337
|
+
launch(event) {
|
|
1338
|
+
const host = this.#el.nativeElement;
|
|
1339
|
+
if (this.rippleDisabled() || host.disabled) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const box = host.getBoundingClientRect();
|
|
1343
|
+
const x = event.clientX - box.left;
|
|
1344
|
+
const y = event.clientY - box.top;
|
|
1345
|
+
const radius = Math.hypot(Math.max(x, box.width - x), Math.max(y, box.height - y));
|
|
1346
|
+
const area = this.#ensureArea(host);
|
|
1347
|
+
const layer = this.#renderer.createElement('span');
|
|
1348
|
+
this.#renderer.addClass(layer, `${BEM_BLOCK$1u}__wave`);
|
|
1349
|
+
this.#renderer.setStyle(layer, 'left', `${x - radius}px`);
|
|
1350
|
+
this.#renderer.setStyle(layer, 'top', `${y - radius}px`);
|
|
1351
|
+
this.#renderer.setStyle(layer, 'width', `${radius * 2}px`);
|
|
1352
|
+
this.#renderer.setStyle(layer, 'height', `${radius * 2}px`);
|
|
1353
|
+
this.#renderer.appendChild(area, layer);
|
|
1354
|
+
const timer = setTimeout(() => {
|
|
1355
|
+
this.#live.delete(timer);
|
|
1356
|
+
if (layer.parentNode) {
|
|
1357
|
+
this.#renderer.removeChild(area, layer);
|
|
1358
|
+
}
|
|
1359
|
+
}, RIPPLE_LIFETIME_MS);
|
|
1360
|
+
this.#live.add(timer);
|
|
1361
|
+
}
|
|
1362
|
+
/** Обрезающий слой один на элемент: он повторяет его скругление и не ловит указатель. */
|
|
1363
|
+
#ensureArea(host) {
|
|
1364
|
+
if (this.#area?.parentNode) {
|
|
1365
|
+
return this.#area;
|
|
1366
|
+
}
|
|
1367
|
+
/* Обрезающий слой стоит absolute, и ему нужен позиционированный предок. Позиция
|
|
1368
|
+
ставится здесь, а не правилом блока: правило висело бы на каждой кнопке кита и
|
|
1369
|
+
меняло растеризацию кадров витрины, ничего не двигая в раскладке. */
|
|
1370
|
+
if (getComputedStyle(host).position === 'static') {
|
|
1371
|
+
this.#renderer.setStyle(host, 'position', 'relative');
|
|
1372
|
+
}
|
|
1373
|
+
const area = this.#renderer.createElement('span');
|
|
1374
|
+
this.#renderer.addClass(area, `${BEM_BLOCK$1u}__area`);
|
|
1375
|
+
this.#renderer.setAttribute(area, 'aria-hidden', 'true');
|
|
1376
|
+
this.#renderer.appendChild(host, area);
|
|
1377
|
+
this.#area = area;
|
|
1378
|
+
return area;
|
|
1379
|
+
}
|
|
1380
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRippleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1381
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: RtRippleDirective, isStandalone: true, selector: "[rtRipple]", inputs: { rippleDisabled: { classPropertyName: "rippleDisabled", publicName: "rippleDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "launch($event)" }, classAttribute: "rt-ripple" }, ngImport: i0 }); }
|
|
1382
|
+
}
|
|
1383
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRippleDirective, decorators: [{
|
|
1384
|
+
type: Directive,
|
|
1385
|
+
args: [{
|
|
1386
|
+
selector: '[rtRipple]',
|
|
1387
|
+
host: {
|
|
1388
|
+
class: BEM_BLOCK$1u,
|
|
1389
|
+
'(pointerdown)': 'launch($event)',
|
|
1390
|
+
},
|
|
1391
|
+
}]
|
|
1392
|
+
}], ctorParameters: () => [], propDecorators: { rippleDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "rippleDisabled", required: false }] }] } });
|
|
1393
|
+
|
|
1394
|
+
/** Признак материального набора в разметке: атрибут или класс, которые ставит приложение. */
|
|
1395
|
+
const RT_MATERIAL_PRESET_SELECTOR = "[data-preset='material'],.rt-preset-material";
|
|
1396
|
+
/** Класс набора, который несёт узел вне поддерева, где набор объявлен. */
|
|
1397
|
+
const RT_MATERIAL_PRESET_CLASS = 'rt-preset-material';
|
|
1398
|
+
/**
|
|
1399
|
+
* Классы набора для панели наложения, открытой от `origin`. Панель лежит в конце страницы, вне
|
|
1400
|
+
* контейнера с признаком набора, и назначения набора до неё не доходят: она берёт класс набора
|
|
1401
|
+
* сама, если признак стоит над узлом, от которого она открыта. Кит читает признак приложения, а не
|
|
1402
|
+
* ставит свой.
|
|
1403
|
+
*/
|
|
1404
|
+
function materialPresetClassesOf(origin) {
|
|
1405
|
+
return origin.closest(RT_MATERIAL_PRESET_SELECTOR) ? [RT_MATERIAL_PRESET_CLASS] : [];
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Классы набора для панели, которую открывает сервис без узла-источника: диалог, боковая панель.
|
|
1409
|
+
* Источником служит узел в фокусе в момент открытия — нажатая кнопка. Панель, открытая не по
|
|
1410
|
+
* нажатию, набор от фокуса не берёт; для неё остаётся `panelClass` приложения.
|
|
1411
|
+
*/
|
|
1412
|
+
function materialPresetClassesOfFocus(document) {
|
|
1413
|
+
const focused = document.activeElement;
|
|
1414
|
+
return focused ? materialPresetClassesOf(focused) : [];
|
|
1415
|
+
}
|
|
1416
|
+
/** Атрибут местного куска темы — его ставит `rtTheme` и читают правила куска в стилях кита. */
|
|
1417
|
+
const RT_THEME_SCOPE_ATTRIBUTE = 'data-theme';
|
|
1418
|
+
/**
|
|
1419
|
+
* Тема местного куска, от которого открыта панель, — значение ближайшего `data-theme` над `origin`.
|
|
1420
|
+
* Панель лежит в конце страницы, вне куска, и без этого рисуется темой страницы: список тёмной
|
|
1421
|
+
* карточки на светлой странице выходил светлым. Тема корня страницы не возвращается: её панель
|
|
1422
|
+
* наследует и так, а признак на панели повторил бы весь набор назначений ещё раз.
|
|
1423
|
+
*/
|
|
1424
|
+
function themeScopeOf(origin) {
|
|
1425
|
+
const scope = origin.closest(`[${RT_THEME_SCOPE_ATTRIBUTE}]`);
|
|
1426
|
+
if (scope === null || scope === origin.ownerDocument.documentElement) {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
return scope.getAttribute(RT_THEME_SCOPE_ATTRIBUTE) || null;
|
|
1430
|
+
}
|
|
1431
|
+
function applyThemeScope(pane, theme) {
|
|
1432
|
+
if (theme === null) {
|
|
1433
|
+
pane.removeAttribute(RT_THEME_SCOPE_ATTRIBUTE);
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
pane.setAttribute(RT_THEME_SCOPE_ATTRIBUTE, theme);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Переносит тему куска `origin` на коробку панели или снимает её, если кусок ушёл. Зовётся на
|
|
1441
|
+
* каждом открытии: коробка переживает закрытие, а кусок вокруг источника мог смениться.
|
|
1442
|
+
*/
|
|
1443
|
+
function carryThemeScope(pane, origin) {
|
|
1444
|
+
applyThemeScope(pane, themeScopeOf(origin));
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Тема местного куска для панели, которую открывает сервис без узла-источника: диалог, боковая
|
|
1448
|
+
* панель. Источником служит узел в фокусе в момент открытия — нажатая кнопка, как у
|
|
1449
|
+
* `materialPresetClassesOfFocus`. Без фокуса, как и под темой корня, возвращает `null`.
|
|
1450
|
+
*/
|
|
1451
|
+
function themeScopeOfFocus(document) {
|
|
1452
|
+
const focused = document.activeElement;
|
|
1453
|
+
return focused ? themeScopeOf(focused) : null;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Переносит на коробку панели тему куска, где стоит фокус. Зовётся до того, как панель заберёт
|
|
1457
|
+
* фокус себе: после этого `activeElement` уже внутри панели, в конце страницы, вне куска.
|
|
1458
|
+
*/
|
|
1459
|
+
function carryThemeScopeOfFocus(pane, document) {
|
|
1460
|
+
applyThemeScope(pane, themeScopeOfFocus(document));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const BEM_BLOCK$1t = 'rt-tooltip';
|
|
1464
|
+
/**
|
|
1465
|
+
* Презентационная панель tooltip'а. Рендерится директивой `[rtTooltip]` через
|
|
1466
|
+
* CDK `ComponentPortal` в overlay; текст выставляется императивно
|
|
1467
|
+
* (`ref.instance.text.set(...)`) — отдельного `input()` нет, т.к. компонент
|
|
1468
|
+
* создаётся вручную, а не в шаблоне.
|
|
1469
|
+
*
|
|
1470
|
+
* `ViewEncapsulation.None` — стили префиксованы `.rt-tooltip` (единообразно с
|
|
1471
|
+
* остальным common/ui). `role="tooltip"` — вспомогательный ярлык для AT.
|
|
1472
|
+
*/
|
|
1473
|
+
class RtTooltipComponent {
|
|
1474
|
+
constructor() {
|
|
1475
|
+
this.text = signal('', /* @ts-ignore */
|
|
1476
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
1477
|
+
}
|
|
1478
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1479
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: RtTooltipComponent, isStandalone: true, selector: "rt-tooltip", host: { attributes: { "role": "tooltip" }, classAttribute: "rt-tooltip" }, ngImport: i0, template: '{{ text() }}', isInline: true, styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-tooltip{--rt-tooltip-radius: var(--rt-overlay-tooltip-radius);--rt-tooltip-shadow: var(--rt-overlay-tooltip-shadow);display:block;max-width:240px;padding:var(--rt-space-xs) var(--rt-space-sm);border-radius:var(--rt-tooltip-radius);background:var(--rt-color-bg-inverse);box-shadow:var(--rt-tooltip-shadow);color:var(--rt-color-text-inverse);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);overflow-wrap:anywhere;pointer-events:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1480
|
+
}
|
|
1481
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipComponent, decorators: [{
|
|
1482
|
+
type: Component,
|
|
1483
|
+
args: [{ selector: 'rt-tooltip', template: '{{ text() }}', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
1484
|
+
class: BEM_BLOCK$1t,
|
|
1485
|
+
role: 'tooltip',
|
|
1486
|
+
}, styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-tooltip{--rt-tooltip-radius: var(--rt-overlay-tooltip-radius);--rt-tooltip-shadow: var(--rt-overlay-tooltip-shadow);display:block;max-width:240px;padding:var(--rt-space-xs) var(--rt-space-sm);border-radius:var(--rt-tooltip-radius);background:var(--rt-color-bg-inverse);box-shadow:var(--rt-tooltip-shadow);color:var(--rt-color-text-inverse);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);overflow-wrap:anywhere;pointer-events:none}}\n"] }]
|
|
1487
|
+
}] });
|
|
1488
|
+
|
|
1489
|
+
const SHOW_DELAY_MS = 300;
|
|
1490
|
+
const VIEWPORT_MARGIN$1 = 8;
|
|
1491
|
+
/**
|
|
1492
|
+
* Tooltip-директива поверх CDK Overlay. Показывает короткую подсказку
|
|
1493
|
+
* (`rtTooltip` = строка) на hover/focus host'а. Движок — CDK `flexibleConnectedTo`
|
|
1494
|
+
* (авто-flip у края viewport, reposition при скролле), так что подсказка не
|
|
1495
|
+
* режется `overflow: hidden` предками (таблицы, aside, карточки).
|
|
1496
|
+
*
|
|
1497
|
+
* Триггеры: `mouseenter`/`focusin` показывают (с задержкой `SHOW_DELAY_MS`),
|
|
1498
|
+
* `mouseleave`/`focusout`/`click` прячут. Пустой текст → no-op (директива
|
|
1499
|
+
* выключена), поэтому её можно безусловно вешать на icon-кнопки и включать
|
|
1500
|
+
* выставлением строки.
|
|
1501
|
+
*
|
|
1502
|
+
* @example
|
|
1503
|
+
* ```html
|
|
1504
|
+
* <rt-icon-button icon="ico-trash" ariaLabel="Удалить" tooltip="Удалить" />
|
|
1505
|
+
* <button rtButton [rtTooltip]="'Подсказка'">…</button>
|
|
1506
|
+
* ```
|
|
1507
|
+
*/
|
|
1508
|
+
class RtTooltipDirective {
|
|
1509
|
+
constructor() {
|
|
1510
|
+
this.#overlay = inject(Overlay);
|
|
1511
|
+
this.#elementRef = inject(ElementRef);
|
|
1512
|
+
this.#scrollStrategies = inject(ScrollStrategyOptions);
|
|
1513
|
+
this.#overlayRef = null;
|
|
1514
|
+
this.#tooltipRef = null;
|
|
1515
|
+
this.#showTimer = null;
|
|
1516
|
+
/**
|
|
1517
|
+
* Текст подсказки. Принимает `null`/`undefined` (частый результат date-пайпа
|
|
1518
|
+
* или nullable-полей модели) и нормализует их в пустую строку — иначе
|
|
1519
|
+
* биндинги вида `value | date` ломали бы строгую типизацию шаблона. Пустая
|
|
1520
|
+
* строка → no-op (директива выключена).
|
|
1521
|
+
*/
|
|
1522
|
+
this.text = input('', { ...(ngDevMode ? { debugName: "text" } : /* istanbul ignore next */ {}), alias: 'rtTooltip',
|
|
1523
|
+
transform: (value) => value ?? '' });
|
|
1524
|
+
/** Сторона подсказки. Не задана — сверху, а под материальным набором снизу, как у первого кита. */
|
|
1525
|
+
this.placement = input(null, { ...(ngDevMode ? { debugName: "placement" } : /* istanbul ignore next */ {}), alias: 'rtTooltipPlacement' });
|
|
1526
|
+
}
|
|
1527
|
+
#overlay;
|
|
1528
|
+
#elementRef;
|
|
1529
|
+
#scrollStrategies;
|
|
1530
|
+
#overlayRef;
|
|
1531
|
+
#tooltipRef;
|
|
1532
|
+
#showTimer;
|
|
1533
|
+
ngOnDestroy() {
|
|
1534
|
+
this.#clearTimer();
|
|
1535
|
+
this.#disposeOverlay();
|
|
1536
|
+
}
|
|
1537
|
+
show() {
|
|
1538
|
+
if (this.text().trim() === '' || this.#tooltipRef !== null) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
this.#clearTimer();
|
|
1542
|
+
this.#showTimer = setTimeout(() => this.#attach(), SHOW_DELAY_MS);
|
|
1543
|
+
}
|
|
1544
|
+
hide() {
|
|
1545
|
+
this.#clearTimer();
|
|
1546
|
+
this.#overlayRef?.detach();
|
|
1547
|
+
this.#tooltipRef = null;
|
|
1548
|
+
}
|
|
1549
|
+
#attach() {
|
|
1550
|
+
if (this.text().trim() === '') {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const overlayRef = this.#ensureOverlay();
|
|
1554
|
+
// Коробка переживает закрытие, а кусок темы вокруг host'а мог смениться — переносим на каждом показе.
|
|
1555
|
+
carryThemeScope(overlayRef.overlayElement, this.#elementRef.nativeElement);
|
|
1556
|
+
const portal = new ComponentPortal(RtTooltipComponent);
|
|
1557
|
+
this.#tooltipRef = overlayRef.attach(portal);
|
|
1558
|
+
this.#tooltipRef.instance.text.set(this.text());
|
|
1559
|
+
this.#tooltipRef.changeDetectorRef.markForCheck();
|
|
1560
|
+
}
|
|
1561
|
+
#ensureOverlay() {
|
|
1562
|
+
if (this.#overlayRef !== null) {
|
|
1563
|
+
return this.#overlayRef;
|
|
1564
|
+
}
|
|
1565
|
+
const above = {
|
|
1566
|
+
originX: 'center',
|
|
1567
|
+
originY: 'top',
|
|
1568
|
+
overlayX: 'center',
|
|
1569
|
+
overlayY: 'bottom',
|
|
1570
|
+
offsetY: -6,
|
|
1571
|
+
};
|
|
1572
|
+
const below = {
|
|
1573
|
+
originX: 'center',
|
|
1574
|
+
originY: 'bottom',
|
|
1575
|
+
overlayX: 'center',
|
|
1576
|
+
overlayY: 'top',
|
|
1577
|
+
offsetY: 6,
|
|
1578
|
+
};
|
|
1579
|
+
const presetClasses = materialPresetClassesOf(this.#elementRef.nativeElement);
|
|
1580
|
+
const placement = this.placement() ?? (presetClasses.length > 0 ? 'bottom' : 'top');
|
|
1581
|
+
const positions = placement === 'top' ? [above, below] : [below, above];
|
|
1582
|
+
const positionStrategy = this.#overlay
|
|
1583
|
+
.position()
|
|
1584
|
+
.flexibleConnectedTo(this.#elementRef)
|
|
1585
|
+
.withPush(true)
|
|
1586
|
+
.withViewportMargin(VIEWPORT_MARGIN$1)
|
|
1587
|
+
.withFlexibleDimensions(false)
|
|
1588
|
+
.withPositions(positions);
|
|
1589
|
+
const config = new OverlayConfig({
|
|
1590
|
+
positionStrategy,
|
|
1591
|
+
scrollStrategy: this.#scrollStrategies.reposition(),
|
|
1592
|
+
hasBackdrop: false,
|
|
1593
|
+
panelClass: ['rt-tooltip-panel', ...presetClasses],
|
|
1594
|
+
});
|
|
1595
|
+
this.#overlayRef = this.#overlay.create(config);
|
|
1596
|
+
return this.#overlayRef;
|
|
1597
|
+
}
|
|
1598
|
+
#clearTimer() {
|
|
1599
|
+
if (this.#showTimer !== null) {
|
|
1600
|
+
clearTimeout(this.#showTimer);
|
|
1601
|
+
this.#showTimer = null;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
#disposeOverlay() {
|
|
1605
|
+
this.#overlayRef?.dispose();
|
|
1606
|
+
this.#overlayRef = null;
|
|
1607
|
+
this.#tooltipRef = null;
|
|
1608
|
+
}
|
|
1609
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1610
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: RtTooltipDirective, isStandalone: true, selector: "[rtTooltip]", inputs: { text: { classPropertyName: "text", publicName: "rtTooltip", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "rtTooltipPlacement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "show()", "mouseleave": "hide()", "focusin": "show()", "focusout": "hide()", "click": "hide()" } }, exportAs: ["rtTooltip"], ngImport: i0 }); }
|
|
1611
|
+
}
|
|
1612
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtTooltipDirective, decorators: [{
|
|
1613
|
+
type: Directive,
|
|
1614
|
+
args: [{
|
|
1615
|
+
selector: '[rtTooltip]',
|
|
1616
|
+
exportAs: 'rtTooltip',
|
|
1617
|
+
host: {
|
|
1618
|
+
'(mouseenter)': 'show()',
|
|
1619
|
+
'(mouseleave)': 'hide()',
|
|
1620
|
+
'(focusin)': 'show()',
|
|
1621
|
+
'(focusout)': 'hide()',
|
|
1622
|
+
'(click)': 'hide()',
|
|
1623
|
+
},
|
|
1624
|
+
}]
|
|
1625
|
+
}], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtTooltip", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtTooltipPlacement", required: false }] }] } });
|
|
1626
|
+
|
|
1627
|
+
const BEM_BLOCK$1s = 'rt-icon-button';
|
|
1628
|
+
/**
|
|
1629
|
+
* Универсальная icon-only кнопка кита.
|
|
1630
|
+
*
|
|
1631
|
+
* Рендерит свой собственный `<button type="...">` внутри template (НЕ host-button).
|
|
1632
|
+
* Все цвета и размеры — через семантические `--rt-*` токены; геометрия квадрата
|
|
1633
|
+
* выставляется CSS-custom-property `--rt-icon-button-size` (overridable inline
|
|
1634
|
+
* через `style="--rt-icon-button-size: 35px"` — нужно для миграции rt-header 35×35).
|
|
1635
|
+
*
|
|
1636
|
+
* Loading state — заменяет иконку на `<rt-icon name="spinner">` и блокирует
|
|
1637
|
+
* клики (`button[disabled]`). Active state управляет `aria-pressed` (для toggle-кнопок).
|
|
1638
|
+
* Indicator — небольшой dot top-right через BEM-элемент `__indicator`.
|
|
1639
|
+
*
|
|
1640
|
+
* BEM-разметка — через `rtBlock="rt-icon-button"` + `[rtMod]` объект; камелкейс
|
|
1641
|
+
* модификаторов автоматически конвертируется директивой в kebab-case
|
|
1642
|
+
* (`hasIndicator` → `rt-icon-button--has-indicator`).
|
|
1643
|
+
*/
|
|
1644
|
+
class RtIconButtonComponent {
|
|
1645
|
+
constructor() {
|
|
1646
|
+
/**
|
|
1647
|
+
* Размер дочерней `<rt-icon>`. По умолчанию маппится 1-к-1 от размера кнопки,
|
|
1648
|
+
* но потребитель может задать свой: у крупной кнопки-действия (плавающая
|
|
1649
|
+
* кнопка чата) иконка во весь диаметр выглядит тяжеловесно, а сама кнопка
|
|
1650
|
+
* должна остаться большой — в неё целятся пальцем.
|
|
1651
|
+
*/
|
|
1652
|
+
this.resolvedIconSize = computed(() => {
|
|
1653
|
+
const map = {
|
|
1654
|
+
sm: 'sm',
|
|
1655
|
+
md: 'md',
|
|
1656
|
+
lg: 'lg',
|
|
1657
|
+
xl: 'xl',
|
|
1658
|
+
'2xl': '2xl',
|
|
1659
|
+
};
|
|
1660
|
+
return this.iconSize() ?? map[this.size()];
|
|
1661
|
+
}, /* @ts-ignore */
|
|
1662
|
+
...(ngDevMode ? [{ debugName: "resolvedIconSize" }] : /* istanbul ignore next */ []));
|
|
1663
|
+
/** Кнопка отключена, если установлен disabled ИЛИ loading. */
|
|
1664
|
+
this.isDisabled = computed(() => this.disabled() || this.loading(), /* @ts-ignore */
|
|
1665
|
+
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
1666
|
+
/**
|
|
1667
|
+
* BEM-модификаторы для `[rtMod]`. Собираем в .ts (а не в шаблоне), потому что
|
|
1668
|
+
* Angular template-parser не поддерживает computed property keys в inline-объектах.
|
|
1669
|
+
* camelCase ключи `hasIndicator` директива автоматически конвертирует в `has-indicator`.
|
|
1670
|
+
*/
|
|
1671
|
+
this.bemMods = computed(() => ({
|
|
1672
|
+
[this.variant()]: true,
|
|
1673
|
+
[this.size()]: true,
|
|
1674
|
+
[this.shape()]: true,
|
|
1675
|
+
loading: this.loading(),
|
|
1676
|
+
active: this.active(),
|
|
1677
|
+
hasIndicator: this.indicator(),
|
|
1678
|
+
}), /* @ts-ignore */
|
|
1679
|
+
...(ngDevMode ? [{ debugName: "bemMods" }] : /* istanbul ignore next */ []));
|
|
1680
|
+
/** Имя иконки — обязательно. */
|
|
1681
|
+
this.icon = input.required(/* @ts-ignore */
|
|
1682
|
+
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
1683
|
+
/** Доступное имя для screen-reader'ов — обязательно (icon-only кнопка). */
|
|
1684
|
+
this.ariaLabel = input.required(/* @ts-ignore */
|
|
1685
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
1686
|
+
/** Семантическая палитра. */
|
|
1687
|
+
this.variant = input('ghost', /* @ts-ignore */
|
|
1688
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
1689
|
+
/**
|
|
1690
|
+
* Цвет самой иконки. Default `current` — иконка наследует цвет текста кнопки
|
|
1691
|
+
* (поведение по умолчанию). Явное значение нужно, когда иконка должна
|
|
1692
|
+
* отличаться от текста кнопки — например крестик-очистка внутри поля,
|
|
1693
|
+
* который должен совпадать по цвету с иконками-аффордансами самого поля.
|
|
1694
|
+
*/
|
|
1695
|
+
this.iconColor = input('current', /* @ts-ignore */
|
|
1696
|
+
...(ngDevMode ? [{ debugName: "iconColor" }] : /* istanbul ignore next */ []));
|
|
1697
|
+
/**
|
|
1698
|
+
* Залитый рисунок значка вместо контурного — как `fill` у `rt-icon`. Рисует его материальный
|
|
1699
|
+
* набор; значок своего набора один и от признака не меняется.
|
|
1700
|
+
*/
|
|
1701
|
+
this.iconFill = input(false, { ...(ngDevMode ? { debugName: "iconFill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1702
|
+
/** Размер квадрата. */
|
|
1703
|
+
this.size = input('md', /* @ts-ignore */
|
|
1704
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1705
|
+
/** Размер иконки; пусто — берётся от размера кнопки. */
|
|
1706
|
+
this.iconSize = input(null, /* @ts-ignore */
|
|
1707
|
+
...(ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []));
|
|
1708
|
+
/** Форма (круг или скруглённый квадрат). */
|
|
1709
|
+
this.shape = input('square', /* @ts-ignore */
|
|
1710
|
+
...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
|
|
1711
|
+
/** HTML-type для нативного `<button>`. */
|
|
1712
|
+
this.type = input('button', /* @ts-ignore */
|
|
1713
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
1714
|
+
/**
|
|
1715
|
+
* Текст styled-tooltip'а (`rtTooltip`), показывается на hover/focus. Пустая
|
|
1716
|
+
* строка → tooltip выключен. Для icon-only кнопок обычно дублирует `ariaLabel`.
|
|
1717
|
+
*/
|
|
1718
|
+
this.tooltip = input('', /* @ts-ignore */
|
|
1719
|
+
...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
|
|
1720
|
+
/**
|
|
1721
|
+
* Tabindex внутренней `<button>`. Default `0` (в обычном таб-порядке).
|
|
1722
|
+
* Значение `-1` исключает кнопку из таб-навигации, оставляя клик мышью —
|
|
1723
|
+
* нужно для вспомогательных аффордансов вроде крестика-очистки внутри поля.
|
|
1724
|
+
*/
|
|
1725
|
+
this.tabIndex = input(0, { ...(ngDevMode ? { debugName: "tabIndex" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
1726
|
+
/** Состояние загрузки: показывает spinner вместо иконки + disabled. */
|
|
1727
|
+
this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1728
|
+
/** Нативное отключение кнопки. */
|
|
1729
|
+
this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1730
|
+
/** Toggle-состояние — превращается в `aria-pressed="true"`. */
|
|
1731
|
+
this.active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1732
|
+
/** Бейдж-индикатор (dot top-right через BEM-модификатор). */
|
|
1733
|
+
this.indicator = input(false, { ...(ngDevMode ? { debugName: "indicator" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1734
|
+
/** Эмиттит MouseEvent при клике (только когда не disabled и не loading). */
|
|
1735
|
+
this.clicked = output();
|
|
1736
|
+
}
|
|
1737
|
+
onClick(event) {
|
|
1738
|
+
if (this.isDisabled()) {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
this.clicked.emit(event);
|
|
1742
|
+
}
|
|
1743
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1744
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: RtIconButtonComponent, isStandalone: true, selector: "rt-icon-button", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, iconColor: { classPropertyName: "iconColor", publicName: "iconColor", isSignal: true, isRequired: false, transformFunction: null }, iconFill: { classPropertyName: "iconFill", publicName: "iconFill", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, indicator: { classPropertyName: "indicator", publicName: "indicator", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, host: { classAttribute: "rt-icon-button" }, ngImport: i0, template: "<button\n rtBlock=\"rt-icon-button\"\n qa-dataid=\"icon-button-control\"\n rtRipple\n [rippleDisabled]=\"isDisabled() || loading()\"\n [rtTooltip]=\"tooltip()\"\n [rtMod]=\"bemMods()\"\n [type]=\"type()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"active() ? 'true' : null\"\n [attr.tabindex]=\"tabIndex()\"\n (click)=\"onClick($event)\">\n @if (loading()) {\n <rt-icon rtElem=\"spinner\" name=\"spinner\" color=\"current\" [size]=\"resolvedIconSize()\" />\n } @else {\n <rt-icon rtElem=\"icon\" [color]=\"iconColor()\" [name]=\"icon()\" [size]=\"resolvedIconSize()\" [fill]=\"iconFill()\" />\n }\n @if (indicator()) {\n <span rtElem=\"indicator\" aria-hidden=\"true\"></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-icon-button{display:inline-flex;--rt-icon-button-size: var(--rt-control-height-md);--rt-icon-button-color: var(--rt-color-text-primary)}rt-icon-button .rt-icon-button{--rt-icon-button-circle-radius: var(--rt-radius-full);--rt-icon-button-square-radius: var(--rt-radius-md);--rt-icon-button-rounded-sm-radius: calc(var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) / 3);--rt-icon-button-rounded-lg-radius: calc( var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) * 2 / 3 );--rt-icon-button-indicator-radius: var(--rt-radius-full);--rt-icon-button-focus-shadow: var(--rt-shadow-focus-ring);--rt-icon-button-shadow: var(--rt-shadow-none);--rt-icon-button-shadow-hover: var(--rt-icon-button-shadow);--rt-icon-button-color-hover: var(--rt-icon-button-color);--rt-icon-button-transition-duration: var(--rt-duration-fast);--rt-icon-button-bg: transparent;position:relative;display:inline-flex;width:var(--rt-icon-button-size);height:var(--rt-icon-button-size);align-items:center;justify-content:center;padding:0;border:0;background-color:var(--rt-icon-button-bg);box-shadow:var(--rt-icon-button-shadow);color:var(--rt-icon-button-color);cursor:pointer;transition:background-color var(--rt-icon-button-transition-duration) ease,box-shadow var(--rt-icon-button-transition-duration) ease}rt-icon-button .rt-icon-button--sm{--rt-icon-button-size: var(--rt-control-height-sm)}rt-icon-button .rt-icon-button--md{--rt-icon-button-size: var(--rt-control-height-md)}rt-icon-button .rt-icon-button--lg{--rt-icon-button-size: var(--rt-control-height-lg)}rt-icon-button .rt-icon-button--xl{--rt-icon-button-size: var(--rt-control-height-xl)}rt-icon-button .rt-icon-button--2xl{--rt-icon-button-size: var(--rt-control-height-2xl)}rt-icon-button .rt-icon-button--circle{border-radius:var(--rt-icon-button-circle-radius)}rt-icon-button .rt-icon-button--rounded-sm{border-radius:var(--rt-icon-button-rounded-sm-radius)}rt-icon-button .rt-icon-button--rounded-lg{border-radius:var(--rt-icon-button-rounded-lg-radius)}rt-icon-button .rt-icon-button--square{border-radius:var(--rt-icon-button-square-radius)}rt-icon-button .rt-icon-button:hover:not(:disabled){box-shadow:var(--rt-icon-button-shadow-hover)}rt-icon-button .rt-icon-button--ghost:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover);color:var(--rt-icon-button-color-hover)}rt-icon-button .rt-icon-button--ghost.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--primary{--rt-icon-button-bg: var(--rt-color-action-primary);color:var(--rt-color-action-on-primary)}rt-icon-button .rt-icon-button--primary:hover:not(:disabled){background-color:var(--rt-color-action-primary-hover)}rt-icon-button .rt-icon-button--secondary{--rt-icon-button-bg: var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary)}rt-icon-button .rt-icon-button--secondary:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger{color:var(--rt-color-state-danger)}rt-icon-button .rt-icon-button--danger:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success{color:var(--rt-color-state-success)}rt-icon-button .rt-icon-button--success:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning{color:var(--rt-color-state-warning)}rt-icon-button .rt-icon-button--warning:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button:disabled{background-color:color-mix(in srgb,var(--rt-icon-button-bg) var(--rt-icon-button-disabled-fill),transparent);cursor:not-allowed;opacity:var(--rt-icon-button-disabled-opacity)}rt-icon-button .rt-icon-button:disabled .rt-icon svg{color:var(--rt-icon-button-disabled-color)}rt-icon-button .rt-icon-button:focus-visible{box-shadow:var(--rt-icon-button-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-icon-button .rt-icon-button__spinner{--rt-icon-button-spin-duration: 1s;animation:rt-icon-button-spin var(--rt-icon-button-spin-duration) linear infinite}rt-icon-button .rt-icon-button__indicator{position:absolute;width:8px;height:8px;border-radius:var(--rt-icon-button-indicator-radius);background:var(--rt-color-state-danger);inset-block-start:-2px;inset-inline-end:-2px;pointer-events:none}@keyframes rt-icon-button-spin{to{transform:rotate(360deg)}}}\n"], dependencies: [{ kind: "component", type:
|
|
1745
|
+
// standalone components / directives
|
|
1746
|
+
RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "fill", "rotate"] }, { kind: "directive", type: RtRippleDirective, selector: "[rtRipple]", inputs: ["rippleDisabled"] }, { kind: "directive", type: RtTooltipDirective, selector: "[rtTooltip]", inputs: ["rtTooltip", "rtTooltipPlacement"], exportAs: ["rtTooltip"] }, { kind: "directive", type: BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1747
|
+
}
|
|
1748
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtIconButtonComponent, decorators: [{
|
|
1749
|
+
type: Component,
|
|
1750
|
+
args: [{ selector: 'rt-icon-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
|
|
1751
|
+
// standalone components / directives
|
|
1752
|
+
RtIconComponent,
|
|
1753
|
+
RtRippleDirective,
|
|
1754
|
+
RtTooltipDirective,
|
|
1755
|
+
BlockDirective,
|
|
1756
|
+
ElemDirective,
|
|
1757
|
+
ModDirective,
|
|
1758
|
+
], host: {
|
|
1759
|
+
class: BEM_BLOCK$1s,
|
|
1760
|
+
}, template: "<button\n rtBlock=\"rt-icon-button\"\n qa-dataid=\"icon-button-control\"\n rtRipple\n [rippleDisabled]=\"isDisabled() || loading()\"\n [rtTooltip]=\"tooltip()\"\n [rtMod]=\"bemMods()\"\n [type]=\"type()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"active() ? 'true' : null\"\n [attr.tabindex]=\"tabIndex()\"\n (click)=\"onClick($event)\">\n @if (loading()) {\n <rt-icon rtElem=\"spinner\" name=\"spinner\" color=\"current\" [size]=\"resolvedIconSize()\" />\n } @else {\n <rt-icon rtElem=\"icon\" [color]=\"iconColor()\" [name]=\"icon()\" [size]=\"resolvedIconSize()\" [fill]=\"iconFill()\" />\n }\n @if (indicator()) {\n <span rtElem=\"indicator\" aria-hidden=\"true\"></span>\n }\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-icon-button{display:inline-flex;--rt-icon-button-size: var(--rt-control-height-md);--rt-icon-button-color: var(--rt-color-text-primary)}rt-icon-button .rt-icon-button{--rt-icon-button-circle-radius: var(--rt-radius-full);--rt-icon-button-square-radius: var(--rt-radius-md);--rt-icon-button-rounded-sm-radius: calc(var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) / 3);--rt-icon-button-rounded-lg-radius: calc( var(--rt-icon-button-square-radius) + (50% - var(--rt-icon-button-square-radius)) * 2 / 3 );--rt-icon-button-indicator-radius: var(--rt-radius-full);--rt-icon-button-focus-shadow: var(--rt-shadow-focus-ring);--rt-icon-button-shadow: var(--rt-shadow-none);--rt-icon-button-shadow-hover: var(--rt-icon-button-shadow);--rt-icon-button-color-hover: var(--rt-icon-button-color);--rt-icon-button-transition-duration: var(--rt-duration-fast);--rt-icon-button-bg: transparent;position:relative;display:inline-flex;width:var(--rt-icon-button-size);height:var(--rt-icon-button-size);align-items:center;justify-content:center;padding:0;border:0;background-color:var(--rt-icon-button-bg);box-shadow:var(--rt-icon-button-shadow);color:var(--rt-icon-button-color);cursor:pointer;transition:background-color var(--rt-icon-button-transition-duration) ease,box-shadow var(--rt-icon-button-transition-duration) ease}rt-icon-button .rt-icon-button--sm{--rt-icon-button-size: var(--rt-control-height-sm)}rt-icon-button .rt-icon-button--md{--rt-icon-button-size: var(--rt-control-height-md)}rt-icon-button .rt-icon-button--lg{--rt-icon-button-size: var(--rt-control-height-lg)}rt-icon-button .rt-icon-button--xl{--rt-icon-button-size: var(--rt-control-height-xl)}rt-icon-button .rt-icon-button--2xl{--rt-icon-button-size: var(--rt-control-height-2xl)}rt-icon-button .rt-icon-button--circle{border-radius:var(--rt-icon-button-circle-radius)}rt-icon-button .rt-icon-button--rounded-sm{border-radius:var(--rt-icon-button-rounded-sm-radius)}rt-icon-button .rt-icon-button--rounded-lg{border-radius:var(--rt-icon-button-rounded-lg-radius)}rt-icon-button .rt-icon-button--square{border-radius:var(--rt-icon-button-square-radius)}rt-icon-button .rt-icon-button:hover:not(:disabled){box-shadow:var(--rt-icon-button-shadow-hover)}rt-icon-button .rt-icon-button--ghost:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover);color:var(--rt-icon-button-color-hover)}rt-icon-button .rt-icon-button--ghost.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--primary{--rt-icon-button-bg: var(--rt-color-action-primary);color:var(--rt-color-action-on-primary)}rt-icon-button .rt-icon-button--primary:hover:not(:disabled){background-color:var(--rt-color-action-primary-hover)}rt-icon-button .rt-icon-button--secondary{--rt-icon-button-bg: var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary)}rt-icon-button .rt-icon-button--secondary:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger{color:var(--rt-color-state-danger)}rt-icon-button .rt-icon-button--danger:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--danger.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success{color:var(--rt-color-state-success)}rt-icon-button .rt-icon-button--success:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--success.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning{color:var(--rt-color-state-warning)}rt-icon-button .rt-icon-button--warning:hover:not(:disabled){background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button--warning.rt-icon-button--active{background-color:var(--rt-icon-button-color-bg-hover)}rt-icon-button .rt-icon-button:disabled{background-color:color-mix(in srgb,var(--rt-icon-button-bg) var(--rt-icon-button-disabled-fill),transparent);cursor:not-allowed;opacity:var(--rt-icon-button-disabled-opacity)}rt-icon-button .rt-icon-button:disabled .rt-icon svg{color:var(--rt-icon-button-disabled-color)}rt-icon-button .rt-icon-button:focus-visible{box-shadow:var(--rt-icon-button-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-icon-button .rt-icon-button__spinner{--rt-icon-button-spin-duration: 1s;animation:rt-icon-button-spin var(--rt-icon-button-spin-duration) linear infinite}rt-icon-button .rt-icon-button__indicator{position:absolute;width:8px;height:8px;border-radius:var(--rt-icon-button-indicator-radius);background:var(--rt-color-state-danger);inset-block-start:-2px;inset-inline-end:-2px;pointer-events:none}@keyframes rt-icon-button-spin{to{transform:rotate(360deg)}}}\n"] }]
|
|
1761
|
+
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], iconColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconColor", required: false }] }], iconFill: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconFill", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], iconSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconSize", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], indicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicator", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Хэндл к программно открытой модалке (через `RtDialogService.open()`).
|
|
1765
|
+
*
|
|
1766
|
+
* Контент-компонент инжектит `RtDialogRef` и закрывает модалку через `.close(result?)`.
|
|
1767
|
+
* Родитель, открывший модалку, ждёт результата через `.afterClosed()`.
|
|
1768
|
+
*
|
|
1769
|
+
* Generic `<T>` — тип результата `close(result)`. Дефолт `unknown` — если результат
|
|
1770
|
+
* не нужен, оставляй generic пустым.
|
|
1771
|
+
*
|
|
1772
|
+
* @example
|
|
1773
|
+
* \`\`\`ts
|
|
1774
|
+
* // в контент-компоненте
|
|
1775
|
+
* private readonly dialogRef = inject(RtDialogRef);
|
|
1776
|
+
* close(): void { this.dialogRef.close({ confirmed: true }); }
|
|
1777
|
+
*
|
|
1778
|
+
* // в родителе
|
|
1779
|
+
* dialogService.open(MyDialogComponent).afterClosed().subscribe((result) => {...});
|
|
1780
|
+
* \`\`\`
|
|
1781
|
+
*/
|
|
1782
|
+
class RtDialogRef {
|
|
1783
|
+
#overlayRef;
|
|
1784
|
+
#afterClosedSource;
|
|
1785
|
+
constructor(overlayRef) {
|
|
1786
|
+
/**
|
|
1787
|
+
* Runtime-флаг, блокирующий close() через ESC / backdrop click.
|
|
1788
|
+
*
|
|
1789
|
+
* Default `false`. Включай (`dialogRef.disableClose.set(true)`) на
|
|
1790
|
+
* время in-flight submit'а, чтобы юзер не потерял введённые в форму
|
|
1791
|
+
* данные на случайном ESC. После next/error subscribe ставь обратно
|
|
1792
|
+
* в `false`. Не влияет на программный `close(result)` — он всегда
|
|
1793
|
+
* закрывает overlay.
|
|
1794
|
+
*
|
|
1795
|
+
* Статические config-флаги `closeOnEscape` / `closeOnBackdropClick`
|
|
1796
|
+
* (см. `IRtDialogConfig`) определяют, подписан ли вообще handler на
|
|
1797
|
+
* соответствующее событие; `disableClose` — это runtime-override,
|
|
1798
|
+
* который гейтит dispatch внутри уже-подписанного handler'а.
|
|
1799
|
+
*/
|
|
1800
|
+
this.disableClose = signal(false, /* @ts-ignore */
|
|
1801
|
+
...(ngDevMode ? [{ debugName: "disableClose" }] : /* istanbul ignore next */ []));
|
|
1802
|
+
this.#afterClosedSource = new Subject();
|
|
1803
|
+
this.#overlayRef = overlayRef;
|
|
1804
|
+
}
|
|
1805
|
+
/** Закрывает overlay; опционально передаёт результат подписчикам `afterClosed()`. */
|
|
1806
|
+
close(result) {
|
|
1807
|
+
this.#overlayRef.dispose();
|
|
1808
|
+
this.#afterClosedSource.next(result);
|
|
1809
|
+
this.#afterClosedSource.complete();
|
|
1810
|
+
}
|
|
1811
|
+
/** Эмиттит один раз — результат `close(result)` — и завершается. */
|
|
1812
|
+
afterClosed() {
|
|
1813
|
+
return this.#afterClosedSource.asObservable();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Injection token для передачи данных в модалку, открытую через `RtDialogService.open()`.
|
|
1943
1819
|
*
|
|
1944
|
-
*
|
|
1945
|
-
*
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
1820
|
+
* Пользовательский компонент-контент модалки инжектит токен и получает данные,
|
|
1821
|
+
* переданные в `DialogConfig.data`. Тип значения задаётся generic'ом — `inject(RT_DIALOG_DATA)`
|
|
1822
|
+
* вернёт `unknown`, прокастуй через `inject(RT_DIALOG_DATA) as MyDataType` ИЛИ
|
|
1823
|
+
* объяви токен с конкретным generic'ом в feature-либе.
|
|
1824
|
+
*
|
|
1825
|
+
* @example
|
|
1826
|
+
* \`\`\`ts
|
|
1827
|
+
* @Component({...})
|
|
1828
|
+
* export class WelcomeModalComponent {
|
|
1829
|
+
* readonly data = inject(RT_DIALOG_DATA) as { userName: string };
|
|
1830
|
+
* }
|
|
1831
|
+
*
|
|
1832
|
+
* // parent
|
|
1833
|
+
* dialogService.open(WelcomeModalComponent, { data: { userName: "Иван" } });
|
|
1834
|
+
* \`\`\`
|
|
1835
|
+
*/
|
|
1836
|
+
const RT_DIALOG_DATA = new InjectionToken('RT_DIALOG_DATA');
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Программный API для открытия модалок поверх CDK Overlay.
|
|
1840
|
+
*
|
|
1841
|
+
* Устройство сервиса:
|
|
1842
|
+
* - `Overlay.position().global().centerHorizontally().centerVertically()` — центрирование.
|
|
1843
|
+
* - `scrollStrategies.block()` — фиксирует background-scroll.
|
|
1844
|
+
* - `hasBackdrop: true` + кастомный класс — затемнение.
|
|
1845
|
+
* - `ComponentPortal` + custom `Injector` подкидывает `RtDialogRef` + `RT_DIALOG_DATA` в DI
|
|
1846
|
+
* контент-компонента.
|
|
1847
|
+
*
|
|
1848
|
+
* Альтернатива: legacy inline `<rt-dialog [visible]>` форма продолжает работать через
|
|
1849
|
+
* native `<dialog>.showModal()`. Новые экраны — на программный API.
|
|
1850
|
+
*
|
|
1851
|
+
* @example
|
|
1852
|
+
* \`\`\`ts
|
|
1853
|
+
* // открытие
|
|
1854
|
+
* private readonly dialogService = inject(RtDialogService);
|
|
1855
|
+
*
|
|
1856
|
+
* openConfirm(): void {
|
|
1857
|
+
* const ref = this.dialogService.open<ConfirmDialogComponent, { question: string }, boolean>(
|
|
1858
|
+
* ConfirmDialogComponent,
|
|
1859
|
+
* { data: { question: "Удалить?" } }
|
|
1860
|
+
* );
|
|
1861
|
+
* ref.afterClosed().subscribe((confirmed) => {
|
|
1862
|
+
* if (confirmed) this.delete();
|
|
1863
|
+
* });
|
|
1864
|
+
* }
|
|
1865
|
+
*
|
|
1866
|
+
* // контент-компонент
|
|
1867
|
+
* @Component({
|
|
1868
|
+
* selector: "rt-confirm-dialog",
|
|
1869
|
+
* template: \`
|
|
1870
|
+
* <rt-dialog size="sm">
|
|
1871
|
+
* <rt-dialog-header [title]="data.question" />
|
|
1872
|
+
* <div>Действие необратимо.</div>
|
|
1873
|
+
* <rt-dialog-footer>
|
|
1874
|
+
* <button rtButton (click)="close(false)">Отмена</button>
|
|
1875
|
+
* <button rtButton theme="danger" (click)="close(true)">Удалить</button>
|
|
1876
|
+
* </rt-dialog-footer>
|
|
1877
|
+
* </rt-dialog>
|
|
1878
|
+
* \`,
|
|
1879
|
+
* })
|
|
1880
|
+
* export class ConfirmDialogComponent {
|
|
1881
|
+
* readonly data = inject(RT_DIALOG_DATA) as { question: string };
|
|
1882
|
+
* readonly #ref = inject(RtDialogRef<boolean>);
|
|
1883
|
+
* close(result: boolean): void { this.#ref.close(result); }
|
|
1951
1884
|
* }
|
|
1885
|
+
* \`\`\`
|
|
1886
|
+
*/
|
|
1887
|
+
class RtDialogService {
|
|
1888
|
+
#overlay = inject(Overlay);
|
|
1889
|
+
#injector = inject(Injector);
|
|
1890
|
+
#document = inject(DOCUMENT);
|
|
1891
|
+
#openSource = new Subject();
|
|
1892
|
+
constructor() {
|
|
1893
|
+
// Per-open close-подписки (backdrop / ESC) живут в одном постоянном
|
|
1894
|
+
// конструкторном стриме: open() эмитит контекст открытия, mergeMap
|
|
1895
|
+
// подписывает event-стримы конкретного OverlayRef. `detachments()`
|
|
1896
|
+
// терминирует внутренний поток при overlay.dispose() (дёргается из
|
|
1897
|
+
// RtDialogRef.close()). mergeMap, а не switchMap — одновременно может
|
|
1898
|
+
// быть открыто несколько модалок (стек).
|
|
1899
|
+
//
|
|
1900
|
+
// Внутри обоих handler'ов — runtime-guard через `dialogRef.disableClose()`:
|
|
1901
|
+
// если контент-компонент выставил `disableClose.set(true)` (например, на
|
|
1902
|
+
// время in-flight submit'а), ESC / backdrop НЕ закрывают overlay.
|
|
1903
|
+
// Программный `dialogRef.close(result)` гейтом не задет — он всегда работает.
|
|
1904
|
+
this.#openSource
|
|
1905
|
+
.pipe(mergeMap((openContext) => {
|
|
1906
|
+
const backdropClose$ = openContext.closeOnBackdropClick
|
|
1907
|
+
? openContext.overlayRef.backdropClick().pipe(map(() => () => {
|
|
1908
|
+
if (openContext.dialogRef.disableClose()) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
openContext.dialogRef.close();
|
|
1912
|
+
}))
|
|
1913
|
+
: EMPTY;
|
|
1914
|
+
const escapeClose$ = openContext.closeOnEscape
|
|
1915
|
+
? openContext.overlayRef.keydownEvents().pipe(map((event) => () => {
|
|
1916
|
+
if (event.key !== 'Escape') {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
event.preventDefault();
|
|
1920
|
+
if (openContext.dialogRef.disableClose()) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
openContext.dialogRef.close();
|
|
1924
|
+
}))
|
|
1925
|
+
: EMPTY;
|
|
1926
|
+
return merge(backdropClose$, escapeClose$).pipe(takeUntil(openContext.overlayRef.detachments()));
|
|
1927
|
+
}), takeUntilDestroyed())
|
|
1928
|
+
.subscribe((handleCloseIntent) => handleCloseIntent());
|
|
1929
|
+
}
|
|
1930
|
+
open(component, config) {
|
|
1931
|
+
const overlayRef = this.#overlay.create({
|
|
1932
|
+
positionStrategy: this.#overlay.position().global().centerHorizontally().centerVertically(),
|
|
1933
|
+
scrollStrategy: this.#overlay.scrollStrategies.block(),
|
|
1934
|
+
hasBackdrop: true,
|
|
1935
|
+
backdropClass: config?.backdropClass ?? 'rt-dialog-backdrop',
|
|
1936
|
+
panelClass: [config?.panelClass ?? 'rt-dialog-overlay', materialPresetClassesOfFocus(this.#document)].flat(),
|
|
1937
|
+
disposeOnNavigation: true,
|
|
1938
|
+
});
|
|
1939
|
+
// До attach: содержимое диалога может забрать фокус, и кусок темы у кнопки будет потерян.
|
|
1940
|
+
carryThemeScopeOfFocus(overlayRef.overlayElement, this.#document);
|
|
1941
|
+
const dialogRef = new RtDialogRef(overlayRef);
|
|
1942
|
+
// Сами close-подписки объявлены один раз в конструкторе — здесь только
|
|
1943
|
+
// эмит контекста открытия (см. #openSource-стрим).
|
|
1944
|
+
this.#openSource.next({
|
|
1945
|
+
overlayRef,
|
|
1946
|
+
dialogRef,
|
|
1947
|
+
closeOnBackdropClick: config?.closeOnBackdropClick !== false,
|
|
1948
|
+
closeOnEscape: config?.closeOnEscape !== false,
|
|
1949
|
+
});
|
|
1950
|
+
const injector = Injector.create({
|
|
1951
|
+
parent: this.#injector,
|
|
1952
|
+
providers: [
|
|
1953
|
+
{ provide: RtDialogRef, useValue: dialogRef },
|
|
1954
|
+
{ provide: RT_DIALOG_DATA, useValue: config?.data ?? null },
|
|
1955
|
+
],
|
|
1956
|
+
});
|
|
1957
|
+
const portal = new ComponentPortal(component, null, injector);
|
|
1958
|
+
overlayRef.attach(portal);
|
|
1959
|
+
return dialogRef;
|
|
1960
|
+
}
|
|
1961
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1962
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, providedIn: 'root' }); }
|
|
1963
|
+
}
|
|
1964
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtDialogService, decorators: [{
|
|
1965
|
+
type: Injectable,
|
|
1966
|
+
args: [{ providedIn: 'root' }]
|
|
1967
|
+
}], ctorParameters: () => [] });
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Настройки кита, которые приложение дало на старте.
|
|
1971
|
+
*
|
|
1972
|
+
* Токен объявлен **значением**, а не сигналом, и этим отличается от подписей и
|
|
1973
|
+
* от темы: настройки — снимок старта. Их читают один раз, когда узел создан, и
|
|
1974
|
+
* поздняя подмена на уже нарисованное не действует. Тому, что меняется на живой
|
|
1975
|
+
* странице, место во входе узла или в службе темы.
|
|
1976
|
+
*
|
|
1977
|
+
* Умолчание — пустой объект: кит без единой настройки рисует то же, что рисовал
|
|
1978
|
+
* до их появления.
|
|
1979
|
+
*/
|
|
1980
|
+
const RT_KIT_CONFIG = new InjectionToken('RT_KIT_CONFIG', {
|
|
1981
|
+
providedIn: 'root',
|
|
1982
|
+
factory: () => ({}),
|
|
1983
|
+
});
|
|
1984
|
+
/**
|
|
1985
|
+
* Разрешение умолчания — одно место на весь кит, а не повтор в каждом узле.
|
|
1986
|
+
*
|
|
1987
|
+
* Порядок от частного к общему: вход на месте вызова, умолчание узла, общее
|
|
1988
|
+
* умолчание, умолчание кита. Первый найденный ответ выигрывает, поиск на нём
|
|
1989
|
+
* останавливается. Вход на месте сюда не приходит вовсе — он перебивает всё
|
|
1990
|
+
* тем, что значение отсюда становится лишь тем, **с чего вход стартует**.
|
|
1991
|
+
*
|
|
1992
|
+
* Зовётся в поле класса: там есть контекст внедрения.
|
|
1993
|
+
*
|
|
1994
|
+
* ```typescript
|
|
1995
|
+
* readonly #size: IButton.Size = rtKitDefault('button', (it: IRtKitConfig.Button): IButton.Size | undefined => it.size, 'md');
|
|
1996
|
+
* public readonly size: InputSignal<IButton.Size> = input<IButton.Size>(this.#size);
|
|
1952
1997
|
* ```
|
|
1953
1998
|
*
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1999
|
+
* @param node Имя узла — ключ раздела умолчаний по узлам.
|
|
2000
|
+
* @param pick Что взять из умолчаний этого узла.
|
|
2001
|
+
* @param fallback Умолчание кита: то, что рисуется, когда не сказано ничего.
|
|
2002
|
+
* @param pickCommon Что взять из общих умолчаний, если поле живёт и там. Поля,
|
|
2003
|
+
* стоящего на обоих уровнях, сегодня нет ни одного — ступень объявлена под
|
|
2004
|
+
* поле, которое назовёт владелец, и до тех пор не зовётся.
|
|
2005
|
+
*/
|
|
2006
|
+
function rtKitDefault(node, pick, fallback, pickCommon) {
|
|
2007
|
+
const config = inject(RT_KIT_CONFIG);
|
|
2008
|
+
const nodeSettings = config.components?.[node];
|
|
2009
|
+
const fromNode = nodeSettings === undefined ? undefined : pick(nodeSettings);
|
|
2010
|
+
if (fromNode !== undefined) {
|
|
2011
|
+
return fromNode;
|
|
2012
|
+
}
|
|
2013
|
+
const global = config.global;
|
|
2014
|
+
const fromCommon = pickCommon === undefined || global === undefined ? undefined : pickCommon(global);
|
|
2015
|
+
return fromCommon ?? fallback;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Отдаёт киту настройки приложения.
|
|
1956
2019
|
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
2020
|
+
* Раздача необязательна: без неё работают умолчания кита. Частично заданный
|
|
2021
|
+
* объект не обнуляет остальное — неназванное поле проваливается на уровень
|
|
2022
|
+
* выше.
|
|
1959
2023
|
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
2024
|
+
* ```typescript
|
|
2025
|
+
* export const appConfig: ApplicationConfig = {
|
|
2026
|
+
* providers: [
|
|
2027
|
+
* provideRtKit({
|
|
2028
|
+
* global: { theme: 'auto' },
|
|
2029
|
+
* components: { button: { size: 'lg' }, aside: { closeOnEscape: false } },
|
|
2030
|
+
* }),
|
|
2031
|
+
* ],
|
|
2032
|
+
* };
|
|
2033
|
+
* ```
|
|
1962
2034
|
*
|
|
1963
|
-
* @param
|
|
1964
|
-
* @param materialBaseUrl Адрес материального набора. По умолчанию `/icons-material`.
|
|
2035
|
+
* @param config Настройки целиком. Всё в них необязательно.
|
|
1965
2036
|
*/
|
|
1966
|
-
function
|
|
1967
|
-
return makeEnvironmentProviders([
|
|
1968
|
-
...(baseUrl === undefined ? [] : [{ provide: RT_ICONS_BASE_URL, useValue: baseUrl }]),
|
|
1969
|
-
...(materialBaseUrl === undefined ? [] : [{ provide: RT_ICONS_MATERIAL_BASE_URL, useValue: materialBaseUrl }]),
|
|
1970
|
-
]);
|
|
2037
|
+
function provideRtKit(config) {
|
|
2038
|
+
return makeEnvironmentProviders([{ provide: RT_KIT_CONFIG, useValue: config }]);
|
|
1971
2039
|
}
|
|
1972
2040
|
|
|
1973
2041
|
const BEM_BLOCK$1r = 'rt-button';
|
|
@@ -4514,7 +4582,7 @@ class RtCheckboxComponent {
|
|
|
4514
4582
|
useExisting: forwardRef(() => RtCheckboxComponent),
|
|
4515
4583
|
multi: true,
|
|
4516
4584
|
},
|
|
4517
|
-
], ngImport: i0, template: "<button\n rtBlock=\"rt-checkbox\"\n qa-dataid=\"checkbox-control\"\n type=\"button\"\n role=\"checkbox\"\n [rtMod]=\"{\n checked: isChecked() && !indeterminate(),\n indeterminate: indeterminate(),\n disabled: isDisabled(),\n }\"\n [attr.id]=\"inputId()\"\n [attr.aria-checked]=\"indeterminate() ? 'mixed' : isChecked()\"\n [attr.aria-label]=\"ariaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\">\n <span rtElem=\"box\">\n @if (indeterminate()) {\n <rt-icon rtElem=\"icon\" name=\"minus\" size=\"xs\" color=\"inverse\" />\n } @else if (isChecked()) {\n <rt-icon rtElem=\"icon\" name=\"check\" size=\"xs\" color=\"inverse\" />\n }\n </span>\n\n <span rtElem=\"label\" qa-dataid=\"checkbox-label\">\n <ng-content />\n </span>\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-checkbox{display:inline-block}rt-checkbox .rt-checkbox{--rt-checkbox-focus-radius: var(--rt-radius-sm);--rt-checkbox-focus-shadow: var(--rt-shadow-focus-ring);--rt-checkbox-box-radius: var(--rt-radius-sm);--rt-checkbox-transition-duration: var(--rt-duration-fast);--rt-checkbox-color-label: var(--rt-color-text-primary);display:inline-flex;align-items:center;padding:0;border:none;background:none;cursor:pointer;font:inherit;gap:var(--rt-space-sm);text-align:start}rt-checkbox .rt-checkbox:focus-visible{border-radius:var(--rt-checkbox-focus-radius);box-shadow:var(--rt-checkbox-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-checkbox .rt-checkbox--disabled{cursor:not-allowed;opacity:.5}rt-checkbox .rt-checkbox__box{display:inline-flex;width:18px;height:18px;flex-shrink:0;align-items:center;justify-content:center;border:var(--rt-input-border-width) solid var(--rt-color-border-strong);border-radius:var(--rt-checkbox-box-radius);background:var(--rt-color-bg-surface);transition:background var(--rt-checkbox-transition-duration) ease,border-color var(--rt-checkbox-transition-duration) ease}rt-checkbox .rt-checkbox--checked .rt-checkbox__box,rt-checkbox .rt-checkbox--indeterminate .rt-checkbox__box{border-color:var(--rt-color-action-primary);background:var(--rt-color-action-primary)}rt-checkbox .rt-checkbox__label{color:var(--rt-checkbox-color-label);font-size:var(--rt-text-sm);-webkit-user-select:none;user-select:none}rt-checkbox .rt-checkbox__label:empty{display:none}}\n"], dependencies: [{ kind: "component", type:
|
|
4585
|
+
], ngImport: i0, template: "<button\n rtBlock=\"rt-checkbox\"\n qa-dataid=\"checkbox-control\"\n type=\"button\"\n role=\"checkbox\"\n [rtMod]=\"{\n checked: isChecked() && !indeterminate(),\n indeterminate: indeterminate(),\n disabled: isDisabled(),\n }\"\n [attr.id]=\"inputId()\"\n [attr.aria-checked]=\"indeterminate() ? 'mixed' : isChecked()\"\n [attr.aria-label]=\"ariaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\">\n <span rtElem=\"box\">\n @if (indeterminate()) {\n <rt-icon rtElem=\"icon\" name=\"minus\" size=\"xs\" color=\"inverse\" />\n } @else if (isChecked()) {\n <rt-icon rtElem=\"icon\" name=\"check\" size=\"xs\" color=\"inverse\" />\n }\n </span>\n\n <span rtElem=\"label\" qa-dataid=\"checkbox-label\">\n <ng-content />\n </span>\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-checkbox{display:inline-block}rt-checkbox .rt-checkbox{--rt-checkbox-focus-radius: var(--rt-radius-sm);--rt-checkbox-focus-shadow: var(--rt-shadow-focus-ring);--rt-checkbox-box-radius: var(--rt-radius-sm);--rt-checkbox-transition-duration: var(--rt-duration-fast);--rt-checkbox-color-label: var(--rt-color-text-primary);display:inline-flex;align-items:center;padding:0;border:none;background:none;cursor:pointer;font:inherit;gap:var(--rt-space-sm);text-align:start;vertical-align:top}rt-checkbox .rt-checkbox:focus-visible{border-radius:var(--rt-checkbox-focus-radius);box-shadow:var(--rt-checkbox-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-checkbox .rt-checkbox--disabled{cursor:not-allowed;opacity:.5}rt-checkbox .rt-checkbox__box{display:inline-flex;width:18px;height:18px;flex-shrink:0;align-items:center;justify-content:center;border:var(--rt-input-border-width) solid var(--rt-color-border-strong);border-radius:var(--rt-checkbox-box-radius);background:var(--rt-color-bg-surface);transition:background var(--rt-checkbox-transition-duration) ease,border-color var(--rt-checkbox-transition-duration) ease}rt-checkbox .rt-checkbox--checked .rt-checkbox__box,rt-checkbox .rt-checkbox--indeterminate .rt-checkbox__box{border-color:var(--rt-color-action-primary);background:var(--rt-color-action-primary)}rt-checkbox .rt-checkbox__label{color:var(--rt-checkbox-color-label);font-size:var(--rt-text-sm);-webkit-user-select:none;user-select:none}rt-checkbox .rt-checkbox__label:empty{display:none}}\n"], dependencies: [{ kind: "component", type:
|
|
4518
4586
|
// standalone components / directives
|
|
4519
4587
|
RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "fill", "rotate"] }, { kind: "directive", type: BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
4520
4588
|
}
|
|
@@ -4534,7 +4602,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
4534
4602
|
},
|
|
4535
4603
|
], host: {
|
|
4536
4604
|
class: BEM_BLOCK$19,
|
|
4537
|
-
}, template: "<button\n rtBlock=\"rt-checkbox\"\n qa-dataid=\"checkbox-control\"\n type=\"button\"\n role=\"checkbox\"\n [rtMod]=\"{\n checked: isChecked() && !indeterminate(),\n indeterminate: indeterminate(),\n disabled: isDisabled(),\n }\"\n [attr.id]=\"inputId()\"\n [attr.aria-checked]=\"indeterminate() ? 'mixed' : isChecked()\"\n [attr.aria-label]=\"ariaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\">\n <span rtElem=\"box\">\n @if (indeterminate()) {\n <rt-icon rtElem=\"icon\" name=\"minus\" size=\"xs\" color=\"inverse\" />\n } @else if (isChecked()) {\n <rt-icon rtElem=\"icon\" name=\"check\" size=\"xs\" color=\"inverse\" />\n }\n </span>\n\n <span rtElem=\"label\" qa-dataid=\"checkbox-label\">\n <ng-content />\n </span>\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-checkbox{display:inline-block}rt-checkbox .rt-checkbox{--rt-checkbox-focus-radius: var(--rt-radius-sm);--rt-checkbox-focus-shadow: var(--rt-shadow-focus-ring);--rt-checkbox-box-radius: var(--rt-radius-sm);--rt-checkbox-transition-duration: var(--rt-duration-fast);--rt-checkbox-color-label: var(--rt-color-text-primary);display:inline-flex;align-items:center;padding:0;border:none;background:none;cursor:pointer;font:inherit;gap:var(--rt-space-sm);text-align:start}rt-checkbox .rt-checkbox:focus-visible{border-radius:var(--rt-checkbox-focus-radius);box-shadow:var(--rt-checkbox-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-checkbox .rt-checkbox--disabled{cursor:not-allowed;opacity:.5}rt-checkbox .rt-checkbox__box{display:inline-flex;width:18px;height:18px;flex-shrink:0;align-items:center;justify-content:center;border:var(--rt-input-border-width) solid var(--rt-color-border-strong);border-radius:var(--rt-checkbox-box-radius);background:var(--rt-color-bg-surface);transition:background var(--rt-checkbox-transition-duration) ease,border-color var(--rt-checkbox-transition-duration) ease}rt-checkbox .rt-checkbox--checked .rt-checkbox__box,rt-checkbox .rt-checkbox--indeterminate .rt-checkbox__box{border-color:var(--rt-color-action-primary);background:var(--rt-color-action-primary)}rt-checkbox .rt-checkbox__label{color:var(--rt-checkbox-color-label);font-size:var(--rt-text-sm);-webkit-user-select:none;user-select:none}rt-checkbox .rt-checkbox__label:empty{display:none}}\n"] }]
|
|
4605
|
+
}, template: "<button\n rtBlock=\"rt-checkbox\"\n qa-dataid=\"checkbox-control\"\n type=\"button\"\n role=\"checkbox\"\n [rtMod]=\"{\n checked: isChecked() && !indeterminate(),\n indeterminate: indeterminate(),\n disabled: isDisabled(),\n }\"\n [attr.id]=\"inputId()\"\n [attr.aria-checked]=\"indeterminate() ? 'mixed' : isChecked()\"\n [attr.aria-label]=\"ariaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\">\n <span rtElem=\"box\">\n @if (indeterminate()) {\n <rt-icon rtElem=\"icon\" name=\"minus\" size=\"xs\" color=\"inverse\" />\n } @else if (isChecked()) {\n <rt-icon rtElem=\"icon\" name=\"check\" size=\"xs\" color=\"inverse\" />\n }\n </span>\n\n <span rtElem=\"label\" qa-dataid=\"checkbox-label\">\n <ng-content />\n </span>\n</button>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{rt-checkbox{display:inline-block}rt-checkbox .rt-checkbox{--rt-checkbox-focus-radius: var(--rt-radius-sm);--rt-checkbox-focus-shadow: var(--rt-shadow-focus-ring);--rt-checkbox-box-radius: var(--rt-radius-sm);--rt-checkbox-transition-duration: var(--rt-duration-fast);--rt-checkbox-color-label: var(--rt-color-text-primary);display:inline-flex;align-items:center;padding:0;border:none;background:none;cursor:pointer;font:inherit;gap:var(--rt-space-sm);text-align:start;vertical-align:top}rt-checkbox .rt-checkbox:focus-visible{border-radius:var(--rt-checkbox-focus-radius);box-shadow:var(--rt-checkbox-focus-shadow);outline:2px solid transparent;outline-offset:2px}rt-checkbox .rt-checkbox--disabled{cursor:not-allowed;opacity:.5}rt-checkbox .rt-checkbox__box{display:inline-flex;width:18px;height:18px;flex-shrink:0;align-items:center;justify-content:center;border:var(--rt-input-border-width) solid var(--rt-color-border-strong);border-radius:var(--rt-checkbox-box-radius);background:var(--rt-color-bg-surface);transition:background var(--rt-checkbox-transition-duration) ease,border-color var(--rt-checkbox-transition-duration) ease}rt-checkbox .rt-checkbox--checked .rt-checkbox__box,rt-checkbox .rt-checkbox--indeterminate .rt-checkbox__box{border-color:var(--rt-color-action-primary);background:var(--rt-color-action-primary)}rt-checkbox .rt-checkbox__label{color:var(--rt-checkbox-color-label);font-size:var(--rt-text-sm);-webkit-user-select:none;user-select:none}rt-checkbox .rt-checkbox__label:empty{display:none}}\n"] }]
|
|
4538
4606
|
}], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }] } });
|
|
4539
4607
|
|
|
4540
4608
|
const BEM_BLOCK$18 = 'rt-collapsible-text';
|
|
@@ -18029,5 +18097,5 @@ const RT_CONSUMER_HANDLE_NAMES = [
|
|
|
18029
18097
|
* Generated bundle index. Do not edit.
|
|
18030
18098
|
*/
|
|
18031
18099
|
|
|
18032
|
-
export { BreakpointsService, CATEGORY_ORDER, DATA_LIST_PAGE_SIZES, DATA_TABLE_FILTER_MENU_OPERATORS, DATA_TABLE_FILTER_OPERATOR_GLYPHS, ERtCalendarDayState, ERtDataTableCellColor, ERtDataTableColumnType, ERtDataTableFilterType, ERtMarkdownBlock, ERtMarkdownInline, ERtStorageKeys, ICON_CATEGORY_RULES, MessageBus, NotificationBus, RT_ACTION_BAR_LEAVE_MS, RT_ASIDE_DATA, RT_CONSUMER_HANDLE_NAMES, RT_COUNTER_DECREASE_KEY, RT_COUNTER_INCREASE_KEY, RT_DATA_TABLE_PRESET_FROM_HOST, RT_DATA_TABLE_ROW_HOST, RT_DATA_TABLE_STOP_ROW_CLICK_ATTRIBUTE, RT_DESIGN_TOKEN_NAMES, RT_DIALOG_DATA, RT_FIELD_DEFAULT_ERROR_KEYS, RT_FIELD_EMPTY_VALUE, RT_ICONS_BASE_URL, RT_ICONS_MATERIAL_BASE_URL, RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX, RT_ICON_MATERIAL_PRESET_SELECTOR, RT_ICON_MATERIAL_SYMBOL_ID_PREFIX, RT_ICON_SPRITE_ID, RT_ICON_SYMBOL_ID_PREFIX, RT_KIT_CONFIG, RT_KIT_LABELS, RT_KIT_LABELS_EN, RT_KIT_LOCALE, RT_KIT_TRANSLATOR, RT_MENU_SELECT_EVENT, RT_PRESET_MATERIAL_CLASS, RT_TABLE_ROW_ACTIONS_COLUMN, RT_TOASTER_GAP_PX, RtActionBarComponent, RtActionBarHolderComponent, RtActionBarService, RtAsideComponent, RtAsideFooterComponent, RtAsideHeaderComponent, RtAsideRef, RtAsideSectionComponent, RtAsideService, RtAutocompleteComponent, RtBarListComponent, RtBottomSheetComponent, RtButtonDirective, RtCalendarComponent, RtCardComponent, RtCheckboxComponent, RtCollapsibleTextComponent, RtConfirmDirective, RtConfirmPopoverComponent, RtContainerComponent, RtContainerContentDirective, RtContainerContentPanelDirective, RtContainerHeaderDirective, RtContainerLeftSidenavDirective, RtContainerLeftSidenavPanelDirective, RtContainerRightSidenavDirective, RtContainerRightSidenavPanelDirective, RtContainerStateService, RtContainerToolbarCenterDirective, RtContainerToolbarLeftDirective, RtContainerToolbarRightDirective, RtCopyCellComponent, RtCounterComponent, RtCounterRowComponent, RtDataListAdditionalRowActionsDirective, RtDataListComponent, RtDataListCustomCellsDirective, RtDataListRowActionsDirective, RtDataListSelectorsDirective, RtDataListSettingsAsideComponent, RtDataListToolbarActionsDirective, RtDataListToolbarComponent, RtDataListToolbarSelectorsDirective, RtDataTableAdditionalRowActionsDirective, RtDataTableCellComponent, RtDataTableComponent, RtDataTableConfigService, RtDataTableCustomCellsDirective, RtDataTableFilterCellComponent, RtDataTableHeaderCellComponent, RtDataTableIconDirective, RtDataTableRowActionsDirective, RtDataTableRowClickDirective, RtDataTableSelectorsDirective, RtDataTableStopRowClickDirective, RtDatePickerComponent, RtDayStampPipe, RtDeltaViewComponent, RtDetailListComponent, RtDetailRowComponent, RtDialogComponent, RtDialogFooterComponent, RtDialogHeaderComponent, RtDialogRef, RtDialogService, RtDownloadLinkComponent, RtDynamicListComponent, RtDynamicListToolbarActionsDirective, RtDynamicListToolbarSelectorsDirective, RtEmptyStateComponent, RtFieldComponent, RtFieldHintDirective, RtFileCardComponent, RtFileDropComponent, RtFileInputComponent, RtFileListComponent, RtFilterControlComponent, RtFormControlBase, RtHeaderComponent, RtIconButtonComponent, RtIconComponent, RtIconRegistry, RtInfiniteScrollDirective, RtInfoItemComponent, RtInputComponent, RtInputNumberComponent, RtKitLabelPipe, RtLiveBadgeComponent, RtLogoComponent, RtMarkdownTextComponent, RtMenuComponent, RtMenuItemComponent, RtMessageComponent, RtMoneyListComponent, RtMoneyRowComponent, RtMultiselectComponent, RtNightGridComponent, RtNoteComponent, RtNotificationsBellComponent, RtPageHeaderComponent, RtPaginationComponent, RtPhotoViewerComponent, RtPopoverDirective, RtRadioButtonComponent, RtRippleDirective, RtRouteAsideComponent, RtRowHasActionsPipe, RtScrollAreaComponent, RtScrollAreaContentDirective, RtScrollAreaFooterDirective, RtScrollAreaHeaderDirective, RtSectionNavComponent, RtSelectComponent, RtSelectTriggerDirective, RtSkeletonComponent, RtSkeletonWrapperComponent, RtSpinnerComponent, RtSplitButtonComponent, RtStatTileComponent, RtStepperComponent, RtTabDirective, RtTableCardDirective, RtTableComponent, RtTableFilterHeaderComponent, RtTableRowActionsDirective, RtTableRowDirective, RtTableSettingsAsideComponent, RtTableSettingsPanelComponent, RtTableSettingsRegistry, RtTableSortHeaderComponent, RtTabsComponent, RtTabsControlDirective, RtTagComponent, RtTextareaComponent, RtThemeScopeDirective, RtThemeToggleComponent, RtThreadListComponent, RtThreadListFiltersDirective, RtThreadListRowDirective, RtThreadListSearchDirective, RtTimelineComponent, RtToastComponent, RtToasterComponent, RtToggleButtonGroupComponent, RtToggleSwitchComponent, RtToolbarCenterDirective, RtToolbarComponent, RtToolbarLeftDirective, RtToolbarRightDirective, RtTooltipComponent, RtTooltipDirective, RtTruncateMiddlePipe, RtWelcomeDialogComponent, RtWorkspaceAsideDirective, RtWorkspaceCenterDirective, RtWorkspaceComponent, RtWorkspaceDesktopOnlyDirective, RtWorkspaceDetailsComponent, RtWorkspaceListDirective, SEARCH_DEBOUNCE_MS, ThemeService, ariaSortOf, breakNode, categoryOf, coerceQuillDeltaOutput, comparePropNames, dataListColumnsFromItems, dataListExcludedAfterEntity, dataListExcludedAfterPageMarked, dataListExcludedAfterPageUnmarked, dataListMoveItem, dataListPageAfterSizeChange, dataListPageCount, dataListPageSizes, dataListSelectedAfterPage, dataListSettingItems, dataListSettingsChanged, dataListToggleHidden, dataTableAllOnPage, dataTableAnyOnPage, dataTableAriaSort, dataTableCellText, dataTableCellValue, dataTableColumnFilter, dataTableCopyButtonSide, dataTableDayToIso, dataTableDefaultOperator, dataTableFiltersWithOperator, dataTableFiltersWithValue, dataTableIconName, dataTableIsoToDay, dataTableKeysOf, dataTableNextSortOrder, dataTablePresetEntities, dataTableSortActive, dataTableWithEntity, dataTableWithPageEntities, dataTableWithoutPageEntities, defaultColumnItems, deriveFileTitle, displayedColumnKeys, fileNameFromUrl, filterCellsOf, filterOf, filterValueOf, filtersWithOperator, filtersWithValue, formatFileSize, getFileTypeIcon, iconsName, indexFromParam, indexFromScroll, isColumnSortable, nextPhotoIndex, nextSort, normalizeQuillDelta, paramFromIndex, parseInline, parseMarkdown, provideRtIcons, provideRtKit, provideRtKitLabels, resolveColumns, rowHasAvailableActions, rtAsideUnsavedGuard, rtKitDefault, rtKitLabel, searchDebounce, shouldRenderPhoto, sortDirectionOf, startOperatorOf, withoutLockedHidden };
|
|
18100
|
+
export { BreakpointsService, CATEGORY_ORDER, DATA_LIST_PAGE_SIZES, DATA_TABLE_FILTER_MENU_OPERATORS, DATA_TABLE_FILTER_OPERATOR_GLYPHS, ERtCalendarDayState, ERtDataTableCellColor, ERtDataTableColumnType, ERtDataTableFilterType, ERtMarkdownBlock, ERtMarkdownInline, ERtStorageKeys, ICON_CATEGORY_RULES, MessageBus, NotificationBus, RT_ACTION_BAR_LEAVE_MS, RT_ASIDE_DATA, RT_CONSUMER_HANDLE_NAMES, RT_COUNTER_DECREASE_KEY, RT_COUNTER_INCREASE_KEY, RT_DATA_TABLE_PRESET_FROM_HOST, RT_DATA_TABLE_ROW_HOST, RT_DATA_TABLE_STOP_ROW_CLICK_ATTRIBUTE, RT_DESIGN_TOKEN_NAMES, RT_DIALOG_DATA, RT_FIELD_DEFAULT_ERROR_KEYS, RT_FIELD_EMPTY_VALUE, RT_ICONS_BASE_URL, RT_ICONS_MATERIAL_BASE_URL, RT_ICON_MATERIAL_FILL_SYMBOL_ID_PREFIX, RT_ICON_MATERIAL_PRESET_SELECTOR, RT_ICON_MATERIAL_SYMBOL_ID_PREFIX, RT_ICON_SPRITE_ID, RT_ICON_SYMBOL_ID_PREFIX, RT_KIT_CONFIG, RT_KIT_LABELS, RT_KIT_LABELS_EN, RT_KIT_LOCALE, RT_KIT_TRANSLATOR, RT_MENU_SELECT_EVENT, RT_PRESET_MATERIAL_CLASS, RT_TABLE_ROW_ACTIONS_COLUMN, RT_TOASTER_GAP_PX, RtAccordionComponent, RtActionBarComponent, RtActionBarHolderComponent, RtActionBarService, RtAsideComponent, RtAsideFooterComponent, RtAsideHeaderComponent, RtAsideRef, RtAsideSectionComponent, RtAsideService, RtAutocompleteComponent, RtBarListComponent, RtBottomSheetComponent, RtButtonDirective, RtCalendarComponent, RtCardComponent, RtCheckboxComponent, RtCollapsibleTextComponent, RtConfirmDirective, RtConfirmPopoverComponent, RtContainerComponent, RtContainerContentDirective, RtContainerContentPanelDirective, RtContainerHeaderDirective, RtContainerLeftSidenavDirective, RtContainerLeftSidenavPanelDirective, RtContainerRightSidenavDirective, RtContainerRightSidenavPanelDirective, RtContainerStateService, RtContainerToolbarCenterDirective, RtContainerToolbarLeftDirective, RtContainerToolbarRightDirective, RtCopyCellComponent, RtCounterComponent, RtCounterRowComponent, RtDataListAdditionalRowActionsDirective, RtDataListComponent, RtDataListCustomCellsDirective, RtDataListRowActionsDirective, RtDataListSelectorsDirective, RtDataListSettingsAsideComponent, RtDataListToolbarActionsDirective, RtDataListToolbarComponent, RtDataListToolbarSelectorsDirective, RtDataTableAdditionalRowActionsDirective, RtDataTableCellComponent, RtDataTableComponent, RtDataTableConfigService, RtDataTableCustomCellsDirective, RtDataTableFilterCellComponent, RtDataTableHeaderCellComponent, RtDataTableIconDirective, RtDataTableRowActionsDirective, RtDataTableRowClickDirective, RtDataTableSelectorsDirective, RtDataTableStopRowClickDirective, RtDatePickerComponent, RtDayStampPipe, RtDeltaViewComponent, RtDetailListComponent, RtDetailRowComponent, RtDialogComponent, RtDialogFooterComponent, RtDialogHeaderComponent, RtDialogRef, RtDialogService, RtDownloadLinkComponent, RtDynamicListComponent, RtDynamicListToolbarActionsDirective, RtDynamicListToolbarSelectorsDirective, RtEmptyStateComponent, RtFieldComponent, RtFieldHintDirective, RtFileCardComponent, RtFileDropComponent, RtFileInputComponent, RtFileListComponent, RtFilterControlComponent, RtFormControlBase, RtHeaderComponent, RtIconButtonComponent, RtIconComponent, RtIconRegistry, RtInfiniteScrollDirective, RtInfoItemComponent, RtInputComponent, RtInputNumberComponent, RtKitLabelPipe, RtLiveBadgeComponent, RtLogoComponent, RtMarkdownTextComponent, RtMenuComponent, RtMenuItemComponent, RtMessageComponent, RtMoneyListComponent, RtMoneyRowComponent, RtMultiselectComponent, RtNightGridComponent, RtNoteComponent, RtNotificationsBellComponent, RtPageHeaderComponent, RtPaginationComponent, RtPhotoViewerComponent, RtPopoverDirective, RtRadioButtonComponent, RtRippleDirective, RtRouteAsideComponent, RtRowHasActionsPipe, RtScrollAreaComponent, RtScrollAreaContentDirective, RtScrollAreaFooterDirective, RtScrollAreaHeaderDirective, RtSectionNavComponent, RtSelectComponent, RtSelectTriggerDirective, RtSkeletonComponent, RtSkeletonWrapperComponent, RtSpinnerComponent, RtSplitButtonComponent, RtStatTileComponent, RtStepperComponent, RtTabDirective, RtTableCardDirective, RtTableComponent, RtTableFilterHeaderComponent, RtTableRowActionsDirective, RtTableRowDirective, RtTableSettingsAsideComponent, RtTableSettingsPanelComponent, RtTableSettingsRegistry, RtTableSortHeaderComponent, RtTabsComponent, RtTabsControlDirective, RtTagComponent, RtTextareaComponent, RtThemeScopeDirective, RtThemeToggleComponent, RtThreadListComponent, RtThreadListFiltersDirective, RtThreadListRowDirective, RtThreadListSearchDirective, RtTimelineComponent, RtToastComponent, RtToasterComponent, RtToggleButtonGroupComponent, RtToggleSwitchComponent, RtToolbarCenterDirective, RtToolbarComponent, RtToolbarLeftDirective, RtToolbarRightDirective, RtTooltipComponent, RtTooltipDirective, RtTruncateMiddlePipe, RtWelcomeDialogComponent, RtWorkspaceAsideDirective, RtWorkspaceCenterDirective, RtWorkspaceComponent, RtWorkspaceDesktopOnlyDirective, RtWorkspaceDetailsComponent, RtWorkspaceListDirective, SEARCH_DEBOUNCE_MS, ThemeService, ariaSortOf, breakNode, categoryOf, coerceQuillDeltaOutput, comparePropNames, dataListColumnsFromItems, dataListExcludedAfterEntity, dataListExcludedAfterPageMarked, dataListExcludedAfterPageUnmarked, dataListMoveItem, dataListPageAfterSizeChange, dataListPageCount, dataListPageSizes, dataListSelectedAfterPage, dataListSettingItems, dataListSettingsChanged, dataListToggleHidden, dataTableAllOnPage, dataTableAnyOnPage, dataTableAriaSort, dataTableCellText, dataTableCellValue, dataTableColumnFilter, dataTableCopyButtonSide, dataTableDayToIso, dataTableDefaultOperator, dataTableFiltersWithOperator, dataTableFiltersWithValue, dataTableIconName, dataTableIsoToDay, dataTableKeysOf, dataTableNextSortOrder, dataTablePresetEntities, dataTableSortActive, dataTableWithEntity, dataTableWithPageEntities, dataTableWithoutPageEntities, defaultColumnItems, deriveFileTitle, displayedColumnKeys, fileNameFromUrl, filterCellsOf, filterOf, filterValueOf, filtersWithOperator, filtersWithValue, formatFileSize, getFileTypeIcon, iconsName, indexFromParam, indexFromScroll, isColumnSortable, nextPhotoIndex, nextSort, normalizeQuillDelta, paramFromIndex, parseInline, parseMarkdown, provideRtIcons, provideRtKit, provideRtKitLabels, resolveColumns, rowHasAvailableActions, rtAsideUnsavedGuard, rtKitDefault, rtKitLabel, searchDebounce, shouldRenderPhoto, sortDirectionOf, startOperatorOf, withoutLockedHidden };
|
|
18033
18101
|
//# sourceMappingURL=rt-tools-ui-kit-v2.mjs.map
|