@fieldia/grid 0.12.0 → 0.13.0
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/package.json +3 -3
- package/src/lib/grid.js +213 -54
- package/src/lib/grid.js.map +1 -1
- package/src/lib/grid.mjs +213 -54
- package/src/lib/grid.mjs.map +1 -1
- package/src/lib/styles.d.mts +1 -1
- package/src/lib/styles.d.ts +1 -1
- package/src/lib/styles.js +24 -3
- package/src/lib/styles.js.map +1 -1
- package/src/lib/styles.mjs +24 -3
- package/src/lib/styles.mjs.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldia/grid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Fieldia's table of lines on AG Grid: spreadsheet editing with Fieldia's own fields in the cells.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://fieldia.dev",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"./package.json": "./package.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@fieldia/core": "0.
|
|
25
|
-
"@fieldia/widgets": "0.
|
|
24
|
+
"@fieldia/core": "0.13.0",
|
|
25
|
+
"@fieldia/widgets": "0.13.0",
|
|
26
26
|
"tslib": "^2.8.1"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
package/src/lib/grid.js
CHANGED
|
@@ -333,6 +333,18 @@ class ChooserHeader {
|
|
|
333
333
|
return true;
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
|
+
/** What an empty grid says in its body: the page's words (`emptyLabel`), or a short line of its own. */
|
|
337
|
+
class EmptyOverlay {
|
|
338
|
+
line;
|
|
339
|
+
init(params) {
|
|
340
|
+
this.line = document.createElement('span');
|
|
341
|
+
this.line.className = 'fd-help fd-grid-empty';
|
|
342
|
+
this.line.textContent = params.words;
|
|
343
|
+
}
|
|
344
|
+
getGui() {
|
|
345
|
+
return this.line;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
336
348
|
/** A cell being edited: the field's own Fieldia widget, writing straight into its line. */
|
|
337
349
|
class FieldiaCellEditor {
|
|
338
350
|
box;
|
|
@@ -344,22 +356,22 @@ class FieldiaCellEditor {
|
|
|
344
356
|
/** The line field this editor writes: the column's, or a section's or note's text. */
|
|
345
357
|
column = '';
|
|
346
358
|
def;
|
|
347
|
-
/** A
|
|
348
|
-
|
|
359
|
+
/** A section or note being edited, not an item. */
|
|
360
|
+
kind = null;
|
|
349
361
|
note = null;
|
|
350
|
-
|
|
362
|
+
/** A tags box in a whole line open at once, which makes its line as tall as it needs. */
|
|
363
|
+
grows = false;
|
|
351
364
|
init(params) {
|
|
352
365
|
this.params = params;
|
|
353
366
|
// Not `column`: AG Grid's own params carry the column object under that name.
|
|
354
367
|
const { cell } = params;
|
|
355
368
|
const key = params.data.key;
|
|
356
|
-
const kind = cell.kindOf(params.data);
|
|
369
|
+
const kind = (this.kind = cell.kindOf(params.data));
|
|
357
370
|
const column = (this.column = kind && cell.kinds ? cell.kinds.text : params.subfield);
|
|
358
371
|
const def = (this.def = kind ? (0, widgets_1.kindTextField)(cell.defs[column], kind) : cell.defs[column]);
|
|
359
372
|
// A cell typed in its own way (hours as HH:MM, a per cent) is typed so in its editor too.
|
|
360
373
|
const look = kind ? undefined : cell.looks?.[column];
|
|
361
374
|
const node = { type: 'field', id: `${cell.fieldId}.${key}.${column}`, field: column, ...(look?.widget ? { widget: look.widget } : {}), ...(look?.options ? { options: look.options } : {}) };
|
|
362
|
-
this.calendar = (def.type === 'date' || def.type === 'datetime') && look?.options?.['weekNumbers'] === true;
|
|
363
375
|
const context = {
|
|
364
376
|
form: (0, widgets_1.lineForm)(cell.form, cell.field, key),
|
|
365
377
|
name: column,
|
|
@@ -379,15 +391,34 @@ class FieldiaCellEditor {
|
|
|
379
391
|
this.box.dataset['kind'] = kind;
|
|
380
392
|
this.box.setAttribute('aria-label', def.label);
|
|
381
393
|
this.box.append(this.widget.element);
|
|
394
|
+
// A whole line open at once keeps its editors in the line (AG Grid has no popups then): a tags box makes the line as tall as it needs.
|
|
395
|
+
if (cell.rowMode && def.type === 'many2many') {
|
|
396
|
+
this.grows = true;
|
|
397
|
+
this.box.classList.add('fd-grid-editor-grows');
|
|
398
|
+
if (typeof ResizeObserver !== 'undefined')
|
|
399
|
+
new ResizeObserver(() => this.fitLine()).observe(this.box);
|
|
400
|
+
}
|
|
382
401
|
if (kind === 'note')
|
|
383
402
|
this.growing(this.box.querySelector('textarea'));
|
|
384
|
-
if (
|
|
385
|
-
this.
|
|
403
|
+
else if (def.type === 'text')
|
|
404
|
+
this.paragraph(this.box.querySelector('textarea'));
|
|
386
405
|
if (this.isPopup()) {
|
|
387
406
|
// AG Grid places a popup over its cell but leaves its size to the editor; a distribution's lines take the room they need.
|
|
388
407
|
const { width, height } = params.eGridCell.getBoundingClientRect();
|
|
389
408
|
if (def.type === 'json')
|
|
390
409
|
Object.assign(this.box.style, { minWidth: `${Math.max(width, 360)}px` });
|
|
410
|
+
// Tags wrap in a box of their own, as wide as a few need and as tall as they take, never spilling over the lines around.
|
|
411
|
+
else if (def.type === 'many2many')
|
|
412
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 280)}px`, minHeight: `${height}px` });
|
|
413
|
+
// A paragraph is typed in a box of several lines, wide enough to read them, never spilling over the rows around.
|
|
414
|
+
else if (def.type === 'text')
|
|
415
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 320)}px`, minHeight: `${height}px` });
|
|
416
|
+
// A date is as wide as its day and time need (the stylesheet's max-content), its cell's width at the least.
|
|
417
|
+
else if (def.type === 'date' || def.type === 'datetime')
|
|
418
|
+
Object.assign(this.box.style, { minWidth: `${width}px`, height: `${height}px` });
|
|
419
|
+
// A link is searched by its name: wide enough to read one over a narrow column, on its row's line.
|
|
420
|
+
else if (def.type === 'many2one' || def.type === 'reference')
|
|
421
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 240)}px`, height: `${height}px` });
|
|
391
422
|
else
|
|
392
423
|
Object.assign(this.box.style, { width: `${width}px`, height: `${height}px` });
|
|
393
424
|
}
|
|
@@ -422,9 +453,11 @@ class FieldiaCellEditor {
|
|
|
422
453
|
this.box.addEventListener('focusin', (event) => {
|
|
423
454
|
const input = event.target;
|
|
424
455
|
if (!pointing && input instanceof HTMLInputElement && input.type !== 'checkbox' && input.type !== 'radio')
|
|
425
|
-
input
|
|
456
|
+
selectFromStart(input);
|
|
426
457
|
pointing = false;
|
|
427
458
|
});
|
|
459
|
+
// A value longer than its cell has room for takes the cell's padding too.
|
|
460
|
+
this.box.addEventListener('input', () => this.fit());
|
|
428
461
|
const show = () => {
|
|
429
462
|
const line = this.line();
|
|
430
463
|
// A cell the form found wrong stays marked while it is fixed.
|
|
@@ -450,39 +483,65 @@ class FieldiaCellEditor {
|
|
|
450
483
|
});
|
|
451
484
|
// Enter makes a new line; Ctrl/Cmd+Enter finishes the note.
|
|
452
485
|
area.addEventListener('keydown', (event) => {
|
|
453
|
-
if (event.key !== 'Enter' ||
|
|
486
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey)
|
|
454
487
|
return;
|
|
455
|
-
|
|
488
|
+
// The popup's own handler hears the key as well as the cell's (whose suppressKeyboardEvent leaves Enter alone).
|
|
456
489
|
(0, ag_grid_community_1._stopPropagationForAgGrid)(event);
|
|
490
|
+
if (!(event.ctrlKey || event.metaKey))
|
|
491
|
+
return;
|
|
492
|
+
event.preventDefault();
|
|
457
493
|
this.params.api.stopEditing();
|
|
458
494
|
});
|
|
459
495
|
}
|
|
460
496
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
497
|
+
* A paragraph (a text field) is typed in a box of several lines over its
|
|
498
|
+
* cell: Enter makes a new line there (the grid leaves it alone), and
|
|
499
|
+
* Ctrl/Cmd+Enter finishes it, as a note's does.
|
|
463
500
|
*/
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (!list)
|
|
501
|
+
paragraph(area) {
|
|
502
|
+
if (!area)
|
|
467
503
|
return;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
layer.append(list);
|
|
471
|
-
const place = () => {
|
|
472
|
-
if (list.hidden || !this.box.isConnected)
|
|
504
|
+
area.addEventListener('keydown', (event) => {
|
|
505
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey)
|
|
473
506
|
return;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
507
|
+
// The popup's own handler hears the key as well as the cell's (whose suppressKeyboardEvent leaves Enter alone).
|
|
508
|
+
(0, ag_grid_community_1._stopPropagationForAgGrid)(event);
|
|
509
|
+
if (!(event.ctrlKey || event.metaKey))
|
|
510
|
+
return;
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
this.params.api.stopEditing();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/** A value wider than the room inside the cell's padding takes the padding too, so more of it shows. */
|
|
516
|
+
fit() {
|
|
517
|
+
const input = this.box.querySelector('input:not([type="checkbox"]):not([type="radio"])');
|
|
518
|
+
if (!input)
|
|
519
|
+
return;
|
|
520
|
+
if (this.isPopup()) {
|
|
521
|
+
// A link's name longer than its box widens the box to show it whole, as far as the window allows.
|
|
522
|
+
const short = input.scrollWidth - input.clientWidth;
|
|
523
|
+
if ((this.def.type === 'many2one' || this.def.type === 'reference') && short > 0) {
|
|
524
|
+
const { left, width } = this.box.getBoundingClientRect();
|
|
525
|
+
const room = this.box.ownerDocument.defaultView.innerWidth - left - 16;
|
|
526
|
+
this.box.style.minWidth = `${Math.round(Math.max(width, Math.min(width + short + 8, LINK_EDITOR_MOST, room)))}px`;
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (input.scrollWidth > input.clientWidth)
|
|
531
|
+
this.box.classList.add('fd-grid-editor-tight');
|
|
532
|
+
}
|
|
533
|
+
/** A line as tall as its tags box needs, never shorter than a line. */
|
|
534
|
+
fitLine() {
|
|
535
|
+
const { node, api } = this.params;
|
|
536
|
+
if (!this.box.isConnected || api.isDestroyed())
|
|
537
|
+
return;
|
|
538
|
+
const height = Math.max(ROW_HEIGHT, this.box.scrollHeight + 2);
|
|
539
|
+
if (height <= (node.rowHeight ?? 0))
|
|
540
|
+
return;
|
|
541
|
+
node.setRowHeight(height);
|
|
542
|
+
api.onRowHeightChanged();
|
|
485
543
|
}
|
|
544
|
+
/** A note's box as tall as its text, and its row with it. */
|
|
486
545
|
resize() {
|
|
487
546
|
const area = this.note;
|
|
488
547
|
if (!area)
|
|
@@ -514,6 +573,9 @@ class FieldiaCellEditor {
|
|
|
514
573
|
return this.box;
|
|
515
574
|
}
|
|
516
575
|
afterGuiAttached() {
|
|
576
|
+
this.fit();
|
|
577
|
+
if (this.grows)
|
|
578
|
+
this.fitLine();
|
|
517
579
|
// With a whole line open, every editor is attached: only the one clicked takes the focus.
|
|
518
580
|
if (this.params.cellStartedEdit === false)
|
|
519
581
|
return;
|
|
@@ -525,28 +587,34 @@ class FieldiaCellEditor {
|
|
|
525
587
|
}
|
|
526
588
|
const input = this.box.querySelector('input');
|
|
527
589
|
if (input && input.type !== 'checkbox' && document.activeElement === input)
|
|
528
|
-
input
|
|
590
|
+
selectFromStart(input);
|
|
529
591
|
}
|
|
530
592
|
getValue() {
|
|
531
593
|
return this.line()?.values[this.column];
|
|
532
594
|
}
|
|
533
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* Lists that open below their input need room the cell does not have (a whole open line floats them instead); so do a
|
|
597
|
+
* distribution's lines, a date wider than its cell, and a paragraph's several lines (a note's grow in its own row).
|
|
598
|
+
*/
|
|
534
599
|
isPopup() {
|
|
535
600
|
if (this.params.cell.rowMode)
|
|
536
601
|
return false;
|
|
537
602
|
const type = this.def.type;
|
|
538
|
-
return type === 'many2one' || type === 'many2many' || type === 'reference' || type === 'json' || this.
|
|
603
|
+
return type === 'many2one' || type === 'many2many' || type === 'reference' || type === 'json' || type === 'date' || type === 'datetime' || (type === 'text' && !this.kind);
|
|
539
604
|
}
|
|
540
605
|
getPopupPosition() {
|
|
541
606
|
return 'over';
|
|
542
607
|
}
|
|
543
608
|
destroy() {
|
|
544
609
|
this.leave();
|
|
545
|
-
this.floating?.watch.disconnect();
|
|
546
|
-
this.floating?.list.remove();
|
|
547
610
|
this.widget.destroy?.();
|
|
548
611
|
const { node, api, cell, data } = this.params;
|
|
549
612
|
cell.finish(data.key);
|
|
613
|
+
// The line closed: it gives back the height its tags box took.
|
|
614
|
+
if (this.grows && !api.isDestroyed()) {
|
|
615
|
+
node.setRowHeight(null);
|
|
616
|
+
api.onRowHeightChanged();
|
|
617
|
+
}
|
|
550
618
|
// A note's row grew by hand while it was typed; drawing it afresh measures
|
|
551
619
|
// it again from its text (resetting the height would make it one line tall).
|
|
552
620
|
if (this.note) {
|
|
@@ -557,12 +625,34 @@ class FieldiaCellEditor {
|
|
|
557
625
|
}
|
|
558
626
|
}
|
|
559
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* A box's text selected so typing replaces it, shown from its start: a
|
|
630
|
+
* browser scrolls a long value to its end as it is selected, and its start is
|
|
631
|
+
* what reads. (A date's own input has no text selection to set.)
|
|
632
|
+
*/
|
|
633
|
+
function selectFromStart(input) {
|
|
634
|
+
try {
|
|
635
|
+
input.setSelectionRange(0, input.value.length, 'backward');
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
input.select();
|
|
639
|
+
}
|
|
640
|
+
input.scrollLeft = 0;
|
|
641
|
+
}
|
|
560
642
|
// ---- the widget -------------------------------------------------------------------
|
|
561
643
|
/** Past this many lines a table stops growing and scrolls inside, drawing only the rows in view. */
|
|
562
644
|
const LONG_TABLE = 15;
|
|
563
645
|
const ROW_HEIGHT = 40;
|
|
564
646
|
const HEADER_HEIGHT = 38;
|
|
565
647
|
const EDITABLE = new Set(['char', 'text', 'html', 'integer', 'float', 'monetary', 'date', 'datetime', 'selection', 'many2one', 'many2many', 'reference']);
|
|
648
|
+
/** A badge's pill round its words, both sides: room its column keeps besides theirs. */
|
|
649
|
+
const BADGE_PADDING = 20;
|
|
650
|
+
/** The kinds of field that hold words: a line is named by the first of them. */
|
|
651
|
+
const WORDS = new Set(['char', 'text', 'html', 'many2one', 'reference']);
|
|
652
|
+
/** The kinds of field whose column is as wide as its values need, in characters, as Flectra's lists size them. */
|
|
653
|
+
/** The widest a link's editor grows to show a long name, in pixels. */
|
|
654
|
+
const LINK_EDITOR_MOST = 480;
|
|
655
|
+
const FIXED_CHARS = { boolean: 4, integer: 4, float: 6, monetary: 14, date: 11, datetime: 17 };
|
|
566
656
|
const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.WIDGET_LABELS.en, preferences, locale, dialogs }) => {
|
|
567
657
|
const def = field;
|
|
568
658
|
const kinds = def.lineKinds;
|
|
@@ -625,6 +715,15 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
625
715
|
element.setAttribute('role', 'group');
|
|
626
716
|
const host = document.createElement('div');
|
|
627
717
|
host.className = 'fd-grid-host';
|
|
718
|
+
// A press on a tick in a line — chosen, or a yes or no — is the tick's alone: the grid focusing its cell would
|
|
719
|
+
// scroll a cell cut by the screen's edge into view, moving the tick from under the pointer, and the click would
|
|
720
|
+
// land on the cell instead. (A button in a cell the grid leaves alone.) The tick still takes the focus and the click.
|
|
721
|
+
for (const type of ['pointerdown', 'mousedown']) {
|
|
722
|
+
host.addEventListener(type, (event) => {
|
|
723
|
+
if (event.target.closest?.('.ag-cell input[type="checkbox"]'))
|
|
724
|
+
event.stopPropagation();
|
|
725
|
+
}, true);
|
|
726
|
+
}
|
|
628
727
|
// What is wrong on which line, once a save has been refused.
|
|
629
728
|
const problems = document.createElement('div');
|
|
630
729
|
problems.className = 'fd-error fd-grid-problems';
|
|
@@ -734,6 +833,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
734
833
|
api.setFocusedCell(index, column);
|
|
735
834
|
api.startEditingCell({ rowIndex: index, colKey: column });
|
|
736
835
|
};
|
|
836
|
+
/** A paragraph (a text field) of an item, edited in its box of several lines over the cell. */
|
|
837
|
+
const paragraphCell = (line, column) => !cell.rowMode && !kindOf(line) && def.fields[column]?.type === 'text';
|
|
737
838
|
/**
|
|
738
839
|
* Keys the grid must leave alone, and the two that run off the end of the
|
|
739
840
|
* table while editing: Enter on the last line and Tab on its last editable
|
|
@@ -745,8 +846,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
745
846
|
// Alt+Up/Down moves the line (below), it does not move the focus.
|
|
746
847
|
if (!editing && event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown'))
|
|
747
848
|
return true;
|
|
748
|
-
// In a note, Enter is a new line
|
|
749
|
-
if (editing && event.key === 'Enter' && !event.ctrlKey && !event.metaKey && kindOf(node.data) === 'note')
|
|
849
|
+
// In a note, or a paragraph opened over its cell, Enter is a new line (Ctrl/Cmd+Enter finishes it).
|
|
850
|
+
if (editing && event.key === 'Enter' && !event.ctrlKey && !event.metaKey && (kindOf(node.data) === 'note' || paragraphCell(node.data, column.getColId())))
|
|
750
851
|
return true;
|
|
751
852
|
if (!editing || event.type !== 'keydown' || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey)
|
|
752
853
|
return false;
|
|
@@ -762,8 +863,37 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
762
863
|
}
|
|
763
864
|
return false;
|
|
764
865
|
};
|
|
866
|
+
// Each column's share of the width by what it holds, as Flectra's lists size theirs: yes/no, numbers and dates by
|
|
867
|
+
// their kind, words and links by the longest the lines have — a long name gets the room a short code does not need.
|
|
868
|
+
const linesOf = (value) => (value ?? []).filter((line) => !kindOf(line));
|
|
869
|
+
const drawnFirst = linesOf(form.getState().values[name]);
|
|
870
|
+
/** A column whose values are drawn as pills (widget="badge"). */
|
|
871
|
+
const badged = (column) => !!node.cells?.[column]?.badge || node.cells?.[column]?.widget === 'badge';
|
|
872
|
+
/** Whether a column is as wide as its values, not a share of the width: yes/no, numbers, money, dates and badges, unless the grid shrinks to fit. */
|
|
873
|
+
const fixedWidth = (column) => (!!FIXED_CHARS[def.fields[column].type] || badged(column)) && node.fit !== 'shrink';
|
|
874
|
+
/** The room a column's values need, in pixels: its longest value, its kind's least, a short label — and the cell's padding. */
|
|
875
|
+
const roomOf = (column, lines = drawnFirst) => {
|
|
876
|
+
const sub = def.fields[column];
|
|
877
|
+
const least = FIXED_CHARS[sub.type] ?? 6;
|
|
878
|
+
let longest = sub.type === 'boolean' ? 0 : lines.reduce((most, line) => Math.max(most, (0, widgets_1.cellText)(sub, line.values[column], line.values, locale, form.getState().values, node.cells?.[column]).length), 0);
|
|
879
|
+
// A badge may turn to any of its choices: room for the longest, though no line holds it yet.
|
|
880
|
+
if (badged(column) && sub.type === 'selection')
|
|
881
|
+
longest = sub.options.reduce((most, option) => Math.max(most, option.label.length), longest);
|
|
882
|
+
// A long label over short numbers wraps onto a second line rather than widen its column; only its longest word sets a floor.
|
|
883
|
+
return Math.round(Math.min(Math.max(longest, least, labelWord(column)), 40) * 7.6 + 40) + (badged(column) ? BADGE_PADDING : 0);
|
|
884
|
+
};
|
|
885
|
+
/** Its label's longest word, in characters: a wrapping header breaks between words, never inside one. */
|
|
886
|
+
const labelWord = (column) => Math.min(Math.max(0, ...def.fields[column].label.split(/\s+/).map((word) => word.length)), 12);
|
|
887
|
+
/** The column a line is named by: its first of words (a name, a description, a link), else its first. */
|
|
888
|
+
const titleColumn = (shown) => shown.find((column) => WORDS.has(def.fields[column].type)) ?? shown[0];
|
|
889
|
+
/** A line's name by its place, "Line 3", when it has none to show. */
|
|
890
|
+
const placeName = (n) => {
|
|
891
|
+
const text = (0, core_1.fill)(labels.lineN, { n });
|
|
892
|
+
return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
893
|
+
};
|
|
765
894
|
const columnDefs = columns.map((column) => {
|
|
766
895
|
const sub = def.fields[column];
|
|
896
|
+
const room = roomOf(column);
|
|
767
897
|
const numeric = sub.type === 'integer' || sub.type === 'float' || sub.type === 'monetary';
|
|
768
898
|
// Words get the room; numbers and yes/no take what they need.
|
|
769
899
|
const wide = ['char', 'text', 'html', 'many2one', 'many2many', 'reference', 'selection'].includes(sub.type);
|
|
@@ -774,6 +904,10 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
774
904
|
const base = {
|
|
775
905
|
colId: column,
|
|
776
906
|
headerName: sub.label,
|
|
907
|
+
// A long label wraps onto a second line rather than be cut, so two columns never read the same; all of it on pointing at it.
|
|
908
|
+
headerTooltip: sub.label,
|
|
909
|
+
wrapHeaderText: true,
|
|
910
|
+
autoHeaderHeight: true,
|
|
777
911
|
valueGetter: (p) => {
|
|
778
912
|
if (p.node?.rowPinned) {
|
|
779
913
|
if (totals.includes(column))
|
|
@@ -803,15 +937,15 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
803
937
|
},
|
|
804
938
|
tooltipValueGetter: (p) => (p.node?.rowPinned ? undefined : problemAt(p.data, column, p.api)),
|
|
805
939
|
suppressKeyboardEvent: keys,
|
|
806
|
-
//
|
|
807
|
-
minWidth:
|
|
808
|
-
|
|
940
|
+
// Numbers, money and dates as wide as their values, never cut; words share the rest by the room they need, giving way when it is tight.
|
|
941
|
+
minWidth: fixedWidth(column) ? 48 : Math.max(96, Math.round(labelWord(column) * 7.6 + 40)),
|
|
942
|
+
...(fixedWidth(column) ? { width: room } : { flex: room }),
|
|
809
943
|
hide: optional[column] === 'hide',
|
|
810
944
|
...(node.cells?.[column]?.width ? { width: node.cells[column].width * 8 + 32, flex: 0 } : {}),
|
|
811
945
|
// Columns as wide as what they hold (fit: content), sized once the lines are drawn.
|
|
812
946
|
...(node.fit === 'content' && !node.cells?.[column]?.width ? { flex: undefined, minWidth: 64 } : {}),
|
|
813
947
|
// Columns that shrink to fit (fit: shrink), as Flectra's list beside a chatter: narrower before the table scrolls, headers wrapping.
|
|
814
|
-
...(node.fit === 'shrink' ? { minWidth: wide ? 72 : sub.type === 'monetary' ? 80 : 48
|
|
948
|
+
...(node.fit === 'shrink' ? { minWidth: wide ? 72 : sub.type === 'monetary' ? 80 : 48 } : {}),
|
|
815
949
|
editable: (p) => {
|
|
816
950
|
if (readonly || p.node.rowPinned)
|
|
817
951
|
return false;
|
|
@@ -914,6 +1048,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
914
1048
|
sortable: false,
|
|
915
1049
|
suppressMovable: true,
|
|
916
1050
|
lockPosition: lineStart,
|
|
1051
|
+
// Held in sight at the line's start, however far the lines scroll sideways.
|
|
1052
|
+
pinned: lineStart,
|
|
917
1053
|
suppressKeyboardEvent: keys,
|
|
918
1054
|
});
|
|
919
1055
|
}
|
|
@@ -957,6 +1093,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
957
1093
|
sortable: false,
|
|
958
1094
|
suppressMovable: true,
|
|
959
1095
|
lockPosition: lineStart,
|
|
1096
|
+
// Held in sight at the line's start, however far the lines scroll sideways.
|
|
1097
|
+
pinned: lineStart,
|
|
960
1098
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: PickRenderer, params: { picking } }),
|
|
961
1099
|
suppressKeyboardEvent: keys,
|
|
962
1100
|
});
|
|
@@ -990,6 +1128,7 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
990
1128
|
sortable: false,
|
|
991
1129
|
suppressMovable: true,
|
|
992
1130
|
lockPosition: lineEnd,
|
|
1131
|
+
pinned: lineEnd,
|
|
993
1132
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: CopyRenderer, params: { copy: copyLine, label: (n) => (0, core_1.fill)(labels.copy, { name: (0, core_1.fill)(labels.lineN, { n }) }) } }),
|
|
994
1133
|
suppressKeyboardEvent: keys,
|
|
995
1134
|
});
|
|
@@ -1009,6 +1148,7 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1009
1148
|
sortable: false,
|
|
1010
1149
|
suppressMovable: true,
|
|
1011
1150
|
lockPosition: lineEnd,
|
|
1151
|
+
pinned: lineEnd,
|
|
1012
1152
|
cellRendererSelector: (p) => p.node.rowPinned || kindOf(p.data)
|
|
1013
1153
|
? undefined
|
|
1014
1154
|
: { component: RowButtonsRenderer, params: { buttons: node.rowButtons, stateOf: (key) => lineStates.get(key), press: (id, key) => form.runRowAction(node.id, id, key) } },
|
|
@@ -1027,6 +1167,7 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1027
1167
|
sortable: false,
|
|
1028
1168
|
suppressMovable: true,
|
|
1029
1169
|
lockPosition: lineEnd,
|
|
1170
|
+
pinned: lineEnd,
|
|
1030
1171
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: OpenRenderer, params: { open: openLine, label: labels.openLine } }),
|
|
1031
1172
|
suppressKeyboardEvent: keys,
|
|
1032
1173
|
});
|
|
@@ -1036,6 +1177,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1036
1177
|
colId: '__delete',
|
|
1037
1178
|
headerName: '',
|
|
1038
1179
|
lockPosition: lineEnd,
|
|
1180
|
+
// A line's buttons held in sight at its end, however far the lines scroll sideways.
|
|
1181
|
+
pinned: lineEnd,
|
|
1039
1182
|
...(optionalIds.length ? { headerComponent: ChooserHeader, headerComponentParams: { chooser } } : {}),
|
|
1040
1183
|
width: 44,
|
|
1041
1184
|
minWidth: 44,
|
|
@@ -1047,6 +1190,11 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1047
1190
|
cellRendererSelector: (p) => (p.node.rowPinned ? undefined : { component: DeleteRenderer, params: { cell, deletable } }),
|
|
1048
1191
|
suppressKeyboardEvent: keys,
|
|
1049
1192
|
});
|
|
1193
|
+
// The buttons' words and the empty table's, as the plain table takes them from the node's options.
|
|
1194
|
+
const words = (key) => {
|
|
1195
|
+
const said = node.options?.[key];
|
|
1196
|
+
return typeof said === 'string' && said.trim() ? said : null;
|
|
1197
|
+
};
|
|
1050
1198
|
const api = (0, ag_grid_community_1.createGrid)(host, {
|
|
1051
1199
|
theme: ag_grid_community_1.themeQuartz.withParams({
|
|
1052
1200
|
fontFamily: 'var(--fd-font)',
|
|
@@ -1083,8 +1231,9 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1083
1231
|
// Lines have few columns: all of them are drawn.
|
|
1084
1232
|
suppressColumnVirtualisation: true,
|
|
1085
1233
|
readOnlyEdit: true,
|
|
1086
|
-
// An empty table
|
|
1087
|
-
|
|
1234
|
+
// An empty table says so in a short line where its rows would be, never a blank row.
|
|
1235
|
+
noRowsOverlayComponent: EmptyOverlay,
|
|
1236
|
+
noRowsOverlayComponentParams: { words: words('emptyLabel') ?? labels.noLines },
|
|
1088
1237
|
singleClickEdit: true,
|
|
1089
1238
|
// A page may ask for a whole line to open at once.
|
|
1090
1239
|
editType: node.editMode === 'row' ? 'fullRow' : undefined,
|
|
@@ -1094,10 +1243,13 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1094
1243
|
// A line dragged by its handle moves among the others as it goes; where
|
|
1095
1244
|
// it is let go, the form moves it too and numbers the lines again.
|
|
1096
1245
|
rowDragManaged: !!sequence,
|
|
1246
|
+
// A line dragged is named by its words (a yes or no names nothing), else by its place.
|
|
1097
1247
|
rowDragText: (p) => {
|
|
1098
1248
|
const line = p.rowNode?.data;
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1249
|
+
const column = titleColumn(columns.filter((c) => api.getColumn(c)?.isVisible() !== false));
|
|
1250
|
+
const value = line && (kindOf(line) && kinds ? line.values[kinds.text] : column ? line.values[column] : null);
|
|
1251
|
+
const text = value && typeof value === 'object' ? String(value.label ?? '') : String(value ?? '');
|
|
1252
|
+
return text.trim() || placeName((p.rowNode?.rowIndex ?? 0) + 1);
|
|
1101
1253
|
},
|
|
1102
1254
|
onRowDragEnd: (event) => {
|
|
1103
1255
|
const key = event.node.data?.key;
|
|
@@ -1128,7 +1280,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1128
1280
|
// A person's widths, order and choices are kept for the next visit. AG Grid
|
|
1129
1281
|
// tells of each change a moment after it; bringing a layout back is told too,
|
|
1130
1282
|
// and keeping it again changes nothing.
|
|
1131
|
-
|
|
1283
|
+
// A person widens a column by dragging its edge; widths the grid sets itself (by its lines) are not theirs.
|
|
1284
|
+
api.addEventListener('columnResized', (event) => event.finished && event.source !== 'api' && remember(event.source));
|
|
1132
1285
|
api.addEventListener('columnMoved', (event) => event.finished && remember(event.source));
|
|
1133
1286
|
api.addEventListener('columnVisible', (event) => remember(event.source));
|
|
1134
1287
|
/** Sizing the grid does by itself is not a choice to keep. */
|
|
@@ -1145,6 +1298,15 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1145
1298
|
const personHidden = new Set(optionalIds.filter((column) => optional[column] === 'hide'));
|
|
1146
1299
|
const hiddenByPerson = (column) => personHidden.has(column);
|
|
1147
1300
|
const saved = preferences?.get(columnsKey);
|
|
1301
|
+
/** The words' columns are sized by the lines once: when the first come, unless the person arranged the columns. */
|
|
1302
|
+
let sizedByLines = drawnFirst.length > 0 || Array.isArray(saved) || node.fit === 'content';
|
|
1303
|
+
function sizeByLines(lines) {
|
|
1304
|
+
if (sizedByLines || !lines.length)
|
|
1305
|
+
return;
|
|
1306
|
+
sizedByLines = true;
|
|
1307
|
+
const state = columns.filter((column) => !node.cells?.[column]?.width).map((colId) => (fixedWidth(colId) ? { colId, width: roomOf(colId, lines), flex: null } : { colId, flex: roomOf(colId, lines) }));
|
|
1308
|
+
api.applyColumnState({ state });
|
|
1309
|
+
}
|
|
1148
1310
|
if (Array.isArray(saved)) {
|
|
1149
1311
|
const known = new Set(api.getColumns()?.map((c) => c.getColId()));
|
|
1150
1312
|
const state = saved.filter((s) => typeof s?.colId === 'string' && known.has(s.colId));
|
|
@@ -1242,11 +1404,6 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1242
1404
|
button.addEventListener('click', () => addAndEdit(values));
|
|
1243
1405
|
adds.append(button);
|
|
1244
1406
|
};
|
|
1245
|
-
// The buttons' words, as the plain table takes them from the node's options.
|
|
1246
|
-
const words = (key) => {
|
|
1247
|
-
const said = node.options?.[key];
|
|
1248
|
-
return typeof said === 'string' && said.trim() ? said : null;
|
|
1249
|
-
};
|
|
1250
1407
|
addButton(words('addLabel') ?? labels.addLine, {}, 'line');
|
|
1251
1408
|
if (kinds) {
|
|
1252
1409
|
addButton(words('addSectionLabel') ?? labels.addSection, { [kinds.field]: kinds.section ?? 'section' }, 'section');
|
|
@@ -1288,7 +1445,8 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1288
1445
|
card.dataset['tone'] = state.tone;
|
|
1289
1446
|
card.classList.toggle('fd-line-bold', !!state?.bold);
|
|
1290
1447
|
const shownHere = shownColumns.filter((column) => !state?.cells[column]?.invisible);
|
|
1291
|
-
const
|
|
1448
|
+
const first = titleColumn(shownHere);
|
|
1449
|
+
const rest = shownHere.filter((column) => column !== first);
|
|
1292
1450
|
const text = (column) => {
|
|
1293
1451
|
// A distribution says its accounts by their names, as its cell does, once they are found.
|
|
1294
1452
|
const shares = distributions.get(column);
|
|
@@ -1301,7 +1459,7 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1301
1459
|
};
|
|
1302
1460
|
const title = document.createElement(dialogs ? 'button' : 'div');
|
|
1303
1461
|
title.className = 'fd-line-card-title';
|
|
1304
|
-
title.textContent = first ? text(first) || (
|
|
1462
|
+
title.textContent = first ? text(first) || placeName(current.indexOf(line) + 1) : '';
|
|
1305
1463
|
if (dialogs && title instanceof HTMLButtonElement) {
|
|
1306
1464
|
title.type = 'button';
|
|
1307
1465
|
title.addEventListener('click', () => void openLine(line));
|
|
@@ -1447,6 +1605,7 @@ const gridWidget = ({ form, name, field, node, id, document, labels = widgets_1.
|
|
|
1447
1605
|
// Columns as wide as what they hold, once more as the lines change.
|
|
1448
1606
|
if (node.fit === 'content')
|
|
1449
1607
|
api.autoSizeAllColumns();
|
|
1608
|
+
sizeByLines(linesOf(current));
|
|
1450
1609
|
// The ticks and copies name a line by where it is: it may have moved.
|
|
1451
1610
|
if (choosing || node.options?.['copy'] === true)
|
|
1452
1611
|
api.refreshCells({ columns: ['__pick', '__copy'], force: true, suppressFlash: true });
|