@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/src/lib/grid.mjs
CHANGED
|
@@ -329,6 +329,18 @@ class ChooserHeader {
|
|
|
329
329
|
return true;
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
|
+
/** What an empty grid says in its body: the page's words (`emptyLabel`), or a short line of its own. */
|
|
333
|
+
class EmptyOverlay {
|
|
334
|
+
line;
|
|
335
|
+
init(params) {
|
|
336
|
+
this.line = document.createElement('span');
|
|
337
|
+
this.line.className = 'fd-help fd-grid-empty';
|
|
338
|
+
this.line.textContent = params.words;
|
|
339
|
+
}
|
|
340
|
+
getGui() {
|
|
341
|
+
return this.line;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
332
344
|
/** A cell being edited: the field's own Fieldia widget, writing straight into its line. */
|
|
333
345
|
class FieldiaCellEditor {
|
|
334
346
|
box;
|
|
@@ -340,22 +352,22 @@ class FieldiaCellEditor {
|
|
|
340
352
|
/** The line field this editor writes: the column's, or a section's or note's text. */
|
|
341
353
|
column = '';
|
|
342
354
|
def;
|
|
343
|
-
/** A
|
|
344
|
-
|
|
355
|
+
/** A section or note being edited, not an item. */
|
|
356
|
+
kind = null;
|
|
345
357
|
note = null;
|
|
346
|
-
|
|
358
|
+
/** A tags box in a whole line open at once, which makes its line as tall as it needs. */
|
|
359
|
+
grows = false;
|
|
347
360
|
init(params) {
|
|
348
361
|
this.params = params;
|
|
349
362
|
// Not `column`: AG Grid's own params carry the column object under that name.
|
|
350
363
|
const { cell } = params;
|
|
351
364
|
const key = params.data.key;
|
|
352
|
-
const kind = cell.kindOf(params.data);
|
|
365
|
+
const kind = (this.kind = cell.kindOf(params.data));
|
|
353
366
|
const column = (this.column = kind && cell.kinds ? cell.kinds.text : params.subfield);
|
|
354
367
|
const def = (this.def = kind ? kindTextField(cell.defs[column], kind) : cell.defs[column]);
|
|
355
368
|
// A cell typed in its own way (hours as HH:MM, a per cent) is typed so in its editor too.
|
|
356
369
|
const look = kind ? undefined : cell.looks?.[column];
|
|
357
370
|
const node = { type: 'field', id: `${cell.fieldId}.${key}.${column}`, field: column, ...(look?.widget ? { widget: look.widget } : {}), ...(look?.options ? { options: look.options } : {}) };
|
|
358
|
-
this.calendar = (def.type === 'date' || def.type === 'datetime') && look?.options?.['weekNumbers'] === true;
|
|
359
371
|
const context = {
|
|
360
372
|
form: lineForm(cell.form, cell.field, key),
|
|
361
373
|
name: column,
|
|
@@ -375,15 +387,34 @@ class FieldiaCellEditor {
|
|
|
375
387
|
this.box.dataset['kind'] = kind;
|
|
376
388
|
this.box.setAttribute('aria-label', def.label);
|
|
377
389
|
this.box.append(this.widget.element);
|
|
390
|
+
// 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.
|
|
391
|
+
if (cell.rowMode && def.type === 'many2many') {
|
|
392
|
+
this.grows = true;
|
|
393
|
+
this.box.classList.add('fd-grid-editor-grows');
|
|
394
|
+
if (typeof ResizeObserver !== 'undefined')
|
|
395
|
+
new ResizeObserver(() => this.fitLine()).observe(this.box);
|
|
396
|
+
}
|
|
378
397
|
if (kind === 'note')
|
|
379
398
|
this.growing(this.box.querySelector('textarea'));
|
|
380
|
-
if (
|
|
381
|
-
this.
|
|
399
|
+
else if (def.type === 'text')
|
|
400
|
+
this.paragraph(this.box.querySelector('textarea'));
|
|
382
401
|
if (this.isPopup()) {
|
|
383
402
|
// AG Grid places a popup over its cell but leaves its size to the editor; a distribution's lines take the room they need.
|
|
384
403
|
const { width, height } = params.eGridCell.getBoundingClientRect();
|
|
385
404
|
if (def.type === 'json')
|
|
386
405
|
Object.assign(this.box.style, { minWidth: `${Math.max(width, 360)}px` });
|
|
406
|
+
// 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.
|
|
407
|
+
else if (def.type === 'many2many')
|
|
408
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 280)}px`, minHeight: `${height}px` });
|
|
409
|
+
// A paragraph is typed in a box of several lines, wide enough to read them, never spilling over the rows around.
|
|
410
|
+
else if (def.type === 'text')
|
|
411
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 320)}px`, minHeight: `${height}px` });
|
|
412
|
+
// A date is as wide as its day and time need (the stylesheet's max-content), its cell's width at the least.
|
|
413
|
+
else if (def.type === 'date' || def.type === 'datetime')
|
|
414
|
+
Object.assign(this.box.style, { minWidth: `${width}px`, height: `${height}px` });
|
|
415
|
+
// A link is searched by its name: wide enough to read one over a narrow column, on its row's line.
|
|
416
|
+
else if (def.type === 'many2one' || def.type === 'reference')
|
|
417
|
+
Object.assign(this.box.style, { minWidth: `${Math.max(width, 240)}px`, height: `${height}px` });
|
|
387
418
|
else
|
|
388
419
|
Object.assign(this.box.style, { width: `${width}px`, height: `${height}px` });
|
|
389
420
|
}
|
|
@@ -418,9 +449,11 @@ class FieldiaCellEditor {
|
|
|
418
449
|
this.box.addEventListener('focusin', (event) => {
|
|
419
450
|
const input = event.target;
|
|
420
451
|
if (!pointing && input instanceof HTMLInputElement && input.type !== 'checkbox' && input.type !== 'radio')
|
|
421
|
-
input
|
|
452
|
+
selectFromStart(input);
|
|
422
453
|
pointing = false;
|
|
423
454
|
});
|
|
455
|
+
// A value longer than its cell has room for takes the cell's padding too.
|
|
456
|
+
this.box.addEventListener('input', () => this.fit());
|
|
424
457
|
const show = () => {
|
|
425
458
|
const line = this.line();
|
|
426
459
|
// A cell the form found wrong stays marked while it is fixed.
|
|
@@ -446,39 +479,65 @@ class FieldiaCellEditor {
|
|
|
446
479
|
});
|
|
447
480
|
// Enter makes a new line; Ctrl/Cmd+Enter finishes the note.
|
|
448
481
|
area.addEventListener('keydown', (event) => {
|
|
449
|
-
if (event.key !== 'Enter' ||
|
|
482
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey)
|
|
450
483
|
return;
|
|
451
|
-
|
|
484
|
+
// The popup's own handler hears the key as well as the cell's (whose suppressKeyboardEvent leaves Enter alone).
|
|
452
485
|
_stopPropagationForAgGrid(event);
|
|
486
|
+
if (!(event.ctrlKey || event.metaKey))
|
|
487
|
+
return;
|
|
488
|
+
event.preventDefault();
|
|
453
489
|
this.params.api.stopEditing();
|
|
454
490
|
});
|
|
455
491
|
}
|
|
456
492
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
493
|
+
* A paragraph (a text field) is typed in a box of several lines over its
|
|
494
|
+
* cell: Enter makes a new line there (the grid leaves it alone), and
|
|
495
|
+
* Ctrl/Cmd+Enter finishes it, as a note's does.
|
|
459
496
|
*/
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!list)
|
|
497
|
+
paragraph(area) {
|
|
498
|
+
if (!area)
|
|
463
499
|
return;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
layer.append(list);
|
|
467
|
-
const place = () => {
|
|
468
|
-
if (list.hidden || !this.box.isConnected)
|
|
500
|
+
area.addEventListener('keydown', (event) => {
|
|
501
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey)
|
|
469
502
|
return;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
503
|
+
// The popup's own handler hears the key as well as the cell's (whose suppressKeyboardEvent leaves Enter alone).
|
|
504
|
+
_stopPropagationForAgGrid(event);
|
|
505
|
+
if (!(event.ctrlKey || event.metaKey))
|
|
506
|
+
return;
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
this.params.api.stopEditing();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/** A value wider than the room inside the cell's padding takes the padding too, so more of it shows. */
|
|
512
|
+
fit() {
|
|
513
|
+
const input = this.box.querySelector('input:not([type="checkbox"]):not([type="radio"])');
|
|
514
|
+
if (!input)
|
|
515
|
+
return;
|
|
516
|
+
if (this.isPopup()) {
|
|
517
|
+
// A link's name longer than its box widens the box to show it whole, as far as the window allows.
|
|
518
|
+
const short = input.scrollWidth - input.clientWidth;
|
|
519
|
+
if ((this.def.type === 'many2one' || this.def.type === 'reference') && short > 0) {
|
|
520
|
+
const { left, width } = this.box.getBoundingClientRect();
|
|
521
|
+
const room = this.box.ownerDocument.defaultView.innerWidth - left - 16;
|
|
522
|
+
this.box.style.minWidth = `${Math.round(Math.max(width, Math.min(width + short + 8, LINK_EDITOR_MOST, room)))}px`;
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (input.scrollWidth > input.clientWidth)
|
|
527
|
+
this.box.classList.add('fd-grid-editor-tight');
|
|
528
|
+
}
|
|
529
|
+
/** A line as tall as its tags box needs, never shorter than a line. */
|
|
530
|
+
fitLine() {
|
|
531
|
+
const { node, api } = this.params;
|
|
532
|
+
if (!this.box.isConnected || api.isDestroyed())
|
|
533
|
+
return;
|
|
534
|
+
const height = Math.max(ROW_HEIGHT, this.box.scrollHeight + 2);
|
|
535
|
+
if (height <= (node.rowHeight ?? 0))
|
|
536
|
+
return;
|
|
537
|
+
node.setRowHeight(height);
|
|
538
|
+
api.onRowHeightChanged();
|
|
481
539
|
}
|
|
540
|
+
/** A note's box as tall as its text, and its row with it. */
|
|
482
541
|
resize() {
|
|
483
542
|
const area = this.note;
|
|
484
543
|
if (!area)
|
|
@@ -510,6 +569,9 @@ class FieldiaCellEditor {
|
|
|
510
569
|
return this.box;
|
|
511
570
|
}
|
|
512
571
|
afterGuiAttached() {
|
|
572
|
+
this.fit();
|
|
573
|
+
if (this.grows)
|
|
574
|
+
this.fitLine();
|
|
513
575
|
// With a whole line open, every editor is attached: only the one clicked takes the focus.
|
|
514
576
|
if (this.params.cellStartedEdit === false)
|
|
515
577
|
return;
|
|
@@ -521,28 +583,34 @@ class FieldiaCellEditor {
|
|
|
521
583
|
}
|
|
522
584
|
const input = this.box.querySelector('input');
|
|
523
585
|
if (input && input.type !== 'checkbox' && document.activeElement === input)
|
|
524
|
-
input
|
|
586
|
+
selectFromStart(input);
|
|
525
587
|
}
|
|
526
588
|
getValue() {
|
|
527
589
|
return this.line()?.values[this.column];
|
|
528
590
|
}
|
|
529
|
-
/**
|
|
591
|
+
/**
|
|
592
|
+
* Lists that open below their input need room the cell does not have (a whole open line floats them instead); so do a
|
|
593
|
+
* distribution's lines, a date wider than its cell, and a paragraph's several lines (a note's grow in its own row).
|
|
594
|
+
*/
|
|
530
595
|
isPopup() {
|
|
531
596
|
if (this.params.cell.rowMode)
|
|
532
597
|
return false;
|
|
533
598
|
const type = this.def.type;
|
|
534
|
-
return type === 'many2one' || type === 'many2many' || type === 'reference' || type === 'json' || this.
|
|
599
|
+
return type === 'many2one' || type === 'many2many' || type === 'reference' || type === 'json' || type === 'date' || type === 'datetime' || (type === 'text' && !this.kind);
|
|
535
600
|
}
|
|
536
601
|
getPopupPosition() {
|
|
537
602
|
return 'over';
|
|
538
603
|
}
|
|
539
604
|
destroy() {
|
|
540
605
|
this.leave();
|
|
541
|
-
this.floating?.watch.disconnect();
|
|
542
|
-
this.floating?.list.remove();
|
|
543
606
|
this.widget.destroy?.();
|
|
544
607
|
const { node, api, cell, data } = this.params;
|
|
545
608
|
cell.finish(data.key);
|
|
609
|
+
// The line closed: it gives back the height its tags box took.
|
|
610
|
+
if (this.grows && !api.isDestroyed()) {
|
|
611
|
+
node.setRowHeight(null);
|
|
612
|
+
api.onRowHeightChanged();
|
|
613
|
+
}
|
|
546
614
|
// A note's row grew by hand while it was typed; drawing it afresh measures
|
|
547
615
|
// it again from its text (resetting the height would make it one line tall).
|
|
548
616
|
if (this.note) {
|
|
@@ -553,12 +621,34 @@ class FieldiaCellEditor {
|
|
|
553
621
|
}
|
|
554
622
|
}
|
|
555
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* A box's text selected so typing replaces it, shown from its start: a
|
|
626
|
+
* browser scrolls a long value to its end as it is selected, and its start is
|
|
627
|
+
* what reads. (A date's own input has no text selection to set.)
|
|
628
|
+
*/
|
|
629
|
+
function selectFromStart(input) {
|
|
630
|
+
try {
|
|
631
|
+
input.setSelectionRange(0, input.value.length, 'backward');
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
input.select();
|
|
635
|
+
}
|
|
636
|
+
input.scrollLeft = 0;
|
|
637
|
+
}
|
|
556
638
|
// ---- the widget -------------------------------------------------------------------
|
|
557
639
|
/** Past this many lines a table stops growing and scrolls inside, drawing only the rows in view. */
|
|
558
640
|
const LONG_TABLE = 15;
|
|
559
641
|
const ROW_HEIGHT = 40;
|
|
560
642
|
const HEADER_HEIGHT = 38;
|
|
561
643
|
const EDITABLE = new Set(['char', 'text', 'html', 'integer', 'float', 'monetary', 'date', 'datetime', 'selection', 'many2one', 'many2many', 'reference']);
|
|
644
|
+
/** A badge's pill round its words, both sides: room its column keeps besides theirs. */
|
|
645
|
+
const BADGE_PADDING = 20;
|
|
646
|
+
/** The kinds of field that hold words: a line is named by the first of them. */
|
|
647
|
+
const WORDS = new Set(['char', 'text', 'html', 'many2one', 'reference']);
|
|
648
|
+
/** The kinds of field whose column is as wide as its values need, in characters, as Flectra's lists size them. */
|
|
649
|
+
/** The widest a link's editor grows to show a long name, in pixels. */
|
|
650
|
+
const LINK_EDITOR_MOST = 480;
|
|
651
|
+
const FIXED_CHARS = { boolean: 4, integer: 4, float: 6, monetary: 14, date: 11, datetime: 17 };
|
|
562
652
|
export const gridWidget = ({ form, name, field, node, id, document, labels = WIDGET_LABELS.en, preferences, locale, dialogs }) => {
|
|
563
653
|
const def = field;
|
|
564
654
|
const kinds = def.lineKinds;
|
|
@@ -621,6 +711,15 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
621
711
|
element.setAttribute('role', 'group');
|
|
622
712
|
const host = document.createElement('div');
|
|
623
713
|
host.className = 'fd-grid-host';
|
|
714
|
+
// 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
|
|
715
|
+
// scroll a cell cut by the screen's edge into view, moving the tick from under the pointer, and the click would
|
|
716
|
+
// land on the cell instead. (A button in a cell the grid leaves alone.) The tick still takes the focus and the click.
|
|
717
|
+
for (const type of ['pointerdown', 'mousedown']) {
|
|
718
|
+
host.addEventListener(type, (event) => {
|
|
719
|
+
if (event.target.closest?.('.ag-cell input[type="checkbox"]'))
|
|
720
|
+
event.stopPropagation();
|
|
721
|
+
}, true);
|
|
722
|
+
}
|
|
624
723
|
// What is wrong on which line, once a save has been refused.
|
|
625
724
|
const problems = document.createElement('div');
|
|
626
725
|
problems.className = 'fd-error fd-grid-problems';
|
|
@@ -730,6 +829,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
730
829
|
api.setFocusedCell(index, column);
|
|
731
830
|
api.startEditingCell({ rowIndex: index, colKey: column });
|
|
732
831
|
};
|
|
832
|
+
/** A paragraph (a text field) of an item, edited in its box of several lines over the cell. */
|
|
833
|
+
const paragraphCell = (line, column) => !cell.rowMode && !kindOf(line) && def.fields[column]?.type === 'text';
|
|
733
834
|
/**
|
|
734
835
|
* Keys the grid must leave alone, and the two that run off the end of the
|
|
735
836
|
* table while editing: Enter on the last line and Tab on its last editable
|
|
@@ -741,8 +842,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
741
842
|
// Alt+Up/Down moves the line (below), it does not move the focus.
|
|
742
843
|
if (!editing && event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown'))
|
|
743
844
|
return true;
|
|
744
|
-
// In a note, Enter is a new line
|
|
745
|
-
if (editing && event.key === 'Enter' && !event.ctrlKey && !event.metaKey && kindOf(node.data) === 'note')
|
|
845
|
+
// In a note, or a paragraph opened over its cell, Enter is a new line (Ctrl/Cmd+Enter finishes it).
|
|
846
|
+
if (editing && event.key === 'Enter' && !event.ctrlKey && !event.metaKey && (kindOf(node.data) === 'note' || paragraphCell(node.data, column.getColId())))
|
|
746
847
|
return true;
|
|
747
848
|
if (!editing || event.type !== 'keydown' || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey)
|
|
748
849
|
return false;
|
|
@@ -758,8 +859,37 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
758
859
|
}
|
|
759
860
|
return false;
|
|
760
861
|
};
|
|
862
|
+
// Each column's share of the width by what it holds, as Flectra's lists size theirs: yes/no, numbers and dates by
|
|
863
|
+
// their kind, words and links by the longest the lines have — a long name gets the room a short code does not need.
|
|
864
|
+
const linesOf = (value) => (value ?? []).filter((line) => !kindOf(line));
|
|
865
|
+
const drawnFirst = linesOf(form.getState().values[name]);
|
|
866
|
+
/** A column whose values are drawn as pills (widget="badge"). */
|
|
867
|
+
const badged = (column) => !!node.cells?.[column]?.badge || node.cells?.[column]?.widget === 'badge';
|
|
868
|
+
/** 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. */
|
|
869
|
+
const fixedWidth = (column) => (!!FIXED_CHARS[def.fields[column].type] || badged(column)) && node.fit !== 'shrink';
|
|
870
|
+
/** The room a column's values need, in pixels: its longest value, its kind's least, a short label — and the cell's padding. */
|
|
871
|
+
const roomOf = (column, lines = drawnFirst) => {
|
|
872
|
+
const sub = def.fields[column];
|
|
873
|
+
const least = FIXED_CHARS[sub.type] ?? 6;
|
|
874
|
+
let longest = sub.type === 'boolean' ? 0 : lines.reduce((most, line) => Math.max(most, cellText(sub, line.values[column], line.values, locale, form.getState().values, node.cells?.[column]).length), 0);
|
|
875
|
+
// A badge may turn to any of its choices: room for the longest, though no line holds it yet.
|
|
876
|
+
if (badged(column) && sub.type === 'selection')
|
|
877
|
+
longest = sub.options.reduce((most, option) => Math.max(most, option.label.length), longest);
|
|
878
|
+
// A long label over short numbers wraps onto a second line rather than widen its column; only its longest word sets a floor.
|
|
879
|
+
return Math.round(Math.min(Math.max(longest, least, labelWord(column)), 40) * 7.6 + 40) + (badged(column) ? BADGE_PADDING : 0);
|
|
880
|
+
};
|
|
881
|
+
/** Its label's longest word, in characters: a wrapping header breaks between words, never inside one. */
|
|
882
|
+
const labelWord = (column) => Math.min(Math.max(0, ...def.fields[column].label.split(/\s+/).map((word) => word.length)), 12);
|
|
883
|
+
/** The column a line is named by: its first of words (a name, a description, a link), else its first. */
|
|
884
|
+
const titleColumn = (shown) => shown.find((column) => WORDS.has(def.fields[column].type)) ?? shown[0];
|
|
885
|
+
/** A line's name by its place, "Line 3", when it has none to show. */
|
|
886
|
+
const placeName = (n) => {
|
|
887
|
+
const text = fill(labels.lineN, { n });
|
|
888
|
+
return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
889
|
+
};
|
|
761
890
|
const columnDefs = columns.map((column) => {
|
|
762
891
|
const sub = def.fields[column];
|
|
892
|
+
const room = roomOf(column);
|
|
763
893
|
const numeric = sub.type === 'integer' || sub.type === 'float' || sub.type === 'monetary';
|
|
764
894
|
// Words get the room; numbers and yes/no take what they need.
|
|
765
895
|
const wide = ['char', 'text', 'html', 'many2one', 'many2many', 'reference', 'selection'].includes(sub.type);
|
|
@@ -770,6 +900,10 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
770
900
|
const base = {
|
|
771
901
|
colId: column,
|
|
772
902
|
headerName: sub.label,
|
|
903
|
+
// 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.
|
|
904
|
+
headerTooltip: sub.label,
|
|
905
|
+
wrapHeaderText: true,
|
|
906
|
+
autoHeaderHeight: true,
|
|
773
907
|
valueGetter: (p) => {
|
|
774
908
|
if (p.node?.rowPinned) {
|
|
775
909
|
if (totals.includes(column))
|
|
@@ -799,15 +933,15 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
799
933
|
},
|
|
800
934
|
tooltipValueGetter: (p) => (p.node?.rowPinned ? undefined : problemAt(p.data, column, p.api)),
|
|
801
935
|
suppressKeyboardEvent: keys,
|
|
802
|
-
//
|
|
803
|
-
minWidth:
|
|
804
|
-
|
|
936
|
+
// 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.
|
|
937
|
+
minWidth: fixedWidth(column) ? 48 : Math.max(96, Math.round(labelWord(column) * 7.6 + 40)),
|
|
938
|
+
...(fixedWidth(column) ? { width: room } : { flex: room }),
|
|
805
939
|
hide: optional[column] === 'hide',
|
|
806
940
|
...(node.cells?.[column]?.width ? { width: node.cells[column].width * 8 + 32, flex: 0 } : {}),
|
|
807
941
|
// Columns as wide as what they hold (fit: content), sized once the lines are drawn.
|
|
808
942
|
...(node.fit === 'content' && !node.cells?.[column]?.width ? { flex: undefined, minWidth: 64 } : {}),
|
|
809
943
|
// Columns that shrink to fit (fit: shrink), as Flectra's list beside a chatter: narrower before the table scrolls, headers wrapping.
|
|
810
|
-
...(node.fit === 'shrink' ? { minWidth: wide ? 72 : sub.type === 'monetary' ? 80 : 48
|
|
944
|
+
...(node.fit === 'shrink' ? { minWidth: wide ? 72 : sub.type === 'monetary' ? 80 : 48 } : {}),
|
|
811
945
|
editable: (p) => {
|
|
812
946
|
if (readonly || p.node.rowPinned)
|
|
813
947
|
return false;
|
|
@@ -910,6 +1044,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
910
1044
|
sortable: false,
|
|
911
1045
|
suppressMovable: true,
|
|
912
1046
|
lockPosition: lineStart,
|
|
1047
|
+
// Held in sight at the line's start, however far the lines scroll sideways.
|
|
1048
|
+
pinned: lineStart,
|
|
913
1049
|
suppressKeyboardEvent: keys,
|
|
914
1050
|
});
|
|
915
1051
|
}
|
|
@@ -953,6 +1089,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
953
1089
|
sortable: false,
|
|
954
1090
|
suppressMovable: true,
|
|
955
1091
|
lockPosition: lineStart,
|
|
1092
|
+
// Held in sight at the line's start, however far the lines scroll sideways.
|
|
1093
|
+
pinned: lineStart,
|
|
956
1094
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: PickRenderer, params: { picking } }),
|
|
957
1095
|
suppressKeyboardEvent: keys,
|
|
958
1096
|
});
|
|
@@ -986,6 +1124,7 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
986
1124
|
sortable: false,
|
|
987
1125
|
suppressMovable: true,
|
|
988
1126
|
lockPosition: lineEnd,
|
|
1127
|
+
pinned: lineEnd,
|
|
989
1128
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: CopyRenderer, params: { copy: copyLine, label: (n) => fill(labels.copy, { name: fill(labels.lineN, { n }) }) } }),
|
|
990
1129
|
suppressKeyboardEvent: keys,
|
|
991
1130
|
});
|
|
@@ -1005,6 +1144,7 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1005
1144
|
sortable: false,
|
|
1006
1145
|
suppressMovable: true,
|
|
1007
1146
|
lockPosition: lineEnd,
|
|
1147
|
+
pinned: lineEnd,
|
|
1008
1148
|
cellRendererSelector: (p) => p.node.rowPinned || kindOf(p.data)
|
|
1009
1149
|
? undefined
|
|
1010
1150
|
: { component: RowButtonsRenderer, params: { buttons: node.rowButtons, stateOf: (key) => lineStates.get(key), press: (id, key) => form.runRowAction(node.id, id, key) } },
|
|
@@ -1023,6 +1163,7 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1023
1163
|
sortable: false,
|
|
1024
1164
|
suppressMovable: true,
|
|
1025
1165
|
lockPosition: lineEnd,
|
|
1166
|
+
pinned: lineEnd,
|
|
1026
1167
|
cellRendererSelector: (p) => (p.node.rowPinned || kindOf(p.data) ? undefined : { component: OpenRenderer, params: { open: openLine, label: labels.openLine } }),
|
|
1027
1168
|
suppressKeyboardEvent: keys,
|
|
1028
1169
|
});
|
|
@@ -1032,6 +1173,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1032
1173
|
colId: '__delete',
|
|
1033
1174
|
headerName: '',
|
|
1034
1175
|
lockPosition: lineEnd,
|
|
1176
|
+
// A line's buttons held in sight at its end, however far the lines scroll sideways.
|
|
1177
|
+
pinned: lineEnd,
|
|
1035
1178
|
...(optionalIds.length ? { headerComponent: ChooserHeader, headerComponentParams: { chooser } } : {}),
|
|
1036
1179
|
width: 44,
|
|
1037
1180
|
minWidth: 44,
|
|
@@ -1043,6 +1186,11 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1043
1186
|
cellRendererSelector: (p) => (p.node.rowPinned ? undefined : { component: DeleteRenderer, params: { cell, deletable } }),
|
|
1044
1187
|
suppressKeyboardEvent: keys,
|
|
1045
1188
|
});
|
|
1189
|
+
// The buttons' words and the empty table's, as the plain table takes them from the node's options.
|
|
1190
|
+
const words = (key) => {
|
|
1191
|
+
const said = node.options?.[key];
|
|
1192
|
+
return typeof said === 'string' && said.trim() ? said : null;
|
|
1193
|
+
};
|
|
1046
1194
|
const api = createGrid(host, {
|
|
1047
1195
|
theme: themeQuartz.withParams({
|
|
1048
1196
|
fontFamily: 'var(--fd-font)',
|
|
@@ -1079,8 +1227,9 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1079
1227
|
// Lines have few columns: all of them are drawn.
|
|
1080
1228
|
suppressColumnVirtualisation: true,
|
|
1081
1229
|
readOnlyEdit: true,
|
|
1082
|
-
// An empty table
|
|
1083
|
-
|
|
1230
|
+
// An empty table says so in a short line where its rows would be, never a blank row.
|
|
1231
|
+
noRowsOverlayComponent: EmptyOverlay,
|
|
1232
|
+
noRowsOverlayComponentParams: { words: words('emptyLabel') ?? labels.noLines },
|
|
1084
1233
|
singleClickEdit: true,
|
|
1085
1234
|
// A page may ask for a whole line to open at once.
|
|
1086
1235
|
editType: node.editMode === 'row' ? 'fullRow' : undefined,
|
|
@@ -1090,10 +1239,13 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1090
1239
|
// A line dragged by its handle moves among the others as it goes; where
|
|
1091
1240
|
// it is let go, the form moves it too and numbers the lines again.
|
|
1092
1241
|
rowDragManaged: !!sequence,
|
|
1242
|
+
// A line dragged is named by its words (a yes or no names nothing), else by its place.
|
|
1093
1243
|
rowDragText: (p) => {
|
|
1094
1244
|
const line = p.rowNode?.data;
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1245
|
+
const column = titleColumn(columns.filter((c) => api.getColumn(c)?.isVisible() !== false));
|
|
1246
|
+
const value = line && (kindOf(line) && kinds ? line.values[kinds.text] : column ? line.values[column] : null);
|
|
1247
|
+
const text = value && typeof value === 'object' ? String(value.label ?? '') : String(value ?? '');
|
|
1248
|
+
return text.trim() || placeName((p.rowNode?.rowIndex ?? 0) + 1);
|
|
1097
1249
|
},
|
|
1098
1250
|
onRowDragEnd: (event) => {
|
|
1099
1251
|
const key = event.node.data?.key;
|
|
@@ -1124,7 +1276,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1124
1276
|
// A person's widths, order and choices are kept for the next visit. AG Grid
|
|
1125
1277
|
// tells of each change a moment after it; bringing a layout back is told too,
|
|
1126
1278
|
// and keeping it again changes nothing.
|
|
1127
|
-
|
|
1279
|
+
// A person widens a column by dragging its edge; widths the grid sets itself (by its lines) are not theirs.
|
|
1280
|
+
api.addEventListener('columnResized', (event) => event.finished && event.source !== 'api' && remember(event.source));
|
|
1128
1281
|
api.addEventListener('columnMoved', (event) => event.finished && remember(event.source));
|
|
1129
1282
|
api.addEventListener('columnVisible', (event) => remember(event.source));
|
|
1130
1283
|
/** Sizing the grid does by itself is not a choice to keep. */
|
|
@@ -1141,6 +1294,15 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1141
1294
|
const personHidden = new Set(optionalIds.filter((column) => optional[column] === 'hide'));
|
|
1142
1295
|
const hiddenByPerson = (column) => personHidden.has(column);
|
|
1143
1296
|
const saved = preferences?.get(columnsKey);
|
|
1297
|
+
/** The words' columns are sized by the lines once: when the first come, unless the person arranged the columns. */
|
|
1298
|
+
let sizedByLines = drawnFirst.length > 0 || Array.isArray(saved) || node.fit === 'content';
|
|
1299
|
+
function sizeByLines(lines) {
|
|
1300
|
+
if (sizedByLines || !lines.length)
|
|
1301
|
+
return;
|
|
1302
|
+
sizedByLines = true;
|
|
1303
|
+
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) }));
|
|
1304
|
+
api.applyColumnState({ state });
|
|
1305
|
+
}
|
|
1144
1306
|
if (Array.isArray(saved)) {
|
|
1145
1307
|
const known = new Set(api.getColumns()?.map((c) => c.getColId()));
|
|
1146
1308
|
const state = saved.filter((s) => typeof s?.colId === 'string' && known.has(s.colId));
|
|
@@ -1238,11 +1400,6 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1238
1400
|
button.addEventListener('click', () => addAndEdit(values));
|
|
1239
1401
|
adds.append(button);
|
|
1240
1402
|
};
|
|
1241
|
-
// The buttons' words, as the plain table takes them from the node's options.
|
|
1242
|
-
const words = (key) => {
|
|
1243
|
-
const said = node.options?.[key];
|
|
1244
|
-
return typeof said === 'string' && said.trim() ? said : null;
|
|
1245
|
-
};
|
|
1246
1403
|
addButton(words('addLabel') ?? labels.addLine, {}, 'line');
|
|
1247
1404
|
if (kinds) {
|
|
1248
1405
|
addButton(words('addSectionLabel') ?? labels.addSection, { [kinds.field]: kinds.section ?? 'section' }, 'section');
|
|
@@ -1284,7 +1441,8 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1284
1441
|
card.dataset['tone'] = state.tone;
|
|
1285
1442
|
card.classList.toggle('fd-line-bold', !!state?.bold);
|
|
1286
1443
|
const shownHere = shownColumns.filter((column) => !state?.cells[column]?.invisible);
|
|
1287
|
-
const
|
|
1444
|
+
const first = titleColumn(shownHere);
|
|
1445
|
+
const rest = shownHere.filter((column) => column !== first);
|
|
1288
1446
|
const text = (column) => {
|
|
1289
1447
|
// A distribution says its accounts by their names, as its cell does, once they are found.
|
|
1290
1448
|
const shares = distributions.get(column);
|
|
@@ -1297,7 +1455,7 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1297
1455
|
};
|
|
1298
1456
|
const title = document.createElement(dialogs ? 'button' : 'div');
|
|
1299
1457
|
title.className = 'fd-line-card-title';
|
|
1300
|
-
title.textContent = first ? text(first) ||
|
|
1458
|
+
title.textContent = first ? text(first) || placeName(current.indexOf(line) + 1) : '';
|
|
1301
1459
|
if (dialogs && title instanceof HTMLButtonElement) {
|
|
1302
1460
|
title.type = 'button';
|
|
1303
1461
|
title.addEventListener('click', () => void openLine(line));
|
|
@@ -1443,6 +1601,7 @@ export const gridWidget = ({ form, name, field, node, id, document, labels = WID
|
|
|
1443
1601
|
// Columns as wide as what they hold, once more as the lines change.
|
|
1444
1602
|
if (node.fit === 'content')
|
|
1445
1603
|
api.autoSizeAllColumns();
|
|
1604
|
+
sizeByLines(linesOf(current));
|
|
1446
1605
|
// The ticks and copies name a line by where it is: it may have moved.
|
|
1447
1606
|
if (choosing || node.options?.['copy'] === true)
|
|
1448
1607
|
api.refreshCells({ columns: ['__pick', '__copy'], force: true, suppressFlash: true });
|