@libs-ui/components-spreadsheet 0.2.356-43

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.
@@ -0,0 +1,1350 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, input, output, viewChild, inject, NgZone, effect, untracked, Component, ChangeDetectionStrategy } from '@angular/core';
3
+ import { Tools, BooleanNumber, LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
4
+ import { defaultTheme } from '@univerjs/design';
5
+ import { UniverDocsPlugin } from '@univerjs/docs';
6
+ import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
7
+ import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
8
+ import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
9
+ import { FUniver } from '@univerjs/facade';
10
+ import { InsertColMutation, RemoveColMutation, MoveColsMutation, UniverSheetsPlugin } from '@univerjs/sheets';
11
+ import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
12
+ import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
13
+ import { getPatternPreview, UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
14
+ import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
15
+ import { UniverUIPlugin } from '@univerjs/ui';
16
+ import DesignEnUS from '@univerjs/design/lib/locale/en-US.json';
17
+ import DesignViVN from '@univerjs/design/lib/locale/vi-VN.json';
18
+ import DocsUIEnUS from '@univerjs/docs-ui/lib/locale/en-US.json';
19
+ import DocsUIViVN from '@univerjs/docs-ui/lib/locale/vi-VN.json';
20
+ import SheetsEnUS from '@univerjs/sheets/lib/locale/en-US.json';
21
+ import SheetsViVN from '@univerjs/sheets/lib/locale/vi-VN.json';
22
+ import SheetsFormulaUIEnUS from '@univerjs/sheets-formula-ui/lib/locale/en-US.json';
23
+ import SheetsFormulaUIViVN from '@univerjs/sheets-formula-ui/lib/locale/vi-VN.json';
24
+ import SheetsUIEnUS from '@univerjs/sheets-ui/lib/locale/en-US.json';
25
+ import SheetsUIViVN from '@univerjs/sheets-ui/lib/locale/vi-VN.json';
26
+ import UIEnUS from '@univerjs/ui/lib/locale/en-US.json';
27
+ import UIViVN from '@univerjs/ui/lib/locale/vi-VN.json';
28
+ import { fromEvent } from 'rxjs';
29
+
30
+ // Numfmt locale embedded directly — avoids TS module resolution issues with JSON in package exports
31
+ const sheetsNumfmtLocaleEn = {
32
+ sheet: {
33
+ numfmt: {
34
+ percent: 'Percentage', title: 'Number format', numfmtType: 'Format types',
35
+ cancel: 'Cancel', confirm: 'Confirm', general: 'General',
36
+ accounting: 'Accounting', text: 'Text', number: 'Number',
37
+ currency: 'Currency', date: 'Date', time: 'Time',
38
+ thousandthPercentile: 'Thousands separator', preview: 'Preview',
39
+ dateTime: 'Date and time', decimalLength: 'Decimal places',
40
+ currencyType: 'Currency Symbol', moreFmt: 'Formats',
41
+ financialValue: 'Financial value', roundingCurrency: 'Rounding up the currency',
42
+ timeDuration: 'Duration Time', addDecimal: 'Increase decimal places',
43
+ subtractDecimal: 'Decreasing decimal places', customFormat: 'Custom Format',
44
+ // Additional keys from en-US.json
45
+ negType: 'A negative number type',
46
+ currencyDes: 'The currency format is used to represent general currency values. The accounting format aligns a column of values with decimal points.',
47
+ accountingDes: 'The accounting number format aligns a column of values with currency symbols and decimal points.',
48
+ dateType: 'Date Type',
49
+ dateDes: 'The date format presents date and time series values as date values.',
50
+ generalDes: 'The regular format does not contain any specific number format.',
51
+ thousandthPercentileDes: 'The percentile format is used for the representation of ordinary numbers.',
52
+ customFormatDes: 'Generate custom number formats based on existing formats.',
53
+ },
54
+ },
55
+ };
56
+ // Numfmt locale tiếng Việt — nhúng trực tiếp cùng lý do như bản en ở trên
57
+ const sheetsNumfmtLocaleVi = {
58
+ sheet: {
59
+ numfmt: {
60
+ percent: 'Phần trăm', title: 'Định dạng số', numfmtType: 'Loại định dạng',
61
+ cancel: 'Hủy bỏ', confirm: 'Xác nhận', general: 'Chung',
62
+ accounting: 'Kế toán', text: 'Văn bản', number: 'Số',
63
+ currency: 'Tiền tệ', date: 'Ngày', time: 'Thời gian',
64
+ thousandthPercentile: 'Phân vị phần nghìn', preview: 'Xem trước',
65
+ dateTime: 'Ngày giờ', decimalLength: 'Số chữ số thập phân',
66
+ currencyType: 'Loại tiền tệ', moreFmt: 'Nhiều định dạng hơn',
67
+ financialValue: 'Giá trị tài chính', roundingCurrency: 'Tiền tệ làm tròn',
68
+ timeDuration: 'Thời lượng', addDecimal: 'Thêm chữ số thập phân',
69
+ subtractDecimal: 'Giảm chữ số thập phân', customFormat: 'Định dạng tùy chỉnh',
70
+ negType: 'Loại số âm',
71
+ currencyDes: 'Định dạng tiền tệ được sử dụng để biểu thị các giá trị tiền tệ thông thường. Định dạng kế toán có thể căn chỉnh các giá trị trong một cột với dấu thập phân.',
72
+ accountingDes: 'Định dạng số kế toán có thể căn chỉnh các ký hiệu tiền tệ và dấu thập phân trong một cột các giá trị.',
73
+ dateType: 'Loại ngày',
74
+ dateDes: 'Định dạng ngày biểu thị các giá trị chuỗi ngày và thời gian dưới dạng giá trị ngày.',
75
+ generalDes: 'Định dạng chung không chứa bất kỳ định dạng số cụ thể nào.',
76
+ thousandthPercentileDes: 'Định dạng phân vị phần nghìn được sử dụng để biểu thị các số thông thường.',
77
+ customFormatDes: 'Tạo định dạng số tùy chỉnh dựa trên các định dạng hiện có.',
78
+ },
79
+ },
80
+ };
81
+ const univerLocaleEn = Tools.deepMerge(SheetsEnUS, DocsUIEnUS, SheetsUIEnUS, SheetsFormulaUIEnUS, sheetsNumfmtLocaleEn, UIEnUS, DesignEnUS);
82
+ const univerLocaleVi = Tools.deepMerge(SheetsViVN, DocsUIViVN, SheetsUIViVN, SheetsFormulaUIViVN, sheetsNumfmtLocaleVi, UIViVN, DesignViVN);
83
+
84
+ /**
85
+ * Giá trị mẫu MẶC ĐỊNH để sinh preview = 2021-03-14 13:45:30 (Excel serial).
86
+ * Chọn ngày 14 (>12) để phân biệt dd↔mm, giờ 13 để phân biệt 24h↔AM/PM, có cả giây.
87
+ * CHỈ dùng khi ô đang chọn không có giá trị ngày hợp lệ — nếu có, ưu tiên giá trị THỰC của ô
88
+ * để label option khớp đúng dữ liệu hiển thị trong cột (giống Excel).
89
+ */
90
+ const SAMPLE_SERIAL = 44269 + 49530 / 86400;
91
+ /**
92
+ * Danh sách định dạng ngày/giờ kiểu Excel (ordered: Ngày → Ngày+Giờ → Giờ).
93
+ * Label hiển thị cho người dùng là PREVIEW của SAMPLE (vd 'dd/mm/yyyy' → "14/03/2021"),
94
+ * trực quan giống hộp "Type" của Excel; pattern thực được lưu ở data-pattern để áp.
95
+ */
96
+ const CUSTOM_DATE_PATTERNS = [
97
+ // ── Ngày ──
98
+ 'dd/mm/yyyy',
99
+ 'd/m/yyyy',
100
+ 'dd/mm/yy',
101
+ 'mm/dd/yyyy',
102
+ 'yyyy-mm-dd',
103
+ 'dd-mmm-yyyy',
104
+ 'dd-mmm-yy',
105
+ 'd mmmm yyyy',
106
+ 'mmm yyyy',
107
+ 'mmmm yyyy',
108
+ 'dddd, d mmmm yyyy',
109
+ // ── Ngày + giờ ──
110
+ 'dd/mm/yyyy hh:mm',
111
+ 'dd/mm/yyyy h:mm AM/PM',
112
+ 'dd/mm/yyyy hh:mm:ss',
113
+ 'yyyy-mm-dd hh:mm:ss',
114
+ // ── Giờ ──
115
+ 'hh:mm',
116
+ 'hh:mm:ss',
117
+ 'h:mm AM/PM',
118
+ 'h:mm:ss AM/PM',
119
+ 'mm:ss',
120
+ '[h]:mm:ss',
121
+ ];
122
+ /** Sinh chuỗi preview (giá trị mẫu đã format) cho 1 pattern; fallback về pattern nếu lỗi. */
123
+ const previewOf = (pattern, sample) => {
124
+ try {
125
+ return getPatternPreview(pattern, sample).result || pattern;
126
+ }
127
+ catch {
128
+ return pattern;
129
+ }
130
+ };
131
+ /**
132
+ * Re-assert highlight cho option đang chọn sau khi Univer re-render panel (React có thể xoá class).
133
+ * Dựa trên cờ data-numfmt-selected (do setActive đặt) — không suy diễn từ DOM label.
134
+ */
135
+ const syncActiveNumfmtHighlight = () => {
136
+ const panel = document.querySelector('.numfmt-panel');
137
+ if (!panel)
138
+ return;
139
+ panel.querySelectorAll('.univer-numfmt-custom-item').forEach((item) => {
140
+ item.classList.toggle('univer-numfmt-active', item.hasAttribute('data-numfmt-selected'));
141
+ });
142
+ };
143
+ /**
144
+ * Thay danh sách định dạng ngày mặc định của Univer bằng các option custom (CUSTOM_DATE_PATTERNS),
145
+ * hiển thị dưới dạng PREVIEW trực quan. Univer render panel numfmt bằng React và không expose API
146
+ * nên phải thao tác DOM trực tiếp.
147
+ *
148
+ * @param getSnapshot Lấy vùng chọn hiện tại (để áp numfmt).
149
+ * @param executeCmd Chạy Univer command.
150
+ * @param onPatternSelected Báo component pattern vừa chọn (Confirm) hoặc null (Cancel) để đồng bộ.
151
+ * @param getActiveCellFormat Đọc pattern hiện tại của cell — dùng highlight option khớp khi mở.
152
+ * @param clearPreview Xoá live-preview overlay của Univer để canvas vẽ đúng model ngay.
153
+ * @param getSampleValue Lấy giá trị THỰC (serial) của ô đại diện để label preview khớp dữ liệu cột;
154
+ * null → dùng SAMPLE_SERIAL mặc định.
155
+ * @returns hàm cleanup (gỡ mọi listener) — gọi khi teardown hoặc trước lần inject mới.
156
+ */
157
+ const injectCustomDateFormats = (getSnapshot, executeCmd, onPatternSelected, getActiveCellFormat, clearPreview, getSampleValue, onConfirm) => {
158
+ const panel = document.querySelector('.numfmt-panel');
159
+ if (!panel)
160
+ return;
161
+ // Guard: chỉ inject 1 lần / phiên (component xoá data-custom-dates khi đóng panel để reopen re-inject).
162
+ const listContainer = panel.querySelector('ul.univer-select-list');
163
+ if (!listContainer || listContainer.hasAttribute('data-custom-dates'))
164
+ return;
165
+ // Nhận diện panel Date theo 2 trường hợp:
166
+ // 1. Univer vừa render list ngày GỐC (li dạng yyyy-mm-dd) — lần đầu chọn Type=Date.
167
+ // 2. Univer TÁI DÙNG DOM panel cũ (reopen) → list đang chứa sẵn custom item của ta từ phiên trước.
168
+ const hasUniverDateItems = Array.from(listContainer.querySelectorAll('li')).some((el) => /^\d{4}[-/]\d{2}[-/]\d{2}$/.test(el.textContent?.trim() ?? ''));
169
+ const hasOurItems = !!listContainer.querySelector('.univer-numfmt-custom-item');
170
+ if (!hasUniverDateItems && !hasOurItems)
171
+ return;
172
+ listContainer.setAttribute('data-custom-dates', 'true');
173
+ const itemTemplate = listContainer.querySelector('li');
174
+ const defaultItems = Array.from(listContainer.children);
175
+ const applyFormat = (pattern) => {
176
+ const snap = getSnapshot();
177
+ if (!snap)
178
+ return;
179
+ const values = [];
180
+ const maxRow = Math.min(snap.endRow, snap.startRow + 499);
181
+ for (let r = snap.startRow; r <= maxRow; r++) {
182
+ for (let c = snap.startColumn; c <= snap.endColumn; c++) {
183
+ values.push({ pattern, row: r, col: c });
184
+ }
185
+ }
186
+ if (!values.length)
187
+ return;
188
+ executeCmd('sheet.command.numfmt.set.numfmt', { unitId: snap.unitId, subUnitId: snap.subUnitId, values });
189
+ // Univer giữ live-preview overlay (NumfmtController._previewPattern) che hiển thị model của
190
+ // vùng chọn khi panel mở. Ta đã ghi model trực tiếp nên phải xoá preview để canvas vẽ đúng ngay.
191
+ clearPreview?.();
192
+ };
193
+ let pendingPattern = null;
194
+ let activeItem = null;
195
+ const setActive = (item) => {
196
+ if (activeItem) {
197
+ activeItem.classList.remove('univer-numfmt-active');
198
+ activeItem.removeAttribute('data-numfmt-selected');
199
+ }
200
+ item.classList.add('univer-numfmt-active');
201
+ item.setAttribute('data-numfmt-selected', 'true');
202
+ activeItem = item;
203
+ };
204
+ const subs = [];
205
+ const cleanup = () => {
206
+ subs.forEach((s) => s.unsubscribe());
207
+ subs.length = 0;
208
+ };
209
+ // Nhận diện nút theo CLASS (không phụ thuộc ngôn ngữ): Univer render Confirm = button-primary,
210
+ // Cancel = button-default. KHÔNG match theo text ('Confirm'/'Cancel') vì sẽ vỡ khi locale là tiếng Việt.
211
+ const panelButtons = Array.from(panel.querySelectorAll('button'));
212
+ const confirmBtn = panelButtons.find((b) => b.classList.contains('univer-button-primary'));
213
+ const cancelBtn = panelButtons.find((b) => b.classList.contains('univer-button-default'));
214
+ // Capture phase: chạy trước Univer handler. KHÔNG cleanup() trong handler vì Univer tái dùng DOM panel —
215
+ // gỡ listener sẽ làm option không click được khi mở lại. cleanup chỉ chạy khi teardown thật.
216
+ if (confirmBtn) {
217
+ subs.push(fromEvent(confirmBtn, 'click', { capture: true }).subscribe(() => {
218
+ // Đánh dấu Confirm → component GIỮ format. Cancel/X/click-ngoài KHÔNG gọi → mặc định revert.
219
+ onConfirm?.();
220
+ if (!pendingPattern)
221
+ return;
222
+ applyFormat(pendingPattern);
223
+ onPatternSelected?.(pendingPattern);
224
+ }));
225
+ }
226
+ if (cancelBtn) {
227
+ subs.push(fromEvent(cancelBtn, 'click', { capture: true }).subscribe(() => {
228
+ if (activeItem) {
229
+ activeItem.classList.remove('univer-numfmt-active');
230
+ activeItem.removeAttribute('data-numfmt-selected');
231
+ activeItem = null;
232
+ }
233
+ // pendingPattern = null → component khôi phục numfmt gốc từng ô (xem close.numfmt.panel handler).
234
+ pendingPattern = null;
235
+ onPatternSelected?.(null);
236
+ }));
237
+ }
238
+ for (const pattern of CUSTOM_DATE_PATTERNS) {
239
+ const item = (itemTemplate ?? document.createElement('div')).cloneNode(false);
240
+ // Khi reopen, itemTemplate là custom item cũ → xoá cờ highlight/selected sao chép theo để tránh stale.
241
+ item.removeAttribute('data-numfmt-selected');
242
+ item.classList.remove('univer-numfmt-active');
243
+ item.textContent = previewOf(pattern, getSampleValue?.() ?? SAMPLE_SERIAL);
244
+ item.dataset['pattern'] = pattern;
245
+ item.style.cursor = 'pointer';
246
+ item.classList.add('univer-numfmt-custom-item');
247
+ subs.push(fromEvent(item, 'mousedown').subscribe((e) => e.preventDefault()));
248
+ subs.push(fromEvent(item, 'click').subscribe((e) => {
249
+ e.stopPropagation();
250
+ setActive(item);
251
+ pendingPattern = pattern;
252
+ applyFormat(pattern);
253
+ onPatternSelected?.(pattern);
254
+ }));
255
+ listContainer.appendChild(item);
256
+ }
257
+ defaultItems.forEach((el) => el.remove());
258
+ /**
259
+ * Đồng bộ panel theo vùng chọn HIỆN TẠI (getSnapshot/getActiveCellFormat/getSampleValue đọc fresh):
260
+ * 1. Sinh lại label preview theo giá trị THỰC của ô đại diện cột mới (khớp dữ liệu, giống Excel).
261
+ * 2. Highlight option khớp format hiện có; nếu cột chưa có format → chọn option ĐẦU TIÊN.
262
+ * 3. ÁP LUÔN pattern đó lên cả vùng → dữ liệu đổi hiển thị ngay; set pending để Confirm giữ.
263
+ * Gọi khi mở panel VÀ mỗi khi user đổi cột/cell trên grid lúc panel đang mở.
264
+ * Cancel / X → component khôi phục giá trị gốc (originalCellFormats đã chụp/tích luỹ khi mở/đổi vùng).
265
+ */
266
+ const resync = () => {
267
+ const sample = getSampleValue?.() ?? SAMPLE_SERIAL;
268
+ const items = Array.from(listContainer.querySelectorAll('.univer-numfmt-custom-item'));
269
+ items.forEach((el) => { el.textContent = previewOf(el.dataset['pattern'] ?? '', sample); });
270
+ const activeFmt = (getActiveCellFormat?.() ?? '').toLowerCase();
271
+ const matched = activeFmt && activeFmt !== 'general'
272
+ ? items.find((el) => (el.dataset['pattern'] ?? '').toLowerCase() === activeFmt)
273
+ : undefined;
274
+ const initial = matched ?? items[0];
275
+ if (!initial)
276
+ return;
277
+ setActive(initial);
278
+ pendingPattern = initial.dataset['pattern'] ?? null;
279
+ if (!pendingPattern)
280
+ return;
281
+ applyFormat(pendingPattern);
282
+ onPatternSelected?.(pendingPattern);
283
+ };
284
+ resync();
285
+ // Net chống race: nếu Univer set lại preview overlay NGAY SAU khi panel render (sau applyFormat ở trên),
286
+ // xoá lần nữa sau 1 nhịp để canvas hiển thị numfmt THẬT của model. (applyFormat đã clearPreview đồng bộ.)
287
+ setTimeout(() => clearPreview?.(), 60);
288
+ return { cleanup, resync };
289
+ };
290
+
291
+ /** Style mặc định cho cột đã map: nền xanh, chữ trắng, bold, canh giữa. */
292
+ const DEFAULT_MAPPED_STYLE = {
293
+ bg: { rgb: '#389E0D' },
294
+ cl: { rgb: '#FFFFFF' },
295
+ bl: BooleanNumber.TRUE,
296
+ ht: 2,
297
+ vt: 2,
298
+ fs: 11,
299
+ };
300
+ /**
301
+ * Config mặc định của lib — consumer có thể dùng trực tiếp hoặc spread-override:
302
+ * `{ ...defaultMappingIndicatorConfig(), prefix: '★ ' }`.
303
+ */
304
+ const defaultMappingIndicatorConfig = () => ({
305
+ mappedStyleId: 'm',
306
+ mappedStyle: { ...DEFAULT_MAPPED_STYLE },
307
+ defaultHeaderStyleId: 'h',
308
+ prefix: '● ',
309
+ headerRowIndex: 0,
310
+ autoResize: true,
311
+ charWidth: 7.5,
312
+ padding: 16,
313
+ });
314
+ const DEFAULT_PREFIX = '● ';
315
+ const DEFAULT_CHAR_W = 7.5;
316
+ const DEFAULT_PADDING = 16;
317
+ const DEFAULT_COL_W = 80;
318
+ const DEFAULT_MAPPED_STYLE_ID = 'm';
319
+ const DEFAULT_HEADER_STYLE_ID = 'h';
320
+ /** Workbook snapshot là plain JSON — clone an toàn nhất bằng JSON. */
321
+ const cloneSnapshot = (snapshot) => JSON.parse(JSON.stringify(snapshot));
322
+ const resolveOpts = (config) => ({
323
+ mappedStyleId: config.mappedStyleId ?? DEFAULT_MAPPED_STYLE_ID,
324
+ mappedStyle: config.mappedStyle ?? DEFAULT_MAPPED_STYLE,
325
+ defaultHeaderStyleId: config.defaultHeaderStyleId ?? DEFAULT_HEADER_STYLE_ID,
326
+ prefix: config.prefix ?? DEFAULT_PREFIX,
327
+ headerRowKey: String(config.headerRowIndex ?? 0),
328
+ autoResize: config.autoResize ?? true,
329
+ charWidth: config.charWidth ?? DEFAULT_CHAR_W,
330
+ padding: config.padding ?? DEFAULT_PADDING,
331
+ });
332
+ const estimateWidth = (text, opts) => Math.ceil(text.length * opts.charWidth + opts.padding);
333
+ /** Mở rộng width cột cho vừa text (không thu nhỏ hơn width hiện tại). */
334
+ const expandColWidth = (sheet, colKey, text, opts) => {
335
+ const colData = sheet.columnData;
336
+ if (!colData)
337
+ return;
338
+ const current = colData[colKey]?.w ?? DEFAULT_COL_W;
339
+ colData[colKey] = { ...(colData[colKey] ?? {}), w: Math.max(current, estimateWidth(text, opts)) };
340
+ };
341
+ /**
342
+ * Dọn header row của 1 sheet: revert style về mặc định, xóa prefix, khôi phục width.
343
+ * @param onlyMapped true → chỉ revert ô đang là `mappedStyleId`; false → revert mọi ô có style.
344
+ */
345
+ const resetHeaderRow = (sheet, opts, onlyMapped) => {
346
+ const headerRow = sheet?.cellData?.[opts.headerRowKey];
347
+ if (!headerRow || !sheet)
348
+ return;
349
+ for (const colKey of Object.keys(headerRow)) {
350
+ const cell = headerRow[colKey];
351
+ if (!cell)
352
+ continue;
353
+ if (onlyMapped) {
354
+ if (cell.s === opts.mappedStyleId)
355
+ cell.s = opts.defaultHeaderStyleId;
356
+ }
357
+ else if (cell.s) {
358
+ cell.s = opts.defaultHeaderStyleId;
359
+ }
360
+ if (typeof cell.v === 'string' && cell.v.startsWith(opts.prefix)) {
361
+ const original = cell.v.slice(opts.prefix.length);
362
+ cell.v = original;
363
+ if (opts.autoResize && sheet.columnData?.[colKey]) {
364
+ sheet.columnData[colKey].w = estimateWidth(original, opts);
365
+ }
366
+ }
367
+ }
368
+ };
369
+ // ─────────────────────────────────────────────────────────────────────────────
370
+ // Public API
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+ /**
373
+ * Đánh dấu header các cột đã map: gắn style `mappedStyleId`, thêm prefix icon và
374
+ * (tùy chọn) tự nới width. Tự reset các sheet trong scope trước để mapping cũ không sót lại.
375
+ *
376
+ * @param snapshot Workbook snapshot (plain JSON). Không bị mutate — trả về bản clone.
377
+ * @param targets Danh sách cột cần đánh dấu (`{ sheetId, colIndex }`).
378
+ * @param config Cấu hình style / prefix / scope. Bỏ trống = dùng default của lib.
379
+ */
380
+ const applyMappingIndicators = (snapshot, targets, config = {}) => {
381
+ const data = cloneSnapshot(snapshot);
382
+ const opts = resolveOpts(config);
383
+ const styles = data['styles'];
384
+ if (styles && !styles[opts.mappedStyleId]) {
385
+ styles[opts.mappedStyleId] = { ...opts.mappedStyle };
386
+ }
387
+ const sheets = data['sheets'];
388
+ if (!sheets)
389
+ return data;
390
+ const scope = config.scopeSheetIds ?? Object.keys(sheets);
391
+ for (const sheetId of scope) {
392
+ resetHeaderRow(sheets[sheetId], opts, false);
393
+ }
394
+ for (const target of targets) {
395
+ const sheet = sheets[target.sheetId];
396
+ const cell = sheet?.cellData?.[opts.headerRowKey]?.[String(target.colIndex)];
397
+ if (!cell || !sheet)
398
+ continue;
399
+ cell.s = opts.mappedStyleId;
400
+ if (typeof cell.v === 'string' && !cell.v.startsWith(opts.prefix)) {
401
+ const withIcon = `${opts.prefix}${cell.v}`;
402
+ cell.v = withIcon;
403
+ if (opts.autoResize)
404
+ expandColWidth(sheet, String(target.colIndex), withIcon, opts);
405
+ }
406
+ }
407
+ return data;
408
+ };
409
+ /**
410
+ * Gỡ toàn bộ indicator ('m' → default style, xóa prefix, khôi phục width) khỏi snapshot
411
+ * — dùng trước khi gửi workbook lên server để data sạch, không chứa style UI nội bộ.
412
+ *
413
+ * @param snapshot Workbook snapshot (plain JSON). Không bị mutate — trả về bản clone.
414
+ * @param config Dùng chung config với `applyMappingIndicators` để gỡ đúng style/prefix.
415
+ * Bỏ trống = dùng default của lib.
416
+ */
417
+ const stripMappingIndicators = (snapshot, config = {}) => {
418
+ const data = cloneSnapshot(snapshot);
419
+ const opts = resolveOpts(config);
420
+ const sheets = data['sheets'];
421
+ if (!sheets)
422
+ return data;
423
+ const scope = config.scopeSheetIds ?? Object.keys(sheets);
424
+ for (const sheetId of scope) {
425
+ resetHeaderRow(sheets[sheetId], opts, true);
426
+ }
427
+ return data;
428
+ };
429
+ /** Chuyển colIndex (0-based) → chữ cái cột kiểu Excel: 0→'A', 25→'Z', 26→'AA'… */
430
+ const toColumnLetter = (index) => {
431
+ let letter = '';
432
+ let n = index;
433
+ do {
434
+ letter = String.fromCharCode(65 + (n % 26)) + letter;
435
+ n = Math.floor(n / 26) - 1;
436
+ } while (n >= 0);
437
+ return letter;
438
+ };
439
+ /** Tính index mới của 1 cột sau thao tác; trả null nếu cột đó bị xóa. */
440
+ const shiftColumnIndex = (col, change) => {
441
+ const { type, startColumn: start, count } = change;
442
+ if (type === 'insert')
443
+ return col >= start ? col + count : col;
444
+ if (type === 'remove') {
445
+ if (col >= start && col < start + count)
446
+ return null; // cột nằm trong vùng bị xóa
447
+ return col >= start + count ? col - count : col;
448
+ }
449
+ // move: khối [start, start+count) chèn TRƯỚC toColumn (toạ độ gốc) = rút khối rồi chèn lại.
450
+ const end = start + count;
451
+ const dest = change.toColumn ?? start;
452
+ const newStart = dest <= start ? dest : dest - count;
453
+ if (col >= start && col < end)
454
+ return newStart + (col - start); // cột thuộc khối di chuyển
455
+ let shifted = col >= end ? col - count : col; // bước rút khối
456
+ if (shifted >= newStart)
457
+ shifted += count; // bước chèn lại
458
+ return shifted;
459
+ };
460
+ /**
461
+ * Remap colIndex của danh sách mapping theo 1 `T_columnStructureChange`.
462
+ * - Item khác `sheetId` giữ nguyên.
463
+ * - Item nằm trong vùng cột bị XÓA → loại khỏi kết quả.
464
+ * - Các item còn lại được dịch index cho khớp vị trí mới.
465
+ *
466
+ * Tự cập nhật cả `colLetter` (nếu item có field này) cho khớp `colIndex` mới — consumer
467
+ * không cần map lại thủ công.
468
+ *
469
+ * @example
470
+ * this.mappings.update((list) => remapColumnIndexes(list, change));
471
+ */
472
+ const remapColumnIndexes = (items, change) => {
473
+ const result = [];
474
+ for (const item of items) {
475
+ if (item.sheetId !== change.sheetId) {
476
+ result.push(item);
477
+ continue;
478
+ }
479
+ const next = shiftColumnIndex(item.colIndex, change);
480
+ if (next === null)
481
+ continue;
482
+ if (next === item.colIndex) {
483
+ result.push(item);
484
+ continue;
485
+ }
486
+ const updated = { ...item, colIndex: next };
487
+ if (typeof item.colLetter === 'string')
488
+ updated.colLetter = toColumnLetter(next);
489
+ result.push(updated);
490
+ }
491
+ return result;
492
+ };
493
+
494
+ // Bộ đếm tạo class scope duy nhất cho mỗi instance spreadsheet (dùng cho <style> chỉnh width toolbar).
495
+ let spreadsheetInstanceCount = 0;
496
+ class LibsUiComponentsSpreadsheetComponent {
497
+ univer = null;
498
+ univerAPI = null;
499
+ columnClickDisposable = null;
500
+ formatDisposable = null;
501
+ columnOpDisposable = null;
502
+ numfmtObserver = null;
503
+ toolbarResizeObserver = null;
504
+ // <style> riêng cho instance: pin width container đo qua CSS rule (React re-render KHÔNG strip được →
505
+ // tránh vòng lặp oscillation giữa set-inline-width và React strip gây kẹt ⋮). Xem registerToolbarWidthSync.
506
+ toolbarStyleEl = null;
507
+ toolbarScopeClass = `libs-ui-spreadsheet-tb-${(spreadsheetInstanceCount += 1)}`;
508
+ numfmtClickDisposable = null;
509
+ lastRangeSnapshot = null;
510
+ pendingNumfmtPattern = null;
511
+ // true CHỈ khi user bấm nút Confirm trong panel numfmt → giữ format; Cancel/X/click-ngoài = false → revert.
512
+ numfmtConfirmed = false;
513
+ // Snapshot numfmt từng ô của vùng chọn tại thời điểm mở panel — dùng khôi phục chính xác khi Cancel.
514
+ originalCellFormats = null;
515
+ numfmtPanelOpen = false;
516
+ // true khi luồng Date custom đang hoạt động (đã inject option ngày). CHỈ khi đó component mới
517
+ // can thiệp confirm/revert; các Type khác (Số, Tiền tệ, %, Text…) do Univer tự xử lý → không đụng vào.
518
+ numfmtDateActive = false;
519
+ // Đồng bộ lại panel Date (label + highlight + auto-apply) theo vùng chọn mới khi user đổi cột lúc panel mở.
520
+ numfmtResync = null;
521
+ listenEnabled = false;
522
+ listenEnableTimer = null;
523
+ // Defer initial empty-workbook init to allow parent's ngAfterViewInit to call loadData
524
+ // first (cancelling the empty init), avoiding a race with Univer's async React renders.
525
+ pendingInitTimer = null;
526
+ numfmtCleanup = null;
527
+ pendingReadyCb = null;
528
+ // Snapshot đã-decorate gần nhất — làm baseline để diff (style id ổn định 'h'/'m', không bị
529
+ // ảnh hưởng bởi style id tự sinh sau mỗi facade setValue). Dùng cho re-apply incremental.
530
+ lastDecorated = null;
531
+ readyObserver = null;
532
+ // Cache controller numfmt nội bộ của Univer (giữ _previewPattern). Tìm 1 lần qua injector.
533
+ // CONVENTION-EXCEPT: any — NumfmtController không export khỏi @univerjs/sheets-numfmt
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ numfmtController = null;
536
+ activeCellFormat = signal('General');
537
+ activeCellRef = signal('');
538
+ showToolbar = signal(true);
539
+ showSheetTabs = signal(true);
540
+ // Ngôn ngữ đang hiển thị thực tế. Khởi tạo từ input `locale` trong ngAfterViewInit,
541
+ // sau đó đổi runtime qua setLocale() (kéo theo reinit để Univer render lại UI).
542
+ activeLocale = signal('vi');
543
+ /** Ngôn ngữ khởi tạo của spreadsheet. Đổi runtime dùng FunctionControl.setLocale(). */
544
+ locale = input('vi');
545
+ /** Cấu hình ẩn / vô hiệu hoá toolbar items. Thay đổi sẽ reinit toàn bộ Univer. */
546
+ toolbarMenuConfig = input({
547
+ // 'sheet.command.numfmt.set.currency': { hidden: true },
548
+ // 'sheet.command.numfmt.add.decimal.command': { hidden: true },
549
+ // 'sheet.command.numfmt.subtract.decimal.command': { hidden: true },
550
+ // 'sheet.command.numfmt.set.percent': { hidden: true },
551
+ 'sheet.command.set-once-format-painter': { hidden: true },
552
+ 'sheet.command.set-text-rotation': { hidden: true },
553
+ 'sheet.command.set-range-font-family': { hidden: true },
554
+ 'sheet.command.set-border-basic': { hidden: true },
555
+ 'sheet.command.add-worksheet-merge': { hidden: true },
556
+ 'sheet.command.add-worksheet-merge-all': { hidden: true },
557
+ 'sheet.command.add-worksheet-merge-vertical': { hidden: true },
558
+ 'sheet.command.add-worksheet-merge-horizontal': { hidden: true },
559
+ 'sheet.command.remove-worksheet-merge': { hidden: true },
560
+ });
561
+ /**
562
+ * Cấu hình highlight cột đã map (style / prefix / scope sheet).
563
+ * Truyền config này = "bật" luồng auto: component sẽ TỰ áp indicator khi load/reset,
564
+ * TỰ gỡ khi save, và TỰ vẽ lại mỗi khi `mappingTargets` đổi. Bỏ trống = spreadsheet thường.
565
+ */
566
+ mappingIndicatorConfig = input();
567
+ /** Danh sách cột cần highlight (`{ sheetId, colIndex }`). Đổi runtime → tự vẽ lại header. */
568
+ mappingTargets = input([]);
569
+ outColumnDblClick = output();
570
+ /**
571
+ * Phát khi user insert / remove / move cột (kể cả chọn nhiều cột, và cả undo/redo).
572
+ * Consumer dùng `remapColumnIndexes(mappings, $event)` để dịch lại mảng mapping bên ngoài
573
+ * cho khớp vị trí mới — tránh lệch khi chèn/xóa cột ở giữa.
574
+ */
575
+ outColumnStructureChange = output();
576
+ containerRef = viewChild('containerRef');
577
+ ngZone = inject(NgZone);
578
+ constructor() {
579
+ // Auto re-apply: chỉ chạy khi đã bật config + Univer đã khởi tạo. Lần đầu (chưa init) tự bỏ qua.
580
+ effect(() => {
581
+ this.mappingTargets();
582
+ this.mappingIndicatorConfig();
583
+ untracked(() => this.reapplyIndicators());
584
+ });
585
+ }
586
+ ngAfterViewInit() {
587
+ this.activeLocale.set(this.locale());
588
+ this.pendingInitTimer = setTimeout(() => {
589
+ this.pendingInitTimer = null;
590
+ this.initUniver(this.buildEmptyWorkbook());
591
+ });
592
+ }
593
+ get FunctionControl() {
594
+ return {
595
+ loadData: (data) => this.loadData(data),
596
+ saveData: () => this.saveData(),
597
+ getActiveSheetInfo: () => this.getActiveSheetInfo(),
598
+ getColumnNumfmt: (sheetId, colIndex) => this.getColumnNumfmt(sheetId, colIndex),
599
+ extractAllColumnFormats: (snapshot) => this.extractAllColumnFormats(snapshot),
600
+ toggleToolbar: () => this.toggleToolbar(),
601
+ toggleSheetTabs: () => this.toggleSheetTabs(),
602
+ reset: (data) => this.reset(data),
603
+ executeCommand: (commandId, params) => this.executeCommand(commandId, params),
604
+ applyNumfmt: (row, col, numRows, numCols, pattern) => this.applyNumfmt(row, col, numRows, numCols, pattern),
605
+ scheduleOnReady: (cb) => {
606
+ this.pendingReadyCb = cb;
607
+ },
608
+ setLocale: (locale) => this.setLocale(locale),
609
+ activeCellRef: this.activeCellRef,
610
+ activeCellFormat: this.activeCellFormat,
611
+ showToolbar: this.showToolbar,
612
+ showSheetTabs: this.showSheetTabs,
613
+ locale: this.activeLocale.asReadonly(),
614
+ };
615
+ }
616
+ setLocale(locale) {
617
+ if (locale === this.activeLocale())
618
+ return;
619
+ this.activeLocale.set(locale);
620
+ this.reinitUniver();
621
+ }
622
+ loadData(data) {
623
+ const decorated = this.decorateSnapshot(data);
624
+ if (this.pendingInitTimer !== null) {
625
+ clearTimeout(this.pendingInitTimer);
626
+ this.pendingInitTimer = null;
627
+ this.initUniver(decorated);
628
+ return;
629
+ }
630
+ this.destroyUniver();
631
+ setTimeout(() => this.initUniver(decorated));
632
+ }
633
+ /** Snapshot thô từ Univer (CÓ thể còn indicator). Dùng nội bộ cho re-apply. */
634
+ saveRaw() {
635
+ return (this.univerAPI?.getActiveWorkbook()?.save() ?? {});
636
+ }
637
+ /** Snapshot trả cho consumer — tự gỡ indicator nếu luồng auto đang bật. */
638
+ saveData() {
639
+ const config = this.mappingIndicatorConfig();
640
+ if (!config)
641
+ return this.saveRaw();
642
+ return stripMappingIndicators(this.saveRaw(), config);
643
+ }
644
+ /** Áp indicator vào snapshot trước khi load — chỉ khi luồng auto đang bật. Lưu baseline để diff. */
645
+ decorateSnapshot(data) {
646
+ const config = this.mappingIndicatorConfig();
647
+ if (!config)
648
+ return data;
649
+ const decorated = applyMappingIndicators(data, this.mappingTargets(), config);
650
+ this.lastDecorated = decorated;
651
+ return decorated;
652
+ }
653
+ /**
654
+ * Vẽ lại indicator KHÔNG reinit Univer: tính snapshot đích rồi CHỈ ghi đè đúng các ô header
655
+ * khác so với baseline trước đó (style + prefix + width) lên instance live qua facade.
656
+ * Diff theo baseline (style id ổn định 'h'/'m') → chỉ các cột vừa map/bỏ map mới được vẽ lại.
657
+ */
658
+ reapplyIndicators() {
659
+ const config = this.mappingIndicatorConfig();
660
+ const wb = this.univerAPI?.getActiveWorkbook();
661
+ const prev = this.lastDecorated;
662
+ if (!config || !wb || !prev)
663
+ return;
664
+ const next = applyMappingIndicators(this.saveRaw(), this.mappingTargets(), config);
665
+ this.lastDecorated = next;
666
+ const headerKey = String(config.headerRowIndex ?? 0);
667
+ const headerRow = Number(headerKey);
668
+ const stylesNext = (next['styles'] ?? {});
669
+ const sheetsNext = (next['sheets'] ?? {});
670
+ const sheetsPrev = (prev['sheets'] ?? {});
671
+ for (const sheetId of Object.keys(sheetsNext)) {
672
+ const ws = wb.getSheetBySheetId(sheetId);
673
+ if (!ws)
674
+ continue;
675
+ const nextRow = sheetsNext[sheetId]?.cellData?.[headerKey] ?? {};
676
+ const prevRow = sheetsPrev[sheetId]?.cellData?.[headerKey] ?? {};
677
+ const colData = sheetsNext[sheetId]?.columnData ?? {};
678
+ for (const colKey of Object.keys(nextRow)) {
679
+ const cellNext = nextRow[colKey];
680
+ const cellPrev = prevRow[colKey];
681
+ // Ô không đổi cả value lẫn style → bỏ qua (mấu chốt: chỉ vẽ lại cột vừa map/bỏ map).
682
+ if (!cellNext || (cellNext.v === cellPrev?.v && cellNext.s === cellPrev?.s))
683
+ continue;
684
+ const style = (typeof cellNext.s === 'string' ? stylesNext[cellNext.s] : cellNext.s);
685
+ const cell = { v: cellNext.v, s: style };
686
+ void ws.getRange(headerRow, Number(colKey)).setValue(cell);
687
+ const width = colData[colKey]?.w;
688
+ if (typeof width === 'number')
689
+ void ws.setColumnWidth(Number(colKey), width);
690
+ }
691
+ }
692
+ }
693
+ toggleToolbar() {
694
+ this.showToolbar.update((v) => !v);
695
+ this.reinitUniver();
696
+ }
697
+ toggleSheetTabs() {
698
+ this.showSheetTabs.update((v) => !v);
699
+ this.reinitUniver();
700
+ }
701
+ reset(data) {
702
+ this.destroyUniver();
703
+ this.initUniver(this.decorateSnapshot(data ?? this.buildEmptyWorkbook()));
704
+ }
705
+ executeCommand(commandId, params) {
706
+ // CONVENTION-EXCEPT: any — FUniver.executeCommand không expose typing
707
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
708
+ void this.univerAPI?.executeCommand?.(commandId, params);
709
+ }
710
+ applyNumfmt(row, col, numRows, numCols, pattern) {
711
+ // CONVENTION-EXCEPT: any — FWorkbook.getId / FWorksheet.getSheetId not exposed in public types
712
+ // Note: FRange.setNumberFormat() has a Univer facade bug — it builds `values` but never passes
713
+ // them to executeCommand. We call the command directly with the correct params instead.
714
+ // FWorkbook uses getId() (not getUnitId()) and FWorksheet uses getSheetId() for subUnitId.
715
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
+ const wb = this.univerAPI?.getActiveWorkbook();
717
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
718
+ const ws = wb?.getActiveSheet?.();
719
+ if (!wb || !ws)
720
+ return;
721
+ const unitId = (wb.getId?.() ?? '');
722
+ const subUnitId = (ws.getSheetId?.() ?? '');
723
+ const values = [];
724
+ for (let r = row; r < row + numRows; r++) {
725
+ for (let c = col; c < col + numCols; c++) {
726
+ values.push({ pattern, row: r, col: c });
727
+ }
728
+ }
729
+ this.executeCommand('sheet.command.numfmt.set.numfmt', { unitId, subUnitId, values });
730
+ }
731
+ getActiveSheetInfo() {
732
+ // CONVENTION-EXCEPT: any — FWorksheet public types chưa expose getSheetId/getName
733
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
734
+ const sheet = this.univerAPI?.getActiveWorkbook()?.getActiveSheet();
735
+ if (!sheet)
736
+ return null;
737
+ return {
738
+ sheetId: (sheet.getSheetId?.() ?? ''),
739
+ sheetName: (sheet.getSheetName?.() ?? sheet.getName?.() ?? ''),
740
+ };
741
+ }
742
+ /** Đọc numfmt pattern của một cell từ workbook snapshot. */
743
+ readCellNumfmt(sheetId, row, col) {
744
+ try {
745
+ // CONVENTION-EXCEPT: any — FWorkbook.save() không expose typing
746
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
747
+ const ss = this.univerAPI?.getActiveWorkbook()?.save();
748
+ const styles = ss?.styles;
749
+ const cells = ss?.sheets?.[sheetId]?.cellData;
750
+ const styleId = cells?.[String(row)]?.[String(col)]?.s;
751
+ if (!styleId || !styles)
752
+ return 'General';
753
+ return styles[styleId]?.n?.pattern ?? styles[styleId]?.nf?.pattern ?? 'General';
754
+ }
755
+ catch {
756
+ return 'General';
757
+ }
758
+ }
759
+ /** Đọc giá trị số (Excel serial) của một ô; null nếu ô rỗng/không phải số. */
760
+ readCellValue(sheetId, row, col) {
761
+ try {
762
+ // CONVENTION-EXCEPT: any — FWorkbook.save() không expose typing
763
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
764
+ const ss = this.univerAPI?.getActiveWorkbook()?.save();
765
+ const cells = ss?.sheets?.[sheetId]?.cellData;
766
+ const value = cells?.[String(row)]?.[String(col)]?.v;
767
+ return typeof value === 'number' && isFinite(value) ? value : null;
768
+ }
769
+ catch {
770
+ return null;
771
+ }
772
+ }
773
+ getColumnNumfmt(sheetId, colIndex) {
774
+ for (const row of [1, 2, 3]) {
775
+ const pattern = this.readCellNumfmt(sheetId, row, colIndex);
776
+ if (pattern !== 'General')
777
+ return pattern;
778
+ }
779
+ return 'General';
780
+ }
781
+ /** Chụp numfmt gốc từng ô của vùng chọn (giới hạn 500 dòng như applyFormat) để Cancel khôi phục đúng. */
782
+ captureRangeNumfmt(snap) {
783
+ if (!snap)
784
+ return null;
785
+ const values = [];
786
+ const maxRow = Math.min(snap.endRow, snap.startRow + 499);
787
+ for (let r = snap.startRow; r <= maxRow; r++) {
788
+ for (let c = snap.startColumn; c <= snap.endColumn; c++) {
789
+ values.push({ pattern: this.readCellNumfmt(snap.subUnitId, r, c), row: r, col: c });
790
+ }
791
+ }
792
+ return values.length ? values : null;
793
+ }
794
+ extractAllColumnFormats(snapshot) {
795
+ const styles = snapshot['styles'];
796
+ const sheets = snapshot['sheets'];
797
+ const order = snapshot['sheetOrder'];
798
+ if (!styles || !sheets)
799
+ return [];
800
+ const results = [];
801
+ const seen = new Set();
802
+ for (const sheetId of order ?? Object.keys(sheets)) {
803
+ const sheetData = sheets[sheetId];
804
+ if (!sheetData?.cellData)
805
+ continue;
806
+ const sheetName = sheetData.name ?? sheetId;
807
+ const headerRow = sheetData.cellData['0'] ?? {};
808
+ for (const rowKey of ['1', '2', '3']) {
809
+ const row = sheetData.cellData[rowKey];
810
+ if (!row)
811
+ continue;
812
+ for (const colKey of Object.keys(row)) {
813
+ const key = `${sheetId}:${colKey}`;
814
+ if (seen.has(key))
815
+ continue;
816
+ const styleId = row[colKey]?.s;
817
+ if (!styleId)
818
+ continue;
819
+ const pattern = styles[styleId]?.n?.pattern ?? styles[styleId]?.nf?.pattern;
820
+ if (!pattern)
821
+ continue;
822
+ seen.add(key);
823
+ const colIndex = parseInt(colKey, 10);
824
+ const colLetter = this.toColLetter(colIndex);
825
+ const headerV = headerRow[colKey]?.v;
826
+ results.push({
827
+ sheet: sheetName,
828
+ col: colLetter,
829
+ colIndex,
830
+ header: headerV ? String(headerV) : colLetter,
831
+ numfmt: pattern,
832
+ });
833
+ }
834
+ }
835
+ }
836
+ return results;
837
+ }
838
+ toColLetter(index) {
839
+ let result = '';
840
+ let n = index;
841
+ do {
842
+ result = String.fromCharCode(65 + (n % 26)) + result;
843
+ n = Math.floor(n / 26) - 1;
844
+ } while (n >= 0);
845
+ return result;
846
+ }
847
+ buildEmptyWorkbook() {
848
+ return {
849
+ id: 'workbook-default',
850
+ locale: this.resolveLocaleType(),
851
+ name: 'Sheet',
852
+ sheetOrder: ['sheet1'],
853
+ appVersion: '3.0.0-alpha',
854
+ sheets: {
855
+ sheet1: { id: 'sheet1', name: 'Sheet1', rowCount: 30, columnCount: 26, cellData: {} },
856
+ },
857
+ };
858
+ }
859
+ resolveLocaleType() {
860
+ return this.activeLocale() === 'vi' ? LocaleType.VI_VN : LocaleType.EN_US;
861
+ }
862
+ registerColumnClickListener() {
863
+ const wrapper = this.containerRef()?.nativeElement;
864
+ if (!wrapper)
865
+ return;
866
+ this.listenEnableTimer = setTimeout(() => {
867
+ this.listenEnabled = true;
868
+ }, 600);
869
+ const handleDblClick = (event) => {
870
+ if (!this.listenEnabled)
871
+ return;
872
+ if (event.target.tagName !== 'CANVAS')
873
+ return;
874
+ setTimeout(() => {
875
+ this.ngZone.run(() => {
876
+ const range = this.univerAPI?.getActiveWorkbook()?.getActiveSheet()?.getActiveRange();
877
+ if (!range)
878
+ return;
879
+ const { startRow, endRow, startColumn, rangeType } = range.getRange();
880
+ const isColHeader = rangeType === 2 || (startRow === 0 && endRow >= 29);
881
+ const isFirstRow = startRow === 0 && endRow === 0;
882
+ if (!isColHeader && !isFirstRow)
883
+ return;
884
+ this.outColumnDblClick.emit(startColumn);
885
+ });
886
+ }, 50);
887
+ };
888
+ wrapper.addEventListener('dblclick', handleDblClick);
889
+ this.columnClickDisposable = { dispose: () => wrapper.removeEventListener('dblclick', handleDblClick) };
890
+ }
891
+ /**
892
+ * Lắng nghe MUTATION cột (insert/remove/move) — mức mutation để bắt cả undo/redo, một lần/thao tác.
893
+ * Chuẩn hoá thành `T_columnStructureChange` rồi phát ra ngoài cho consumer remap mapping.
894
+ */
895
+ registerColumnOpListener() {
896
+ // CONVENTION-EXCEPT: any — FUniver.onCommandExecuted / commandInfo không expose trong public types
897
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
898
+ const api = this.univerAPI;
899
+ if (typeof api?.onCommandExecuted !== 'function')
900
+ return;
901
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
902
+ this.columnOpDisposable = api.onCommandExecuted((commandInfo) => {
903
+ const change = this.toColumnStructureChange(commandInfo?.id, commandInfo?.params);
904
+ if (change)
905
+ this.ngZone.run(() => this.outColumnStructureChange.emit(change));
906
+ });
907
+ }
908
+ toColumnStructureChange(id, params) {
909
+ const p = params;
910
+ const sheetId = p?.subUnitId ?? '';
911
+ if (id === InsertColMutation.id || id === RemoveColMutation.id) {
912
+ if (!p?.range)
913
+ return null;
914
+ const type = id === InsertColMutation.id ? 'insert' : 'remove';
915
+ return { sheetId, type, startColumn: p.range.startColumn, count: p.range.endColumn - p.range.startColumn + 1 };
916
+ }
917
+ if (id === MoveColsMutation.id) {
918
+ if (!p?.sourceRange || !p?.targetRange)
919
+ return null;
920
+ return {
921
+ sheetId,
922
+ type: 'move',
923
+ startColumn: p.sourceRange.startColumn,
924
+ count: p.sourceRange.endColumn - p.sourceRange.startColumn + 1,
925
+ toColumn: p.targetRange.startColumn,
926
+ };
927
+ }
928
+ return null;
929
+ }
930
+ registerFormatListener() {
931
+ // CONVENTION-EXCEPT: any — FUniver.onCommandExecuted / commandInfo không expose trong public types
932
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
933
+ const api = this.univerAPI;
934
+ if (typeof api?.onCommandExecuted !== 'function')
935
+ return;
936
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
937
+ this.formatDisposable = api.onCommandExecuted((commandInfo) => {
938
+ this.ngZone.run(() => {
939
+ if (commandInfo?.id === 'sheet.operation.open.numfmt.panel') {
940
+ this.numfmtPanelOpen = true;
941
+ this.numfmtConfirmed = false; // reset cờ — phiên mới mặc định chưa Confirm
942
+ this.numfmtDateActive = false; // chưa chọn Type=Date → chưa kích hoạt luồng custom
943
+ // Lưu numfmt gốc từng ô để Cancel khôi phục đúng (vùng chọn có thể nhiều format khác nhau)
944
+ this.originalCellFormats = this.captureRangeNumfmt(this.lastRangeSnapshot);
945
+ // Chỉ bật observer khi panel mở (tránh chạy liên tục theo mọi mutation toàn trang)
946
+ this.numfmtObserver?.observe(document.body, { childList: true, subtree: true });
947
+ // Reopen: Univer hiện lại panel cũ bằng CSS (không mutation) → observer không fire. Chủ động
948
+ // thử inject sau 1 nhịp để bắt trường hợp list ngày đã sẵn trong DOM tái dùng.
949
+ setTimeout(() => {
950
+ if (this.numfmtPanelOpen)
951
+ this.processNumfmtPanel();
952
+ }, 60);
953
+ return;
954
+ }
955
+ if (commandInfo?.id === 'sheet.operation.close.numfmt.panel') {
956
+ this.numfmtPanelOpen = false;
957
+ // Tắt observer khi đóng (perf). KHÔNG gỡ listener: Univer tái dùng DOM panel, gỡ sẽ làm
958
+ // option chết khi mở lại. cleanup chạy trước lần inject mới (DOM mới) hoặc khi destroy.
959
+ this.numfmtObserver?.disconnect();
960
+ // Univer TÁI DÙNG DOM panel khi reopen (nhớ cả Type=Date) → guard data-custom-dates còn sót sẽ
961
+ // chặn re-inject ở lần mở sau, khiến highlight stale + dateActive=false (mất revert). Xoá guard
962
+ // để lần mở kế tiếp inject lại + resync highlight/auto-apply theo cột hiện tại.
963
+ document.querySelector('.numfmt-panel ul.univer-select-list')?.removeAttribute('data-custom-dates');
964
+ // Chỉ khi bấm CONFIRM (numfmtConfirmed=true) mới GIỮ pattern đã chọn cho cả vùng.
965
+ // Cancel / X / click-ngoài đều KHÔNG set cờ → KHÔI PHỤC numfmt GỐC TỪNG Ô (đã chụp khi mở panel).
966
+ const dateActive = this.numfmtDateActive;
967
+ const confirmed = this.numfmtConfirmed;
968
+ const pending = this.pendingNumfmtPattern;
969
+ const originalCells = this.originalCellFormats;
970
+ this.numfmtConfirmed = false;
971
+ this.pendingNumfmtPattern = null;
972
+ this.originalCellFormats = null;
973
+ this.numfmtDateActive = false;
974
+ // Type khác Date (Số/Tiền tệ/%/Text…) do Univer tự apply/cancel → KHÔNG can thiệp,
975
+ // nếu không sẽ revert nhầm format mà user vừa áp qua luồng native của Univer.
976
+ if (!dateActive) {
977
+ this.clearNumfmtPreview();
978
+ return;
979
+ }
980
+ const snap = this.lastRangeSnapshot;
981
+ if (!snap)
982
+ return;
983
+ let values;
984
+ if (confirmed && pending) {
985
+ values = [];
986
+ for (let r = snap.startRow; r <= Math.min(snap.endRow, snap.startRow + 499); r++) {
987
+ for (let c = snap.startColumn; c <= snap.endColumn; c++) {
988
+ values.push({ pattern: pending, row: r, col: c });
989
+ }
990
+ }
991
+ }
992
+ else if (!confirmed && originalCells) {
993
+ values = originalCells;
994
+ }
995
+ else {
996
+ return;
997
+ }
998
+ if (!values.length)
999
+ return;
1000
+ // CONVENTION-EXCEPT: any — FUniver.executeCommand không expose typing
1001
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1002
+ void this.univerAPI?.executeCommand?.('sheet.command.numfmt.set.numfmt', {
1003
+ unitId: snap.unitId,
1004
+ subUnitId: snap.subUnitId,
1005
+ values,
1006
+ });
1007
+ this.clearNumfmtPreview();
1008
+ return;
1009
+ }
1010
+ // Khi panel đang mở: CHỈ user đổi vùng chọn trên grid (set-selections) mới được cập nhật snapshot
1011
+ // để re-target panel sang cột/cell mới. Command nội bộ panel (numfmt.set, mutation…) KHÔNG đụng snapshot.
1012
+ const isUserSelection = commandInfo?.id === 'sheet.operation.set-selections';
1013
+ if (this.numfmtPanelOpen && !isUserSelection)
1014
+ return;
1015
+ const wb = this.univerAPI?.getActiveWorkbook();
1016
+ const ws = wb?.getActiveSheet();
1017
+ const range = ws?.getActiveRange();
1018
+ if (!range || !wb || !ws)
1019
+ return;
1020
+ const rangeData = range.getRange();
1021
+ this.activeCellRef.set(`${this.toColLetter(rangeData.startColumn)}${rangeData.startRow + 1}`);
1022
+ // CONVENTION-EXCEPT: any — FWorkbook.getId / FWorksheet.getSheetId chưa expose trong types
1023
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1024
+ const wbAny = wb;
1025
+ const wsAny = ws;
1026
+ /* eslint-enable @typescript-eslint/no-explicit-any */
1027
+ this.lastRangeSnapshot = {
1028
+ unitId: (wbAny.getId?.() ?? wbAny.getUnitId?.() ?? ''),
1029
+ subUnitId: (wsAny.getSheetId?.() ?? wsAny.getUnitId?.() ?? ''),
1030
+ startRow: rangeData.startRow,
1031
+ endRow: rangeData.endRow,
1032
+ startColumn: rangeData.startColumn,
1033
+ endColumn: rangeData.endColumn,
1034
+ };
1035
+ // CONVENTION-EXCEPT: any — FRange.getCellStyleData chưa expose trong types
1036
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1037
+ const rangeAny = range;
1038
+ const style = rangeAny.getCellStyleData?.();
1039
+ const numFmt = style?.n?.pattern ?? style?.nf?.pattern;
1040
+ this.activeCellFormat.set(numFmt && numFmt !== '0' ? numFmt : 'General');
1041
+ // User đổi vùng chọn LÚC panel Date đang mở → tích luỹ numfmt gốc của vùng mới (để Cancel
1042
+ // revert đúng mọi ô đã chạm) rồi đồng bộ lại panel (label + highlight + auto-apply) theo cột mới.
1043
+ if (this.numfmtPanelOpen && this.numfmtDateActive) {
1044
+ this.mergeOriginalCellFormats(this.captureRangeNumfmt(this.lastRangeSnapshot));
1045
+ this.numfmtResync?.();
1046
+ }
1047
+ });
1048
+ });
1049
+ }
1050
+ /** Gộp numfmt gốc của vùng vừa chọn vào snapshot revert, giữ giá trị GỐC ĐẦU TIÊN cho mỗi ô. */
1051
+ mergeOriginalCellFormats(extra) {
1052
+ if (!extra)
1053
+ return;
1054
+ if (!this.originalCellFormats) {
1055
+ this.originalCellFormats = extra;
1056
+ return;
1057
+ }
1058
+ const seen = new Set(this.originalCellFormats.map((v) => `${v.row}:${v.col}`));
1059
+ for (const cell of extra) {
1060
+ if (seen.has(`${cell.row}:${cell.col}`))
1061
+ continue;
1062
+ this.originalCellFormats.push(cell);
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Xoá live-preview overlay của Univer numfmt panel.
1067
+ *
1068
+ * Khi panel numfmt mở, `NumfmtController._previewPattern` được Univer dùng để vẽ đè
1069
+ * (preview) lên hiển thị thật của vùng chọn. Vì các option date custom ghi numfmt
1070
+ * trực tiếp vào model (không đi qua panel gốc của Univer), preview này không bao giờ
1071
+ * khớp model → cột không đổi hiển thị ngay. Set `_previewPattern = null` + force re-render
1072
+ * (`_forceUpdate`) để canvas vẽ đúng giá trị model tức thì.
1073
+ *
1074
+ * NumfmtController không export khỏi `@univerjs/sheets-numfmt` nên phải tìm instance
1075
+ * qua injector của Univer (cache lại sau lần đầu).
1076
+ */
1077
+ clearNumfmtPreview() {
1078
+ const controller = this.resolveNumfmtController();
1079
+ if (!controller)
1080
+ return;
1081
+ if (controller._previewPattern != null)
1082
+ controller._previewPattern = null;
1083
+ // LUÔN force re-render: numfmt vừa áp vào model cần vẽ lại canvas ngay (kể cả khi không có preview).
1084
+ controller._forceUpdate?.();
1085
+ }
1086
+ // CONVENTION-EXCEPT: any — truy cập injector + NumfmtController nội bộ của Univer (không có public API)
1087
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1088
+ resolveNumfmtController() {
1089
+ if (this.numfmtController)
1090
+ return this.numfmtController;
1091
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1092
+ const injector = this.univer?.__getInjector?.();
1093
+ if (!injector)
1094
+ return null;
1095
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1096
+ const scanMap = (map) => {
1097
+ if (!(map instanceof Map))
1098
+ return null;
1099
+ for (const value of map.values()) {
1100
+ const candidates = Array.isArray(value) ? value : [value];
1101
+ for (const candidate of candidates) {
1102
+ const instance = candidate?.instance !== undefined ? candidate.instance : candidate;
1103
+ if (instance && typeof instance === 'object' && '_previewPattern' in instance)
1104
+ return instance;
1105
+ }
1106
+ }
1107
+ return null;
1108
+ };
1109
+ // Các instance của redi nằm trong Map lồng 1 cấp (vd injector.resolvedDependencyCollection.resolvedDependencies)
1110
+ for (const key of Object.keys(injector)) {
1111
+ const value = injector[key];
1112
+ let found = scanMap(value);
1113
+ if (!found && value && typeof value === 'object') {
1114
+ for (const nestedKey of Object.keys(value)) {
1115
+ found = scanMap(value[nestedKey]);
1116
+ if (found)
1117
+ break;
1118
+ }
1119
+ }
1120
+ if (found) {
1121
+ this.numfmtController = found;
1122
+ return found;
1123
+ }
1124
+ }
1125
+ return null;
1126
+ }
1127
+ /**
1128
+ * Tạo observer để inject option ngày custom + highlight khi panel numfmt render.
1129
+ * Observer CHỈ được bật/tắt theo vòng đời panel (xem open/close.numfmt.panel handler) để
1130
+ * không chạy liên tục theo mọi mutation DOM toàn trang.
1131
+ */
1132
+ registerNumfmtDropdownObserver() {
1133
+ this.numfmtObserver = new MutationObserver(() => this.processNumfmtPanel());
1134
+ }
1135
+ /**
1136
+ * Thử inject option ngày custom + highlight cho panel numfmt hiện tại.
1137
+ * Gọi từ 2 nguồn: (1) MutationObserver khi Univer render list ngày; (2) trực tiếp lúc mở panel —
1138
+ * vì Univer TÁI DÙNG DOM panel khi reopen (hiện lại bằng CSS, không phát sinh mutation) nên observer
1139
+ * không kích hoạt, phải chủ động gọi để re-inject + resync theo cột hiện tại.
1140
+ */
1141
+ processNumfmtPanel() {
1142
+ syncActiveNumfmtHighlight();
1143
+ const result = injectCustomDateFormats(() => this.lastRangeSnapshot,
1144
+ // CONVENTION-EXCEPT: any — FUniver.executeCommand không expose typing
1145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1146
+ (cmdId, params) => void this.univerAPI.executeCommand?.(cmdId, params), (pattern) => {
1147
+ this.pendingNumfmtPattern = pattern;
1148
+ }, () => {
1149
+ const snap = this.lastRangeSnapshot;
1150
+ if (!snap)
1151
+ return this.activeCellFormat();
1152
+ // Quét vài dòng đầu của vùng chọn, bỏ qua header/General để lấy format ĐẠI DIỆN của dữ liệu.
1153
+ // Khi chọn cả cột, startRow = 0 là header (thường General) → nếu chỉ đọc dòng đó sẽ highlight sai option.
1154
+ const maxScan = Math.min(snap.endRow, snap.startRow + 4);
1155
+ for (let r = snap.startRow; r <= maxScan; r++) {
1156
+ const fmt = this.readCellNumfmt(snap.subUnitId, r, snap.startColumn);
1157
+ if (fmt !== 'General')
1158
+ return fmt;
1159
+ }
1160
+ return this.activeCellFormat();
1161
+ }, () => this.clearNumfmtPreview(), () => {
1162
+ // Giá trị thực của ô đại diện (dòng dữ liệu đầu có số) → label option khớp dữ liệu cột.
1163
+ const snap = this.lastRangeSnapshot;
1164
+ if (!snap)
1165
+ return null;
1166
+ const maxScan = Math.min(snap.endRow, snap.startRow + 4);
1167
+ for (let r = snap.startRow; r <= maxScan; r++) {
1168
+ const value = this.readCellValue(snap.subUnitId, r, snap.startColumn);
1169
+ if (value !== null)
1170
+ return value;
1171
+ }
1172
+ return null;
1173
+ }, () => {
1174
+ this.numfmtConfirmed = true;
1175
+ });
1176
+ // result chỉ truthy khi THỰC SỰ inject trên 1 listContainer Date mới (guard data-custom-dates chặn
1177
+ // các lần gọi thừa). Gỡ listener của lần inject trước trước khi giữ cleanup mới → tránh tích tụ.
1178
+ // Đồng thời lưu resync để re-target panel khi user đổi cột, và đánh dấu luồng Date đang hoạt động.
1179
+ if (result) {
1180
+ this.numfmtCleanup?.();
1181
+ this.numfmtCleanup = result.cleanup;
1182
+ this.numfmtResync = result.resync;
1183
+ this.numfmtDateActive = true;
1184
+ }
1185
+ }
1186
+ initUniver(data) {
1187
+ const wrapper = this.containerRef()?.nativeElement;
1188
+ if (!wrapper)
1189
+ return;
1190
+ wrapper.replaceChildren();
1191
+ const container = document.createElement('div');
1192
+ container.style.cssText = 'width:100%;height:100%';
1193
+ wrapper.appendChild(container);
1194
+ const univer = new Univer({
1195
+ locale: this.resolveLocaleType(),
1196
+ locales: {
1197
+ [LocaleType.EN_US]: univerLocaleEn,
1198
+ [LocaleType.VI_VN]: univerLocaleVi,
1199
+ },
1200
+ theme: defaultTheme,
1201
+ });
1202
+ this.univer = univer;
1203
+ univer.registerPlugin(UniverRenderEnginePlugin);
1204
+ univer.registerPlugin(UniverFormulaEnginePlugin);
1205
+ univer.registerPlugin(UniverUIPlugin, { container, header: this.showToolbar(), footer: this.showSheetTabs() });
1206
+ univer.registerPlugin(UniverDocsPlugin, { hasScroll: false });
1207
+ univer.registerPlugin(UniverDocsUIPlugin);
1208
+ univer.registerPlugin(UniverSheetsPlugin);
1209
+ univer.registerPlugin(UniverSheetsUIPlugin, { menu: this.toolbarMenuConfig() });
1210
+ univer.registerPlugin(UniverSheetsNumfmtPlugin);
1211
+ univer.registerPlugin(UniverSheetsFormulaPlugin);
1212
+ univer.registerPlugin(UniverSheetsFormulaUIPlugin);
1213
+ univer.createUnit(UniverInstanceType.UNIVER_SHEET, data);
1214
+ this.univerAPI = FUniver.newAPI(univer);
1215
+ if (this.pendingReadyCb) {
1216
+ // Wait for .more-numfmt-type to appear in DOM — this signals that UniverSheetsUIPlugin
1217
+ // has fully rendered the toolbar and UniverSheetsNumfmtPlugin's INumfmtModel is ready.
1218
+ const wrapper = this.containerRef()?.nativeElement;
1219
+ if (wrapper) {
1220
+ this.readyObserver = new MutationObserver(() => {
1221
+ if (!wrapper.querySelector('.more-numfmt-type'))
1222
+ return;
1223
+ this.readyObserver?.disconnect();
1224
+ this.readyObserver = null;
1225
+ const cb = this.pendingReadyCb;
1226
+ this.pendingReadyCb = null;
1227
+ cb?.();
1228
+ });
1229
+ this.readyObserver.observe(wrapper, { childList: true, subtree: true });
1230
+ }
1231
+ }
1232
+ this.registerColumnClickListener();
1233
+ this.registerColumnOpListener();
1234
+ this.registerFormatListener();
1235
+ this.registerNumfmtDropdownObserver();
1236
+ this.registerToolbarWidthSync();
1237
+ this.registerNumfmtClickInterceptor();
1238
+ }
1239
+ /**
1240
+ * Khắc phục lỗi toolbar collapse sai (gom item vào menu ⋮ dù còn khoảng trắng).
1241
+ *
1242
+ * Univer (component Ribbon) quyết định overflow bằng 1 container ĐO off-screen (con trực tiếp của
1243
+ * header, position:absolute). Thuật toán: `total = 90; cho mỗi ITEM total += item.clientWidth + 8;
1244
+ * nếu total > measuringContainer.clientWidth → collapse item`. Công thức này OVER-COUNT 2 chỗ:
1245
+ * • +90px reserve cho nút ⋮ (cộng kể cả khi không cần collapse).
1246
+ * • +8px MỖI item, trong khi khoảng cách (gap) thực tế chỉ nằm giữa các GROUP.
1247
+ * Nếu set bề rộng container đo = đúng headerW thì `total` (vd 961) > headerW (vd 900) → Univer tưởng
1248
+ * tràn và gom item vào ⋮ dù content thật chỉ ~829px (còn ~70px trống).
1249
+ *
1250
+ * Fix (triệt để, không phụ thuộc timing): pin bề rộng container ĐO = HẰNG SỐ rất lớn qua CSS RULE trong
1251
+ * 1 <style> riêng. Khi đó `total` của Univer KHÔNG BAO GIỜ vượt → KHÔNG gom item vào ⋮ → luôn hiển thị
1252
+ * ĐỦ option (đúng yêu cầu). Vì sao chọn cách này thay vì khớp width = headerW:
1253
+ * • Thuật toán Univer over-count (90 reserve ⋮ + 8px/item) → khớp sát headerW vẫn collapse sai khi còn chỗ.
1254
+ * • ResizeObserver của Univer hay KẸT STALE: 1 transient lúc load (icon/font làm item rộng nhất thời) đặt
1255
+ * collapsedIds rồi KHÔNG re-fire → kẹt ⋮ vĩnh viễn dù sau đó còn chỗ (đúng triệu chứng reload luôn ⋮).
1256
+ * • Set inline lại bị React re-render strip → oscillation. CSS rule !important React không strip được.
1257
+ * Container đo vốn off-screen (position:absolute, opacity:0) nên width khổng lồ hoàn toàn vô hình. Khi
1258
+ * vùng chứa quá hẹp (hiếm với spreadsheet) toolbar cuộn ngang — xem SCSS `.univer-toolbar` overflow-x.
1259
+ */
1260
+ registerToolbarWidthSync() {
1261
+ const wrapper = this.containerRef()?.nativeElement;
1262
+ if (!wrapper)
1263
+ return;
1264
+ wrapper.classList.add(this.toolbarScopeClass);
1265
+ const styleEl = document.createElement('style');
1266
+ this.toolbarStyleEl = styleEl;
1267
+ styleEl.textContent = `.${this.toolbarScopeClass} .univer-workbench-container-header > .univer-toolbar-container{width:100000px !important;max-width:none !important;}`;
1268
+ document.head.appendChild(styleEl);
1269
+ }
1270
+ registerNumfmtClickInterceptor() {
1271
+ const clickHandler = (e) => {
1272
+ const target = e.target;
1273
+ if (target.closest('.numfmt-panel') ?? target.closest('.more-numfmt-type-options'))
1274
+ return;
1275
+ const numfmtText = target.closest('.more-numfmt-type');
1276
+ const numfmtSelect = target.closest('.univer-toolbar-item-select');
1277
+ const isNumfmtBtn = numfmtText ?? (numfmtSelect?.querySelector('.more-numfmt-type') ? numfmtSelect : null);
1278
+ if (!isNumfmtBtn)
1279
+ return;
1280
+ e.preventDefault();
1281
+ e.stopPropagation();
1282
+ this.executeCommand('sheet.operation.open.numfmt.panel', undefined);
1283
+ };
1284
+ document.body.addEventListener('click', clickHandler, true);
1285
+ this.numfmtClickDisposable = { dispose: () => document.body.removeEventListener('click', clickHandler, true) };
1286
+ }
1287
+ destroyUniver() {
1288
+ if (this.pendingInitTimer !== null) {
1289
+ clearTimeout(this.pendingInitTimer);
1290
+ this.pendingInitTimer = null;
1291
+ }
1292
+ this.columnClickDisposable?.dispose();
1293
+ this.columnClickDisposable = null;
1294
+ this.columnOpDisposable?.dispose();
1295
+ this.columnOpDisposable = null;
1296
+ this.formatDisposable?.dispose();
1297
+ this.formatDisposable = null;
1298
+ this.numfmtObserver?.disconnect();
1299
+ this.numfmtObserver = null;
1300
+ this.toolbarResizeObserver?.disconnect();
1301
+ this.toolbarResizeObserver = null;
1302
+ this.toolbarStyleEl?.remove();
1303
+ this.toolbarStyleEl = null;
1304
+ this.numfmtClickDisposable?.dispose();
1305
+ this.numfmtClickDisposable = null;
1306
+ this.numfmtCleanup?.();
1307
+ this.numfmtCleanup = null;
1308
+ this.numfmtResync = null;
1309
+ this.numfmtDateActive = false;
1310
+ this.readyObserver?.disconnect();
1311
+ this.readyObserver = null;
1312
+ this.activeCellFormat.set('General');
1313
+ this.activeCellRef.set('');
1314
+ this.lastRangeSnapshot = null;
1315
+ this.pendingNumfmtPattern = null;
1316
+ this.numfmtConfirmed = false;
1317
+ this.originalCellFormats = null;
1318
+ this.numfmtPanelOpen = false;
1319
+ this.listenEnabled = false;
1320
+ if (this.listenEnableTimer !== null) {
1321
+ clearTimeout(this.listenEnableTimer);
1322
+ this.listenEnableTimer = null;
1323
+ }
1324
+ this.numfmtController = null;
1325
+ this.univer?.dispose();
1326
+ this.univer = null;
1327
+ this.univerAPI = null;
1328
+ }
1329
+ reinitUniver() {
1330
+ const currentData = this.univerAPI?.getActiveWorkbook()?.save();
1331
+ this.destroyUniver();
1332
+ setTimeout(() => this.initUniver(currentData ?? this.buildEmptyWorkbook()));
1333
+ }
1334
+ ngOnDestroy() {
1335
+ this.destroyUniver();
1336
+ }
1337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsSpreadsheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.14", type: LibsUiComponentsSpreadsheetComponent, isStandalone: true, selector: "libs_ui-components-spreadsheet", inputs: { locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, toolbarMenuConfig: { classPropertyName: "toolbarMenuConfig", publicName: "toolbarMenuConfig", isSignal: true, isRequired: false, transformFunction: null }, mappingIndicatorConfig: { classPropertyName: "mappingIndicatorConfig", publicName: "mappingIndicatorConfig", isSignal: true, isRequired: false, transformFunction: null }, mappingTargets: { classPropertyName: "mappingTargets", publicName: "mappingTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outColumnDblClick: "outColumnDblClick", outColumnStructureChange: "outColumnStructureChange" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["containerRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #containerRef class=\"libs-ui-components-spreadsheet-container\"></div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}.libs-ui-components-spreadsheet-container{width:100%;height:100%}::ng-deep .univer-toolbar-container{flex-grow:1!important;margin:0!important}::ng-deep .univer-toolbar{overflow-x:auto}::ng-deep .univer-toolbar>.univer-toolbar-container{justify-content:flex-start!important;flex-shrink:0!important}::ng-deep .univer-toolbar-more-container{display:flex!important;flex-wrap:wrap!important;gap:8px!important;width:max-content!important;max-width:320px!important;padding:8px!important}::ng-deep .univer-toolbar-more-container .univer-toolbar-group{flex-shrink:0!important}::ng-deep .univer-select-dropdown .univer-select-item[title=\"Custom Format\"]{display:none!important}::ng-deep .numfmt-panel{padding:0 4px}::ng-deep .univer-custom-format-title{margin-top:12px!important;font-size:11px!important;font-weight:700!important;color:#8c8c8c!important;text-transform:uppercase!important;letter-spacing:.5px!important}::ng-deep .univer-custom-format-input{margin-top:6px!important}::ng-deep .univer-custom-format-input input,::ng-deep .univer-custom-format-input textarea{font-family:SFMono-Regular,Consolas,monospace!important;font-size:12px!important;border-radius:6px!important}::ng-deep .univer-custom-format-history-list{margin-top:6px!important;padding:4px!important;border:1.5px solid #f0f0f0!important;border-radius:8px!important;max-height:260px!important;overflow-y:auto!important;background:#fafafa!important;scrollbar-width:thin;scrollbar-color:#d9d9d9 transparent}::ng-deep .univer-custom-format-history-list::-webkit-scrollbar{width:4px}::ng-deep .univer-custom-format-history-list::-webkit-scrollbar-thumb{background:#d9d9d9;border-radius:4px}::ng-deep .univer-custom-format-history-list-item{padding:5px 8px!important;border-radius:6px!important;gap:8px!important;transition:all .12s;border-bottom:1px solid transparent!important}::ng-deep .univer-custom-format-history-list-item:hover{background:#e6f4ff!important}::ng-deep .univer-custom-format-history-list-item:hover>span:last-child,::ng-deep .univer-custom-format-history-list-item:hover>div:last-child{color:#1677ff!important}::ng-deep .univer-custom-format-history-list-item>span:not(.univer-custom-format-history-list-item-icon-wrap),::ng-deep .univer-custom-format-history-list-item>div:not(.univer-custom-format-history-list-item-icon-wrap){font-family:SFMono-Regular,Consolas,monospace!important;font-size:11px!important;color:#434343!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;max-width:240px!important}::ng-deep .univer-custom-format-history-list-item-icon-wrap{flex-shrink:0!important;opacity:.6}::ng-deep .univer-custom-format-des{margin-top:6px!important;font-size:11px!important;line-height:1.5!important;color:#8c8c8c!important;padding:6px 8px!important;background:#f5f5f5!important;border-radius:6px!important;border-left:3px solid #d9d9d9!important}::ng-deep .univer-numfmt-custom-item{position:relative!important;padding:5px 8px!important;border-radius:6px!important;font-family:SFMono-Regular,Consolas,monospace!important;font-size:12px!important;color:#434343!important;cursor:pointer!important;transition:background .12s,color .12s;display:block!important;border-left:3px solid transparent}::ng-deep .univer-numfmt-custom-item:hover{background:#f0f5ff!important;color:#1677ff!important;border-left-color:#1677ff!important}::ng-deep .univer-numfmt-custom-item.univer-numfmt-active{background:#e6f4ff!important;color:#1677ff!important;font-weight:600!important;padding-left:24px!important;border-left:3px solid #1677ff!important}::ng-deep .univer-numfmt-custom-item.univer-numfmt-active:before{content:\"\\2713\";position:absolute;left:7px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#1677ff;line-height:1}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item){background:#e6f4ff!important;border-left:3px solid #1677ff!important;border-radius:6px!important}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item) a{color:#1677ff!important}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item) .univer-select-list-item-icon{color:#1677ff!important}::ng-deep .more-numfmt-type{cursor:pointer;font-size:0!important;display:inline-flex;align-items:center;line-height:1}::ng-deep .more-numfmt-type:before{content:\"\";display:inline-block;width:31px;height:18px;background-color:currentColor;mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2034%2020'%3E%3Ctext%20x='0'%20y='15'%20font-size='13'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E123%3C/text%3E%3Ctext%20x='20'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E%25%3C/text%3E%3Ctext%20x='27'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E$%3C/text%3E%3Ccircle%20cx='26.5'%20cy='15'%20r='3.4'%20fill='none'%20stroke='black'%20stroke-width='1.1'/%3E%3Cpath%20d='M26.5%2013.2v1.8l1.2.9'%20fill='none'%20stroke='black'%20stroke-width='1.1'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2034%2020'%3E%3Ctext%20x='0'%20y='15'%20font-size='13'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E123%3C/text%3E%3Ctext%20x='20'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E%25%3C/text%3E%3Ctext%20x='27'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E$%3C/text%3E%3Ccircle%20cx='26.5'%20cy='15'%20r='3.4'%20fill='none'%20stroke='black'%20stroke-width='1.1'/%3E%3Cpath%20d='M26.5%2013.2v1.8l1.2.9'%20fill='none'%20stroke='black'%20stroke-width='1.1'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E\") no-repeat center/contain}::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type):hover{background-color:rgb(var(--grey-100));border-radius:var(--border-radius-base)}::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type) .more-numfmt-type,::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type) .univer-toolbar-item-select-arrow{background-color:transparent!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1339
+ }
1340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsSpreadsheetComponent, decorators: [{
1341
+ type: Component,
1342
+ args: [{ selector: 'libs_ui-components-spreadsheet', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div #containerRef class=\"libs-ui-components-spreadsheet-container\"></div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}.libs-ui-components-spreadsheet-container{width:100%;height:100%}::ng-deep .univer-toolbar-container{flex-grow:1!important;margin:0!important}::ng-deep .univer-toolbar{overflow-x:auto}::ng-deep .univer-toolbar>.univer-toolbar-container{justify-content:flex-start!important;flex-shrink:0!important}::ng-deep .univer-toolbar-more-container{display:flex!important;flex-wrap:wrap!important;gap:8px!important;width:max-content!important;max-width:320px!important;padding:8px!important}::ng-deep .univer-toolbar-more-container .univer-toolbar-group{flex-shrink:0!important}::ng-deep .univer-select-dropdown .univer-select-item[title=\"Custom Format\"]{display:none!important}::ng-deep .numfmt-panel{padding:0 4px}::ng-deep .univer-custom-format-title{margin-top:12px!important;font-size:11px!important;font-weight:700!important;color:#8c8c8c!important;text-transform:uppercase!important;letter-spacing:.5px!important}::ng-deep .univer-custom-format-input{margin-top:6px!important}::ng-deep .univer-custom-format-input input,::ng-deep .univer-custom-format-input textarea{font-family:SFMono-Regular,Consolas,monospace!important;font-size:12px!important;border-radius:6px!important}::ng-deep .univer-custom-format-history-list{margin-top:6px!important;padding:4px!important;border:1.5px solid #f0f0f0!important;border-radius:8px!important;max-height:260px!important;overflow-y:auto!important;background:#fafafa!important;scrollbar-width:thin;scrollbar-color:#d9d9d9 transparent}::ng-deep .univer-custom-format-history-list::-webkit-scrollbar{width:4px}::ng-deep .univer-custom-format-history-list::-webkit-scrollbar-thumb{background:#d9d9d9;border-radius:4px}::ng-deep .univer-custom-format-history-list-item{padding:5px 8px!important;border-radius:6px!important;gap:8px!important;transition:all .12s;border-bottom:1px solid transparent!important}::ng-deep .univer-custom-format-history-list-item:hover{background:#e6f4ff!important}::ng-deep .univer-custom-format-history-list-item:hover>span:last-child,::ng-deep .univer-custom-format-history-list-item:hover>div:last-child{color:#1677ff!important}::ng-deep .univer-custom-format-history-list-item>span:not(.univer-custom-format-history-list-item-icon-wrap),::ng-deep .univer-custom-format-history-list-item>div:not(.univer-custom-format-history-list-item-icon-wrap){font-family:SFMono-Regular,Consolas,monospace!important;font-size:11px!important;color:#434343!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;max-width:240px!important}::ng-deep .univer-custom-format-history-list-item-icon-wrap{flex-shrink:0!important;opacity:.6}::ng-deep .univer-custom-format-des{margin-top:6px!important;font-size:11px!important;line-height:1.5!important;color:#8c8c8c!important;padding:6px 8px!important;background:#f5f5f5!important;border-radius:6px!important;border-left:3px solid #d9d9d9!important}::ng-deep .univer-numfmt-custom-item{position:relative!important;padding:5px 8px!important;border-radius:6px!important;font-family:SFMono-Regular,Consolas,monospace!important;font-size:12px!important;color:#434343!important;cursor:pointer!important;transition:background .12s,color .12s;display:block!important;border-left:3px solid transparent}::ng-deep .univer-numfmt-custom-item:hover{background:#f0f5ff!important;color:#1677ff!important;border-left-color:#1677ff!important}::ng-deep .univer-numfmt-custom-item.univer-numfmt-active{background:#e6f4ff!important;color:#1677ff!important;font-weight:600!important;padding-left:24px!important;border-left:3px solid #1677ff!important}::ng-deep .univer-numfmt-custom-item.univer-numfmt-active:before{content:\"\\2713\";position:absolute;left:7px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#1677ff;line-height:1}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item){background:#e6f4ff!important;border-left:3px solid #1677ff!important;border-radius:6px!important}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item) a{color:#1677ff!important}::ng-deep .numfmt-panel li.univer-select-list-item.univer-select-list-item-select:not(.univer-numfmt-custom-item) .univer-select-list-item-icon{color:#1677ff!important}::ng-deep .more-numfmt-type{cursor:pointer;font-size:0!important;display:inline-flex;align-items:center;line-height:1}::ng-deep .more-numfmt-type:before{content:\"\";display:inline-block;width:31px;height:18px;background-color:currentColor;mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2034%2020'%3E%3Ctext%20x='0'%20y='15'%20font-size='13'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E123%3C/text%3E%3Ctext%20x='20'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E%25%3C/text%3E%3Ctext%20x='27'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E$%3C/text%3E%3Ccircle%20cx='26.5'%20cy='15'%20r='3.4'%20fill='none'%20stroke='black'%20stroke-width='1.1'/%3E%3Cpath%20d='M26.5%2013.2v1.8l1.2.9'%20fill='none'%20stroke='black'%20stroke-width='1.1'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2034%2020'%3E%3Ctext%20x='0'%20y='15'%20font-size='13'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E123%3C/text%3E%3Ctext%20x='20'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E%25%3C/text%3E%3Ctext%20x='27'%20y='8'%20font-size='7'%20font-weight='700'%20fill='black'%20font-family='Arial,sans-serif'%3E$%3C/text%3E%3Ccircle%20cx='26.5'%20cy='15'%20r='3.4'%20fill='none'%20stroke='black'%20stroke-width='1.1'/%3E%3Cpath%20d='M26.5%2013.2v1.8l1.2.9'%20fill='none'%20stroke='black'%20stroke-width='1.1'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E\") no-repeat center/contain}::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type):hover{background-color:rgb(var(--grey-100));border-radius:var(--border-radius-base)}::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type) .more-numfmt-type,::ng-deep .univer-toolbar-item-select:has(.more-numfmt-type) .univer-toolbar-item-select-arrow{background-color:transparent!important}\n"] }]
1343
+ }], ctorParameters: () => [] });
1344
+
1345
+ /**
1346
+ * Generated bundle index. Do not edit.
1347
+ */
1348
+
1349
+ export { LibsUiComponentsSpreadsheetComponent, applyMappingIndicators, defaultMappingIndicatorConfig, remapColumnIndexes, stripMappingIndicators, toColumnLetter };
1350
+ //# sourceMappingURL=libs-ui-components-spreadsheet.mjs.map