@navette-js/infolists 1.0.0-beta.1 → 1.0.0-beta.2
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/README.md +2 -2
- package/lib/index.d.ts +30 -29
- package/lib/index.js +41 -41
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @navette-js/infolists
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only infolist entries for Navette admin panels.
|
|
4
4
|
|
|
5
|
-
Part of [Navette](https://github.com/NavetteJS/navette),
|
|
5
|
+
Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
|
|
6
6
|
|
|
7
7
|
**Beta:** the API may still change between beta releases.
|
|
8
8
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component, type ComponentContext } from '@navette-js/schemas';
|
|
2
2
|
import { type ColorManager } from '@navette-js/support';
|
|
3
3
|
export declare const useColorManager: (manager: ColorManager) => void;
|
|
4
|
-
/** Base
|
|
4
|
+
/** Base entry: a label and a value read from the record (dotted paths allowed) or computed. */
|
|
5
5
|
declare abstract class Entry extends Component {
|
|
6
6
|
protected _label: string | null;
|
|
7
7
|
protected _state: ((ctx: ComponentContext) => unknown | Promise<unknown>) | null;
|
|
@@ -9,16 +9,17 @@ declare abstract class Entry extends Component {
|
|
|
9
9
|
hasStatePath(): boolean;
|
|
10
10
|
label(label: string): this;
|
|
11
11
|
protected _hiddenLabel: boolean;
|
|
12
|
-
/**
|
|
12
|
+
/** Keeps the label for screen readers only. */
|
|
13
13
|
hiddenLabel(condition?: boolean): this;
|
|
14
|
-
/**
|
|
14
|
+
/** Computed value; without it, the record attribute of the same name is shown. */
|
|
15
15
|
state(resolver: (ctx: ComponentContext) => unknown | Promise<unknown>): this;
|
|
16
16
|
protected getState(): Promise<unknown>;
|
|
17
|
-
/**
|
|
17
|
+
/** Wraps the content with the entry label. */
|
|
18
18
|
protected wrap(content: string): string;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Sanitizer applied to HTML an entry renders as is (`markdown()`). Without one, the content is escaped. */
|
|
21
21
|
export declare const useHtmlSanitizer: (sanitizer: ((html: string) => string) | null) => void;
|
|
22
|
+
/** Read-only text: badge, color, icon, money, date, placeholder. */
|
|
22
23
|
export declare class TextEntry extends Entry {
|
|
23
24
|
private _badge;
|
|
24
25
|
private _color;
|
|
@@ -46,48 +47,48 @@ export declare class TextEntry extends Entry {
|
|
|
46
47
|
suffix(text: string | null): this;
|
|
47
48
|
private _prose;
|
|
48
49
|
private _markdown;
|
|
49
|
-
/**
|
|
50
|
+
/** Typographic styling of the content (`fi-prose`). */
|
|
50
51
|
prose(condition?: boolean): this;
|
|
51
52
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* Rich text content, rendered as HTML after the sanitizer set with `useHtmlSanitizer()`.
|
|
54
|
+
* Existing HTML is kept (sanitized) and plain text becomes paragraphs; Markdown syntax itself is not converted.
|
|
54
55
|
*/
|
|
55
56
|
markdown(condition?: boolean): this;
|
|
56
57
|
private _copyable;
|
|
57
58
|
private _numeric;
|
|
58
|
-
/**
|
|
59
|
+
/** Clicking copies the value to the clipboard. */
|
|
59
60
|
copyable(condition?: boolean): this;
|
|
60
|
-
/**
|
|
61
|
+
/** Formatted number (thousands separators), aligned as a number. */
|
|
61
62
|
numeric(condition?: boolean): this;
|
|
62
63
|
toHtml(): Promise<string>;
|
|
63
64
|
}
|
|
64
|
-
/**
|
|
65
|
+
/** Color swatch: only hex or simple functional colors are injected into the style attribute. */
|
|
65
66
|
export declare class ColorEntry extends Entry {
|
|
66
67
|
private _placeholder;
|
|
67
68
|
static make(name: string): ColorEntry;
|
|
68
69
|
placeholder(text: string | null): this;
|
|
69
70
|
toHtml(): Promise<string>;
|
|
70
71
|
}
|
|
71
|
-
/**
|
|
72
|
+
/** Read-only image: only same-site (`/…`) or http(s) URLs are rendered. */
|
|
72
73
|
export declare class ImageEntry extends Entry {
|
|
73
74
|
private _height;
|
|
74
75
|
private _alt;
|
|
75
76
|
private _circular;
|
|
76
77
|
static make(name: string): ImageEntry;
|
|
77
|
-
/**
|
|
78
|
+
/** CSS height of the image (`8rem` by default). */
|
|
78
79
|
imageHeight(height: string): this;
|
|
79
80
|
alt(text: string | null): this;
|
|
80
81
|
circular(condition?: boolean): this;
|
|
81
82
|
toHtml(): Promise<string>;
|
|
82
83
|
}
|
|
83
|
-
/**
|
|
84
|
+
/** Icon based on the value; `boolean()` shows a green check or a red cross. */
|
|
84
85
|
export declare class IconEntry extends Entry {
|
|
85
86
|
private _boolean;
|
|
86
87
|
static make(name: string): IconEntry;
|
|
87
88
|
boolean(condition?: boolean): this;
|
|
88
89
|
toHtml(): Promise<string>;
|
|
89
90
|
}
|
|
90
|
-
/**
|
|
91
|
+
/** Key/value pairs as a table: the state is an object (or its JSON form); a non-scalar value is shown as JSON. */
|
|
91
92
|
export declare class KeyValueEntry extends Entry {
|
|
92
93
|
private _keyLabel;
|
|
93
94
|
private _valueLabel;
|
|
@@ -95,15 +96,15 @@ export declare class KeyValueEntry extends Entry {
|
|
|
95
96
|
static make(name: string): KeyValueEntry;
|
|
96
97
|
keyLabel(label: string | null): this;
|
|
97
98
|
valueLabel(label: string | null): this;
|
|
98
|
-
/**
|
|
99
|
+
/** Text shown when there are no pairs ("No entries" by default). */
|
|
99
100
|
placeholder(text: string | null): this;
|
|
100
|
-
/** @deprecated
|
|
101
|
+
/** @deprecated Use `placeholder()`. */
|
|
101
102
|
emptyMessage(text: string | null): this;
|
|
102
103
|
toHtml(): Promise<string>;
|
|
103
104
|
}
|
|
104
105
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
106
|
+
* Read-only code, rendered as `<pre class="phiki …">` but WITHOUT syntax highlighting:
|
|
107
|
+
* each line is a single escaped text token. An array or object is shown as indented JSON.
|
|
107
108
|
*/
|
|
108
109
|
export declare class CodeEntry extends Entry {
|
|
109
110
|
private _grammar;
|
|
@@ -114,43 +115,43 @@ export declare class CodeEntry extends Entry {
|
|
|
114
115
|
private _copyMessage;
|
|
115
116
|
private _copyMessageDuration;
|
|
116
117
|
static make(name: string): CodeEntry;
|
|
117
|
-
/**
|
|
118
|
+
/** Language (`php`, `json`, `javascript`…) used in the `language-…` class; `html` by default, `json` for an array. */
|
|
118
119
|
grammar(grammar: string | null): this;
|
|
119
|
-
/**
|
|
120
|
+
/** Theme class names only: without a highlighter, colors stay those of the default themes. */
|
|
120
121
|
lightTheme(theme: string | null): this;
|
|
121
122
|
darkTheme(theme: string | null): this;
|
|
122
123
|
placeholder(text: string | null): this;
|
|
123
|
-
/**
|
|
124
|
+
/** Clicking copies the code to the clipboard. */
|
|
124
125
|
copyable(condition?: boolean): this;
|
|
125
126
|
copyMessage(message: string | null): this;
|
|
126
127
|
copyMessageDuration(milliseconds: number): this;
|
|
127
128
|
toHtml(): Promise<string>;
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
131
|
+
* Custom-rendered entry: the given function receives the state, record and context, and returns the content
|
|
132
|
+
* placed in the entry wrapper. This HTML is inserted AS IS: escape any user- or database-supplied value with `e()`.
|
|
132
133
|
*/
|
|
133
134
|
export declare class ViewEntry extends Entry {
|
|
134
135
|
private _view;
|
|
135
136
|
private _wrapped;
|
|
136
137
|
static make(name: string): ViewEntry;
|
|
137
|
-
/**
|
|
138
|
+
/** Content render function. */
|
|
138
139
|
view(renderer: ((ctx: ComponentContext & {
|
|
139
140
|
state: unknown;
|
|
140
141
|
entry: ViewEntry;
|
|
141
142
|
}) => string | Promise<string>) | null): this;
|
|
142
|
-
/** `false
|
|
143
|
+
/** `false`: the function renders the whole entry, without the usual wrapper (label). */
|
|
143
144
|
wrapped(condition?: boolean): this;
|
|
144
145
|
toHtml(): Promise<string>;
|
|
145
146
|
}
|
|
146
|
-
/**
|
|
147
|
+
/** Column header of a table repeatable entry (same shape as `TableColumn` from @navette-js/forms). */
|
|
147
148
|
export type RepeatableColumn = {
|
|
148
149
|
label: string;
|
|
149
150
|
getWidth(): string | null;
|
|
150
151
|
};
|
|
151
152
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
153
|
+
* Read-only list of items, as a table: one row per item of the array read from
|
|
154
|
+
* the record, one cell per schema entry.
|
|
154
155
|
*/
|
|
155
156
|
export declare class RepeatableEntry extends Entry {
|
|
156
157
|
private _columns;
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// @navette-js/infolists —
|
|
1
|
+
// @navette-js/infolists — read-only entries (TextEntry, IconEntry…).
|
|
2
2
|
import { Component, Schema } from '@navette-js/schemas';
|
|
3
3
|
import { Attributes, e, icon, js, configured } from '@navette-js/support';
|
|
4
4
|
import { t } from '@navette-js/support/i18n';
|
|
5
5
|
const headline = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').toLowerCase().replace(/^./, (c) => c.toUpperCase());
|
|
6
|
-
/**
|
|
6
|
+
/** Default label: "Department" for `department.name`. */
|
|
7
7
|
const defaultLabel = (name) => headline(name.includes('.') ? name.split('.').slice(-2, -1)[0] : name);
|
|
8
8
|
let colorManager = null;
|
|
9
9
|
export const useColorManager = (manager) => { colorManager = manager; };
|
|
10
|
-
/** Base
|
|
10
|
+
/** Base entry: a label and a value read from the record (dotted paths allowed) or computed. */
|
|
11
11
|
class Entry extends Component {
|
|
12
12
|
_label = null;
|
|
13
13
|
_state = null;
|
|
@@ -15,32 +15,32 @@ class Entry extends Component {
|
|
|
15
15
|
hasStatePath() { return true; }
|
|
16
16
|
label(label) { this._label = label; return this; }
|
|
17
17
|
_hiddenLabel = false;
|
|
18
|
-
/**
|
|
18
|
+
/** Keeps the label for screen readers only. */
|
|
19
19
|
hiddenLabel(condition = true) { this._hiddenLabel = condition; return this; }
|
|
20
|
-
/**
|
|
20
|
+
/** Computed value; without it, the record attribute of the same name is shown. */
|
|
21
21
|
state(resolver) { this._state = resolver; return this; }
|
|
22
22
|
async getState() {
|
|
23
23
|
const ctx = this.context();
|
|
24
24
|
if (this._state)
|
|
25
25
|
return this._state(ctx);
|
|
26
26
|
const record = ctx.record;
|
|
27
|
-
// `department.name
|
|
27
|
+
// `department.name`: flat `department_name` attribute (column computed by the adapter), otherwise a path into the object.
|
|
28
28
|
if (record && this._name.includes('.'))
|
|
29
29
|
return record[this._name.replace(/\./g, '_')] ?? this._name.split('.').reduce((node, key) => node?.[key], record);
|
|
30
30
|
return record?.[this._name];
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
32
|
+
/** Wraps the content with the entry label. */
|
|
33
33
|
wrap(content) {
|
|
34
34
|
if (this._hiddenLabel)
|
|
35
35
|
return `<div class="fi-in-entry"><div class="fi-in-entry-label fi-sr-only" role="term">${e(this._label ?? defaultLabel(this._name))}</div><div class="fi-in-entry-content-col"><div class="fi-in-entry-content-ctn" role="definition"><div class="fi-in-entry-content">${content}</div></div></div></div>`;
|
|
36
36
|
return `<div class="fi-in-entry"><div class="fi-in-entry-label-col"><div class="fi-in-entry-label-ctn"><div class="fi-in-entry-label" role="term">${e(this._label ?? defaultLabel(this._name))}</div></div></div><div class="fi-in-entry-content-col"><div class="fi-in-entry-content-ctn" role="definition"><div class="fi-in-entry-content">${content}</div></div></div></div>`;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
/** Texte en lecture seule (port partiel de TextEntry) : pastille, couleur, icône, montant, date, texte de remplacement. */
|
|
40
39
|
let htmlSanitizer = null;
|
|
41
40
|
const TIGHT_ITEM = new RegExp('<li><p>((?:(?!</?p>|</?li>)[^])*)</p></li>', 'g');
|
|
42
|
-
/**
|
|
41
|
+
/** Sanitizer applied to HTML an entry renders as is (`markdown()`). Without one, the content is escaped. */
|
|
43
42
|
export const useHtmlSanitizer = (sanitizer) => { htmlSanitizer = sanitizer; };
|
|
43
|
+
/** Read-only text: badge, color, icon, money, date, placeholder. */
|
|
44
44
|
export class TextEntry extends Entry {
|
|
45
45
|
_badge = false;
|
|
46
46
|
_color = null;
|
|
@@ -65,18 +65,18 @@ export class TextEntry extends Entry {
|
|
|
65
65
|
suffix(text) { this._suffix = text; return this; }
|
|
66
66
|
_prose = false;
|
|
67
67
|
_markdown = false;
|
|
68
|
-
/**
|
|
68
|
+
/** Typographic styling of the content (`fi-prose`). */
|
|
69
69
|
prose(condition = true) { this._prose = condition; return this; }
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Rich text content, rendered as HTML after the sanitizer set with `useHtmlSanitizer()`.
|
|
72
|
+
* Existing HTML is kept (sanitized) and plain text becomes paragraphs; Markdown syntax itself is not converted.
|
|
73
73
|
*/
|
|
74
74
|
markdown(condition = true) { this._markdown = condition; return this; }
|
|
75
75
|
_copyable = false;
|
|
76
76
|
_numeric = false;
|
|
77
|
-
/**
|
|
77
|
+
/** Clicking copies the value to the clipboard. */
|
|
78
78
|
copyable(condition = true) { this._copyable = condition; return this; }
|
|
79
|
-
/**
|
|
79
|
+
/** Formatted number (thousands separators), aligned as a number. */
|
|
80
80
|
numeric(condition = true) { this._numeric = condition; return this; }
|
|
81
81
|
async toHtml() {
|
|
82
82
|
const record = this.context().record;
|
|
@@ -84,7 +84,7 @@ export class TextEntry extends Entry {
|
|
|
84
84
|
const empty = state === null || state === undefined || state === '';
|
|
85
85
|
if (empty && this._placeholder !== null)
|
|
86
86
|
return this.wrap(`<div class="fi-in-text"><p class="fi-in-placeholder">${e(this._placeholder)}</p></div>`);
|
|
87
|
-
//
|
|
87
|
+
// Empty value: bare container (with `fi-numeric` for money).
|
|
88
88
|
if (empty)
|
|
89
89
|
return this._state || (!this._money && !this._date && !this._badge) ? this.wrap('<div class="fi-in-text-item fi-size-sm fi-in-text fi-wrapped"></div>') : this.wrap(`<div class="${['fi-in-text', this._money || this._numeric ? 'fi-numeric' : null].filter(Boolean).join(' ')}"></div>`);
|
|
90
90
|
let shown = String(state);
|
|
@@ -94,10 +94,10 @@ export class TextEntry extends Entry {
|
|
|
94
94
|
shown = new Intl.NumberFormat('en-US', { style: 'currency', currency: this._money }).format(Number(state));
|
|
95
95
|
if (this._date)
|
|
96
96
|
shown = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${shown.replace(' ', 'T').slice(0, 19)}Z`));
|
|
97
|
-
// `dateTime()
|
|
97
|
+
// `dateTime()`: "Jan 5, 2024 13:45:00" (`M j, Y H:i:s`).
|
|
98
98
|
if (this._dateTime)
|
|
99
99
|
shown = `${new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${shown.replace(' ', 'T').slice(0, 19)}Z`))} ${shown.slice(11, 19)}`;
|
|
100
|
-
// `time('H:i')`
|
|
100
|
+
// `time('H:i')` or `time()` (`H:i:s`): the time part of an `H:i:s` or `Y-m-d H:i:s` value.
|
|
101
101
|
if (this._time)
|
|
102
102
|
shown = (shown.length > 8 ? shown.slice(11, 19) : shown).slice(0, this._time === 'H:i' ? 5 : 8);
|
|
103
103
|
if (this._format)
|
|
@@ -107,13 +107,13 @@ export class TextEntry extends Entry {
|
|
|
107
107
|
if (this._markdown) {
|
|
108
108
|
const source = String(state);
|
|
109
109
|
const html = /<[a-z][^>]*>/i.test(source)
|
|
110
|
-
//
|
|
110
|
+
// Tight lists: a list item's single paragraph is unwrapped.
|
|
111
111
|
? (htmlSanitizer ? htmlSanitizer(source).replace(TIGHT_ITEM, '<li>$1</li>') : e(source))
|
|
112
112
|
: source.split(/\n{2,}/).map((paragraph) => `<p>${e(paragraph.trim()).replace(/\n/g, '<br />')}</p>`).join('');
|
|
113
113
|
return this.wrap(`<div class="${['fi-in-text-item', this._prose ? 'fi-prose' : null, 'fi-size-sm', 'fi-in-text', 'fi-wrapped'].filter(Boolean).join(' ')}">${html}</div>`);
|
|
114
114
|
}
|
|
115
115
|
if (this._badge && Array.isArray(state)) {
|
|
116
|
-
//
|
|
116
|
+
// Several values: a list of badges (tags, for instance).
|
|
117
117
|
const color = typeof this._color === 'function' ? this._color(record) : (this._color ?? 'primary');
|
|
118
118
|
const classes = ['fi-badge', 'fi-size-sm', ...(color !== 'gray' ? (colorManager?.getComponentClasses('badge', color) ?? []) : [])].join(' ');
|
|
119
119
|
return this.wrap(`<ul class="fi-in-text fi-in-text-has-badges fi-wrapped">${state.map((item) => `<li class="fi-in-text-item"><span class="${classes}">${e(String(item))}</span></li>`).join('')}</ul>`);
|
|
@@ -139,7 +139,7 @@ export class TextEntry extends Entry {
|
|
|
139
139
|
return this.wrap(`<div class="${['fi-in-text-item', 'fi-size-sm', 'fi-in-text', this._money || this._numeric ? 'fi-numeric' : null, 'fi-wrapped'].filter(Boolean).join(' ')}">${content}</div>`);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
-
/**
|
|
142
|
+
/** Color swatch: only hex or simple functional colors are injected into the style attribute. */
|
|
143
143
|
export class ColorEntry extends Entry {
|
|
144
144
|
_placeholder = null;
|
|
145
145
|
static make(name) { return configured(new ColorEntry(name)); }
|
|
@@ -152,13 +152,13 @@ export class ColorEntry extends Entry {
|
|
|
152
152
|
return this.wrap(`<div class="fi-in-color fi-wrapped"><div role="img" aria-label="${e(color)}" class="fi-in-color-item" style="background-color: ${color};"></div></div>`);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
/**
|
|
155
|
+
/** Read-only image: only same-site (`/…`) or http(s) URLs are rendered. */
|
|
156
156
|
export class ImageEntry extends Entry {
|
|
157
157
|
_height = '8rem';
|
|
158
158
|
_alt = null;
|
|
159
159
|
_circular = false;
|
|
160
160
|
static make(name) { return configured(new ImageEntry(name)); }
|
|
161
|
-
/**
|
|
161
|
+
/** CSS height of the image (`8rem` by default). */
|
|
162
162
|
imageHeight(height) { if (/^[0-9.]+(rem|px|em)$/.test(height))
|
|
163
163
|
this._height = height; return this; }
|
|
164
164
|
alt(text) { this._alt = text; return this; }
|
|
@@ -169,7 +169,7 @@ export class ImageEntry extends Entry {
|
|
|
169
169
|
return this.wrap(`<div class="fi-in-image fi-wrapped">${sources.map((src) => `<img alt="${e(this._alt ?? '')}" src="${e(src)}" style="height: ${this._height};"${this._circular ? ' class="fi-circular"' : ''} />`).join('')}</div>`);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
/**
|
|
172
|
+
/** Icon based on the value; `boolean()` shows a green check or a red cross. */
|
|
173
173
|
export class IconEntry extends Entry {
|
|
174
174
|
_boolean = false;
|
|
175
175
|
static make(name) { return configured(new IconEntry(name)); }
|
|
@@ -183,7 +183,7 @@ export class IconEntry extends Entry {
|
|
|
183
183
|
return this.wrap(`<div class="fi-in-icon fi-wrapped">${icon(on ? 'o-check-circle' : 'o-x-circle', { size: 'lg', attributes: new Attributes().class(classes) })}<span class="fi-sr-only">${e(t(on ? 'filament-tables::table.columns.icon.boolean.true' : 'filament-tables::table.columns.icon.boolean.false'))}</span></div>`);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
/**
|
|
186
|
+
/** Key/value pairs as a table: the state is an object (or its JSON form); a non-scalar value is shown as JSON. */
|
|
187
187
|
export class KeyValueEntry extends Entry {
|
|
188
188
|
_keyLabel = null;
|
|
189
189
|
_valueLabel = null;
|
|
@@ -191,9 +191,9 @@ export class KeyValueEntry extends Entry {
|
|
|
191
191
|
static make(name) { return configured(new KeyValueEntry(name)); }
|
|
192
192
|
keyLabel(label) { this._keyLabel = label; return this; }
|
|
193
193
|
valueLabel(label) { this._valueLabel = label; return this; }
|
|
194
|
-
/**
|
|
194
|
+
/** Text shown when there are no pairs ("No entries" by default). */
|
|
195
195
|
placeholder(text) { this._placeholder = text; return this; }
|
|
196
|
-
/** @deprecated
|
|
196
|
+
/** @deprecated Use `placeholder()`. */
|
|
197
197
|
emptyMessage(text) { return this.placeholder(text); }
|
|
198
198
|
async toHtml() {
|
|
199
199
|
let state = await this.getState();
|
|
@@ -213,13 +213,13 @@ export class KeyValueEntry extends Entry {
|
|
|
213
213
|
return this.wrap(`<table class="fi-in-key-value"><thead><tr><th scope="col">${e(this._keyLabel ?? tr('columns.key.label'))}</th><th scope="col">${e(this._valueLabel ?? tr('columns.value.label'))}</th></tr></thead><tbody>${rows}${empty}</tbody></table>`);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
/**
|
|
216
|
+
/** Pretty-printed JSON: four-space indent, with `/` and non-ASCII characters escaped. */
|
|
217
217
|
const phpPrettyJson = (value) => (JSON.stringify(value, null, 4) ?? 'null').replace(/\//g, '\\/').replace(/[\u0080-]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
218
|
-
/**
|
|
218
|
+
/** Background and text colors of the default themes (github-light / github-dark-high-contrast). */
|
|
219
219
|
const PHIKI_DEFAULT_STYLE = 'background-color: #fff;color: #24292e;--phiki-dark-background-color: #0a0c10;--phiki-dark-color: #f0f3f6;';
|
|
220
220
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
221
|
+
* Read-only code, rendered as `<pre class="phiki …">` but WITHOUT syntax highlighting:
|
|
222
|
+
* each line is a single escaped text token. An array or object is shown as indented JSON.
|
|
223
223
|
*/
|
|
224
224
|
export class CodeEntry extends Entry {
|
|
225
225
|
_grammar = null;
|
|
@@ -230,13 +230,13 @@ export class CodeEntry extends Entry {
|
|
|
230
230
|
_copyMessage = null;
|
|
231
231
|
_copyMessageDuration = 2000;
|
|
232
232
|
static make(name) { return configured(new CodeEntry(name)); }
|
|
233
|
-
/**
|
|
233
|
+
/** Language (`php`, `json`, `javascript`…) used in the `language-…` class; `html` by default, `json` for an array. */
|
|
234
234
|
grammar(grammar) { this._grammar = grammar; return this; }
|
|
235
|
-
/**
|
|
235
|
+
/** Theme class names only: without a highlighter, colors stay those of the default themes. */
|
|
236
236
|
lightTheme(theme) { this._lightTheme = theme; return this; }
|
|
237
237
|
darkTheme(theme) { this._darkTheme = theme; return this; }
|
|
238
238
|
placeholder(text) { this._placeholder = text; return this; }
|
|
239
|
-
/**
|
|
239
|
+
/** Clicking copies the code to the clipboard. */
|
|
240
240
|
copyable(condition = true) { this._copyable = condition; return this; }
|
|
241
241
|
copyMessage(message) { this._copyMessage = message; return this; }
|
|
242
242
|
copyMessageDuration(milliseconds) { this._copyMessageDuration = milliseconds; return this; }
|
|
@@ -254,7 +254,7 @@ export class CodeEntry extends Entry {
|
|
|
254
254
|
const language = (grammar ?? 'html').replace(/[^a-zA-Z0-9_+#-]/g, '');
|
|
255
255
|
const light = (this._lightTheme ?? 'github-light').replace(/[^a-zA-Z0-9_-]/g, ''), dark = (this._darkTheme ?? 'github-dark-high-contrast').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
256
256
|
const style = this._lightTheme === null && this._darkTheme === null ? ` style="${PHIKI_DEFAULT_STYLE}"` : '';
|
|
257
|
-
//
|
|
257
|
+
// One `<span class="line">` per line, the newline being the last token of each line.
|
|
258
258
|
const lines = code.split('\n');
|
|
259
259
|
const linesHtml = lines.map((line, index) => `<span class="line">${line === '' ? '' : `<span class="token">${e(line)}</span>`}${index < lines.length - 1 ? '<span class="token">\n</span>' : ''}</span>`).filter((html, index) => !(index === lines.length - 1 && lines[index] === '' && lines.length > 1)).join('');
|
|
260
260
|
const pre = `<pre class="phiki language-${language} ${light} phiki-themes ${dark}" data-language="${language}"${style}><code>${linesHtml}</code></pre>`;
|
|
@@ -272,27 +272,27 @@ export class CodeEntry extends Entry {
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
275
|
+
* Custom-rendered entry: the given function receives the state, record and context, and returns the content
|
|
276
|
+
* placed in the entry wrapper. This HTML is inserted AS IS: escape any user- or database-supplied value with `e()`.
|
|
277
277
|
*/
|
|
278
278
|
export class ViewEntry extends Entry {
|
|
279
279
|
_view = null;
|
|
280
280
|
_wrapped = true;
|
|
281
281
|
static make(name) { return configured(new ViewEntry(name)); }
|
|
282
|
-
/**
|
|
282
|
+
/** Content render function. */
|
|
283
283
|
view(renderer) { this._view = renderer; return this; }
|
|
284
|
-
/** `false
|
|
284
|
+
/** `false`: the function renders the whole entry, without the usual wrapper (label). */
|
|
285
285
|
wrapped(condition = true) { this._wrapped = condition; return this; }
|
|
286
286
|
async toHtml() {
|
|
287
287
|
if (!this._view)
|
|
288
|
-
throw new Error(`ViewEntry [${this._name}]
|
|
288
|
+
throw new Error(`ViewEntry [${this._name}]: view() is required.`);
|
|
289
289
|
const html = await this._view({ ...this.context(), state: await this.getState(), entry: this });
|
|
290
290
|
return this._wrapped ? this.wrap(html) : html;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
294
|
+
* Read-only list of items, as a table: one row per item of the array read from
|
|
295
|
+
* the record, one cell per schema entry.
|
|
296
296
|
*/
|
|
297
297
|
export class RepeatableEntry extends Entry {
|
|
298
298
|
_columns = [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/infolists",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Read-only infolist entries for Navette admin panels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"navette",
|
|
33
|
-
"filament",
|
|
34
33
|
"admin",
|
|
35
34
|
"panel",
|
|
36
35
|
"livewire",
|
|
@@ -38,8 +37,8 @@
|
|
|
38
37
|
"node"
|
|
39
38
|
],
|
|
40
39
|
"dependencies": {
|
|
41
|
-
"@navette-js/support": "1.0.0-beta.
|
|
42
|
-
"@navette-js/schemas": "1.0.0-beta.
|
|
40
|
+
"@navette-js/support": "1.0.0-beta.2",
|
|
41
|
+
"@navette-js/schemas": "1.0.0-beta.2"
|
|
43
42
|
},
|
|
44
43
|
"publishConfig": {
|
|
45
44
|
"access": "public",
|