@navette-js/infolists 1.0.0-beta.2 → 1.0.0-beta.4
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/lib/index.d.ts +334 -56
- package/lib/index.js +891 -137
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1,198 +1,890 @@
|
|
|
1
1
|
// @navette-js/infolists — read-only entries (TextEntry, IconEntry…).
|
|
2
|
-
import { Component, Schema } from '@navette-js/schemas';
|
|
3
|
-
import { Attributes, e, icon, js, configured } from '@navette-js/support';
|
|
4
|
-
import { t } from '@navette-js/support/i18n';
|
|
2
|
+
import { Component, Schema, Text, renderInlineItems } from '@navette-js/schemas';
|
|
3
|
+
import { Attributes, HtmlString, cls, e, evaluate, findShade, icon, isSafeUrl, js, markdownToHtml, sanitizeHtml, configured } from '@navette-js/support';
|
|
4
|
+
import { diffForHumans, t, tc } 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
|
+
/** Filament-style merge: the defaults come first (and keep their position), `class` and `style` are concatenated defaults-first. */
|
|
11
|
+
function mergeAttrs(defaults, attributes) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const [key, value] of Object.entries(defaults))
|
|
14
|
+
if (value !== null && value !== undefined && value !== false)
|
|
15
|
+
out[key] = value;
|
|
16
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
17
|
+
if (value === null || value === undefined || value === false)
|
|
18
|
+
continue;
|
|
19
|
+
out[key] = (key === 'class' || key === 'style') && out[key] ? [out[key], value].filter((part) => part !== '').join(' ') : value;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** Appends classes (the attribute keeps its position, or goes last). */
|
|
24
|
+
function withClass(attributes, list) {
|
|
25
|
+
const added = cls(list);
|
|
26
|
+
return added ? { ...attributes, class: attributes.class ? `${attributes.class} ${added}` : added } : attributes;
|
|
27
|
+
}
|
|
28
|
+
function withStyle(attributes, styles) {
|
|
29
|
+
const added = styles.filter(Boolean).join('; ');
|
|
30
|
+
if (!added)
|
|
31
|
+
return attributes;
|
|
32
|
+
const existing = String(attributes.style ?? '').replace(/[;\s]+$/, '');
|
|
33
|
+
return { ...attributes, style: `${existing ? `${existing}; ` : ''}${added};` };
|
|
34
|
+
}
|
|
35
|
+
/** Values are expected to be escaped already; only `"` is escaped again. */
|
|
36
|
+
const renderAttrs = (attributes) => new Attributes(attributes).toHtml();
|
|
37
|
+
const ATTRIBUTE_NAME = /^[a-zA-Z_:@][a-zA-Z0-9_:.@-]*$/;
|
|
38
|
+
/** Developer-supplied attributes: valid names only, values escaped. */
|
|
39
|
+
const escapeAttributes = (attributes) => Object.fromEntries(Object.entries(attributes)
|
|
40
|
+
.filter(([name, value]) => ATTRIBUTE_NAME.test(name) && value !== null && value !== undefined && value !== false)
|
|
41
|
+
.map(([name, value]) => [name, value === true ? true : e(value)]));
|
|
42
|
+
const isBlank = (value) => value === null || value === undefined || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0);
|
|
43
|
+
const ALIGNMENTS = new Set(['start', 'center', 'end', 'left', 'right', 'justify', 'between']);
|
|
44
|
+
const alignmentClass = (alignment) => (alignment && ALIGNMENTS.has(alignment) ? `fi-align-${alignment}` : null);
|
|
45
|
+
const safeName = (value) => value.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
46
|
+
const ICON_NAME = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
|
|
47
|
+
const renderIcon = (name, options = {}) => (typeof name === 'string' && ICON_NAME.test(name) ? icon(name, options) : '');
|
|
48
|
+
const tooltipJs = (text) => (text === null || text === undefined || text === '' ? null : `{ content: ${js(String(text))}, theme: $store.theme, allowHTML: false }`);
|
|
49
|
+
const decodeEntities = (html) => html.replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/&/g, '&');
|
|
50
|
+
const stripTags = (html) => decodeEntities(html.replace(/<[^>]*>/g, ''));
|
|
51
|
+
const isNumericValue = (value) => (typeof value === 'number' && Number.isFinite(value)) || (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)));
|
|
52
|
+
/** Simple CSS length (`40px`, `2.5rem`, `50%`, `auto`); a number is a length in pixels. Anything else is rejected. */
|
|
53
|
+
const cssLength = (value) => {
|
|
54
|
+
if (typeof value === 'number')
|
|
55
|
+
return Number.isFinite(value) && value >= 0 ? `${value}px` : null;
|
|
56
|
+
if (typeof value !== 'string')
|
|
57
|
+
return null;
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
return /^(\d+(\.\d+)?|\.\d+)(px|rem|em|%|vh|vw|ch)$/.test(trimmed) || trimmed === 'auto' ? trimmed : null;
|
|
60
|
+
};
|
|
61
|
+
/** Image sources: same-site paths (`/…`) and http(s) URLs only. */
|
|
62
|
+
const isImageUrl = (value) => typeof value === 'string' && /^(\/(?!\/)|https?:\/\/)/i.test(value.trim());
|
|
63
|
+
const resolveItem = (option, record, state) => (typeof option === 'function' ? option(record, state) : option);
|
|
64
|
+
const resolveMap = (option, record, state) => {
|
|
65
|
+
if (option !== null && typeof option === 'object')
|
|
66
|
+
return option[String(state)] ?? null;
|
|
67
|
+
return resolveItem(option, record, state);
|
|
68
|
+
};
|
|
69
|
+
const TEXT_SIZES = new Set(['xs', 'sm', 'md', 'lg']);
|
|
70
|
+
const textSize = (size) => (size === 'base' ? 'md' : size && TEXT_SIZES.has(size) ? size : size ? safeName(size) || 'sm' : 'sm');
|
|
71
|
+
const FONT_WEIGHTS = new Set(['thin', 'extralight', 'light', 'normal', 'medium', 'semibold', 'bold', 'extrabold', 'black']);
|
|
72
|
+
const FONT_FAMILIES = new Set(['sans', 'serif', 'mono']);
|
|
73
|
+
const fontClass = (value, allowed) => (!value ? null : allowed.has(value) ? `fi-font-${value}` : safeName(value) || null);
|
|
74
|
+
/** Text color classes of a text entry item (Filament's `TextEntryComponent\ItemComponent` color map). */
|
|
75
|
+
const itemColorCache = new Map();
|
|
76
|
+
function itemColorClasses(color) {
|
|
77
|
+
if (!color)
|
|
78
|
+
return [];
|
|
79
|
+
const base = ['fi-color', `fi-color-${safeName(color)}`];
|
|
80
|
+
const palette = colorManager?.getColor(color), gray = colorManager?.getColor('gray');
|
|
81
|
+
if (!palette || !gray)
|
|
82
|
+
return base;
|
|
83
|
+
const key = `${color}|${Object.values(palette).join(',')}|${gray[800]}`;
|
|
84
|
+
let classes = itemColorCache.get(key);
|
|
85
|
+
if (!classes) {
|
|
86
|
+
const text = findShade(palette, 'oklch(1 0 0)') ?? 900;
|
|
87
|
+
const dark = findShade(palette, gray[800], { maxShade: 600, fromDarkest: true }) ?? 200;
|
|
88
|
+
classes = [...base, `fi-text-color-${text}`, `dark:fi-text-color-${dark}`];
|
|
89
|
+
itemColorCache.set(key, classes);
|
|
90
|
+
}
|
|
91
|
+
return classes;
|
|
92
|
+
}
|
|
93
|
+
const badgeColorClasses = (color) => (color && color !== 'gray' ? (colorManager?.getComponentClasses('badge', color) ?? []) : []);
|
|
94
|
+
/** Icon entries: 3:1 contrast shades; gray is the default and gets no classes. */
|
|
95
|
+
const iconColorClasses = (color) => (color && color !== 'gray' ? (colorManager?.getComponentClasses('icon-column', color) ?? []) : []);
|
|
96
|
+
// ------------------------------------------------------------------ dates
|
|
97
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
98
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
99
|
+
const pad = (value, length = 2) => String(Math.abs(value)).padStart(length, '0');
|
|
100
|
+
const ordinal = (value) => (value % 10 === 1 && value % 100 !== 11 ? 'st' : value % 10 === 2 && value % 100 !== 12 ? 'nd' : value % 10 === 3 && value % 100 !== 13 ? 'rd' : 'th');
|
|
101
|
+
/** Date state: a `Date`, a timestamp in milliseconds, or a string. Strings without a time zone (SQL values) are read as UTC; a time alone is today's. */
|
|
102
|
+
function parseDate(value) {
|
|
103
|
+
if (value instanceof Date)
|
|
104
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
105
|
+
if (typeof value === 'number')
|
|
106
|
+
return Number.isFinite(value) ? new Date(value) : null;
|
|
107
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
108
|
+
return null;
|
|
109
|
+
let text = value.trim();
|
|
110
|
+
const time = /^(\d{1,2}):(\d{2})(?::(\d{2}))?(\.\d+)?$/.exec(text);
|
|
111
|
+
if (time)
|
|
112
|
+
text = `${new Date().toISOString().slice(0, 10)}T${pad(Number(time[1]))}:${time[2]}:${time[3] ?? '00'}Z`;
|
|
113
|
+
else if (/^\d{4}-\d{2}-\d{2}$/.test(text))
|
|
114
|
+
text = `${text}T00:00:00Z`;
|
|
115
|
+
else if (!/[zZ]$|[+-]\d\d:?\d\d$/.test(text))
|
|
116
|
+
text = `${text.replace(' ', 'T').replace(/(\.\d{3})\d+/, '$1')}Z`;
|
|
117
|
+
const date = new Date(text);
|
|
118
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
119
|
+
}
|
|
120
|
+
const isTimeZone = (zone) => { try {
|
|
121
|
+
new Intl.DateTimeFormat('en-US', { timeZone: zone });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
} };
|
|
127
|
+
function dateParts(date, timeZone) {
|
|
128
|
+
const zone = isTimeZone(timeZone) ? timeZone : 'UTC';
|
|
129
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: zone, year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hourCycle: 'h23', timeZoneName: 'short' }).formatToParts(date).map((part) => [part.type, part.value]));
|
|
130
|
+
const year = Number(parts.year), month = Number(parts.month), day = Number(parts.day), hour = Number(parts.hour) % 24, minute = Number(parts.minute), second = Number(parts.second);
|
|
131
|
+
const offset = Math.round((Date.UTC(year, month - 1, day, hour, minute, second) - Math.floor(date.getTime() / 1000) * 1000) / 60000);
|
|
132
|
+
return { year, month, day, hour, minute, second, ms: date.getUTCMilliseconds(), weekday: new Date(Date.UTC(year, month - 1, day)).getUTCDay(), offset, zone: parts.timeZoneName ?? zone, timestamp: Math.floor(date.getTime() / 1000) };
|
|
133
|
+
}
|
|
134
|
+
const offsetText = (minutes, colon) => `${minutes < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(minutes) / 60))}${colon ? ':' : ''}${pad(Math.abs(minutes) % 60)}`;
|
|
135
|
+
const isLeapYear = (year) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
136
|
+
/** PHP `date()` format (`M j, Y`, `d/m/Y H:i`…), with English month and day names. A backslash escapes a character. */
|
|
137
|
+
export function formatPhpDate(date, format, timeZone = 'UTC') {
|
|
138
|
+
const p = dateParts(date, timeZone);
|
|
139
|
+
const hour12 = p.hour % 12 || 12;
|
|
140
|
+
const tokens = {
|
|
141
|
+
d: () => pad(p.day), D: () => DAYS[p.weekday].slice(0, 3), j: () => String(p.day), l: () => DAYS[p.weekday], N: () => String(p.weekday || 7), S: () => ordinal(p.day), w: () => String(p.weekday),
|
|
142
|
+
z: () => String(Math.round((Date.UTC(p.year, p.month - 1, p.day) - Date.UTC(p.year, 0, 1)) / 86400000)),
|
|
143
|
+
F: () => MONTHS[p.month - 1], M: () => MONTHS[p.month - 1].slice(0, 3), m: () => pad(p.month), n: () => String(p.month), t: () => String(new Date(Date.UTC(p.year, p.month, 0)).getUTCDate()),
|
|
144
|
+
L: () => (isLeapYear(p.year) ? '1' : '0'), Y: () => String(p.year), y: () => pad(p.year % 100),
|
|
145
|
+
a: () => (p.hour < 12 ? 'am' : 'pm'), A: () => (p.hour < 12 ? 'AM' : 'PM'), g: () => String(hour12), G: () => String(p.hour), h: () => pad(hour12), H: () => pad(p.hour),
|
|
146
|
+
i: () => pad(p.minute), s: () => pad(p.second), u: () => pad(p.ms * 1000, 6), v: () => pad(p.ms, 3),
|
|
147
|
+
e: () => (isTimeZone(timeZone) ? timeZone : 'UTC'), T: () => p.zone, P: () => offsetText(p.offset, true), O: () => offsetText(p.offset, false), p: () => (p.offset === 0 ? 'Z' : offsetText(p.offset, true)), Z: () => String(p.offset * 60),
|
|
148
|
+
U: () => String(p.timestamp),
|
|
149
|
+
c: () => `${p.year}-${pad(p.month)}-${pad(p.day)}T${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}${offsetText(p.offset, true)}`,
|
|
150
|
+
r: () => `${DAYS[p.weekday].slice(0, 3)}, ${pad(p.day)} ${MONTHS[p.month - 1].slice(0, 3)} ${p.year} ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)} ${offsetText(p.offset, false)}`,
|
|
151
|
+
};
|
|
152
|
+
let out = '';
|
|
153
|
+
for (let index = 0; index < format.length; index++) {
|
|
154
|
+
const character = format[index];
|
|
155
|
+
if (character === '\\' && index + 1 < format.length) {
|
|
156
|
+
out += format[++index];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
out += Object.hasOwn(tokens, character) ? tokens[character]() : character;
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** English macro formats of Carbon's `isoFormat()`. */
|
|
164
|
+
const ISO_MACROS = {
|
|
165
|
+
LT: 'h:mm A', LTS: 'h:mm:ss A', L: 'MM/DD/YYYY', LL: 'MMMM D, YYYY', LLL: 'MMMM D, YYYY h:mm A', LLLL: 'dddd, MMMM D, YYYY h:mm A',
|
|
166
|
+
l: 'M/D/YYYY', ll: 'MMM D, YYYY', lll: 'MMM D, YYYY h:mm A', llll: 'ddd, MMM D, YYYY h:mm A',
|
|
167
|
+
};
|
|
168
|
+
const ISO_TOKEN = /\[[^\]]*\]|LTS|LT|LLLL|LLL|LL|L|llll|lll|ll|l|YYYY|YY|Qo|Q|MMMM|MMM|MM|Mo|M|DDDD|DDDo|DDD|Do|DD|D|dddd|ddd|dd|do|d|E|e|HH|H|hh|h|kk|k|mm|m|ss|s|SSS|SS|S|A|a|ZZ|Z|X|x|zz|z/g;
|
|
169
|
+
/** Carbon `isoFormat()` format (`L`, `LLL`, `dddd D MMMM YYYY`…), in English. Text in `[brackets]` is kept as is. */
|
|
170
|
+
export function formatIsoDate(date, format, timeZone = 'UTC') {
|
|
171
|
+
const p = dateParts(date, timeZone);
|
|
172
|
+
const hour12 = p.hour % 12 || 12;
|
|
173
|
+
const dayOfYear = Math.round((Date.UTC(p.year, p.month - 1, p.day) - Date.UTC(p.year, 0, 1)) / 86400000) + 1;
|
|
174
|
+
const tokens = {
|
|
175
|
+
YYYY: () => String(p.year), YY: () => pad(p.year % 100), Q: () => String(Math.ceil(p.month / 3)), Qo: () => `${Math.ceil(p.month / 3)}${ordinal(Math.ceil(p.month / 3))}`,
|
|
176
|
+
MMMM: () => MONTHS[p.month - 1], MMM: () => MONTHS[p.month - 1].slice(0, 3), MM: () => pad(p.month), Mo: () => `${p.month}${ordinal(p.month)}`, M: () => String(p.month),
|
|
177
|
+
DDDD: () => pad(dayOfYear, 3), DDDo: () => `${dayOfYear}${ordinal(dayOfYear)}`, DDD: () => String(dayOfYear), Do: () => `${p.day}${ordinal(p.day)}`, DD: () => pad(p.day), D: () => String(p.day),
|
|
178
|
+
dddd: () => DAYS[p.weekday], ddd: () => DAYS[p.weekday].slice(0, 3), dd: () => DAYS[p.weekday].slice(0, 2), do: () => `${p.weekday}${ordinal(p.weekday)}`, d: () => String(p.weekday), e: () => String(p.weekday), E: () => String(p.weekday || 7),
|
|
179
|
+
HH: () => pad(p.hour), H: () => String(p.hour), hh: () => pad(hour12), h: () => String(hour12), kk: () => pad(p.hour || 24), k: () => String(p.hour || 24),
|
|
180
|
+
mm: () => pad(p.minute), m: () => String(p.minute), ss: () => pad(p.second), s: () => String(p.second),
|
|
181
|
+
SSS: () => pad(p.ms, 3), SS: () => pad(Math.floor(p.ms / 10)), S: () => String(Math.floor(p.ms / 100)),
|
|
182
|
+
A: () => (p.hour < 12 ? 'AM' : 'PM'), a: () => (p.hour < 12 ? 'am' : 'pm'), Z: () => offsetText(p.offset, true), ZZ: () => offsetText(p.offset, false),
|
|
183
|
+
X: () => String(p.timestamp), x: () => String(date.getTime()), z: () => p.zone, zz: () => p.zone,
|
|
184
|
+
};
|
|
185
|
+
return format.replace(ISO_TOKEN, (token) => {
|
|
186
|
+
if (token.startsWith('['))
|
|
187
|
+
return token.slice(1, -1);
|
|
188
|
+
if (Object.hasOwn(ISO_MACROS, token))
|
|
189
|
+
return formatIsoDate(date, ISO_MACROS[token], timeZone);
|
|
190
|
+
return tokens[token]();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** Relative date: "2 days ago", or "in 3 hours" style for a future date ("3 hours from now"). */
|
|
194
|
+
function since(date, now = new Date()) {
|
|
195
|
+
if (date.getTime() <= now.getTime())
|
|
196
|
+
return diffForHumans(date, now) ?? '';
|
|
197
|
+
return (diffForHumans(now, date) ?? '').replace(/ ago$/, ' from now');
|
|
198
|
+
}
|
|
199
|
+
// ------------------------------------------------------------------ numbers
|
|
200
|
+
/** PHP `number_format()`: rounded to `decimals`, with the given separators. */
|
|
201
|
+
function numberFormat(value, decimals, decimalSeparator, thousandsSeparator) {
|
|
202
|
+
const fixed = Math.abs(value).toFixed(Math.max(0, Math.min(20, Math.floor(decimals))));
|
|
203
|
+
const [integer, fraction] = fixed.split('.');
|
|
204
|
+
const sign = value < 0 && Number(fixed) !== 0 ? '-' : '';
|
|
205
|
+
return `${sign}${integer.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator)}${fraction ? `${decimalSeparator}${fraction}` : ''}`;
|
|
206
|
+
}
|
|
207
|
+
const fractionDigits = (digits) => (typeof digits === 'number' && Number.isFinite(digits) ? Math.max(0, Math.min(20, Math.floor(digits))) : null);
|
|
10
208
|
/** Base entry: a label and a value read from the record (dotted paths allowed) or computed. */
|
|
11
209
|
class Entry extends Component {
|
|
12
210
|
_label = null;
|
|
13
211
|
_state = null;
|
|
212
|
+
_default = null;
|
|
213
|
+
_hiddenLabel = false;
|
|
214
|
+
_placeholder = null;
|
|
215
|
+
_url = null;
|
|
216
|
+
_openUrlInNewTab = false;
|
|
217
|
+
_tooltip = null;
|
|
218
|
+
_emptyTooltip = null;
|
|
219
|
+
_inlineLabel = false;
|
|
220
|
+
_alignment = null;
|
|
221
|
+
_hiddenJs = null;
|
|
222
|
+
_visibleJs = null;
|
|
223
|
+
_extraEntryWrapperAttributes = {};
|
|
224
|
+
_slots = {};
|
|
14
225
|
constructor(name) { super(); this._name = name; }
|
|
15
226
|
hasStatePath() { return true; }
|
|
227
|
+
/** Label: a string or a function of the context. Escaped when rendered. */
|
|
16
228
|
label(label) { this._label = label; return this; }
|
|
17
|
-
_hiddenLabel = false;
|
|
18
229
|
/** Keeps the label for screen readers only. */
|
|
19
230
|
hiddenLabel(condition = true) { this._hiddenLabel = condition; return this; }
|
|
20
231
|
/** Computed value; without it, the record attribute of the same name is shown. */
|
|
21
232
|
state(resolver) { this._state = resolver; return this; }
|
|
233
|
+
/** State used when the real state is `null` or `undefined`; it is displayed as if it were real. */
|
|
234
|
+
default(state) { this._default = state; return this; }
|
|
235
|
+
/** Gray text shown when the state is empty. */
|
|
236
|
+
placeholder(text) { this._placeholder = text; return this; }
|
|
237
|
+
/** Makes the content a link. Only `http(s)`, `mailto` and relative URLs are rendered: any other scheme is ignored. */
|
|
238
|
+
url(url, openInNewTab = null) {
|
|
239
|
+
this._url = url;
|
|
240
|
+
if (openInNewTab !== null)
|
|
241
|
+
this._openUrlInNewTab = openInNewTab;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
openUrlInNewTab(condition = true) { this._openUrlInNewTab = condition; return this; }
|
|
245
|
+
/** Tooltip on the content: a string, or a function of the record and the state item. */
|
|
246
|
+
tooltip(tooltip) { this._tooltip = tooltip; return this; }
|
|
247
|
+
/** Tooltip shown on the content when the state is empty. */
|
|
248
|
+
emptyTooltip(tooltip) { this._emptyTooltip = tooltip; return this; }
|
|
249
|
+
/** Label next to the content instead of above it (from the `sm` breakpoint). */
|
|
250
|
+
inlineLabel(condition = true) { this._inlineLabel = condition; return this; }
|
|
251
|
+
/** Content alignment: `start`, `center`, `end`, `left`, `right`, `justify` or `between`. */
|
|
252
|
+
alignment(alignment) { this._alignment = alignment; return this; }
|
|
253
|
+
alignStart() { return this.alignment('start'); }
|
|
254
|
+
alignCenter() { return this.alignment('center'); }
|
|
255
|
+
alignEnd() { return this.alignment('end'); }
|
|
256
|
+
alignLeft() { return this.alignment('left'); }
|
|
257
|
+
alignRight() { return this.alignment('right'); }
|
|
258
|
+
alignJustify() { return this.alignment('justify'); }
|
|
259
|
+
/** Hides the entry in the browser while this JavaScript expression is true (`$get('role') !== 'staff'`). Written by the developer, never from user input. */
|
|
260
|
+
hiddenJs(expression) { this._hiddenJs = expression; return this; }
|
|
261
|
+
/** Shows the entry in the browser only while this JavaScript expression is true. */
|
|
262
|
+
visibleJs(expression) { this._visibleJs = expression; return this; }
|
|
263
|
+
/** Attributes of the outer element of the entry wrapper (label and content). Values are escaped. */
|
|
264
|
+
extraEntryWrapperAttributes(attributes) { Object.assign(this._extraEntryWrapperAttributes, attributes); return this; }
|
|
265
|
+
aboveLabel(content) { this._slots.aboveLabel = content; return this; }
|
|
266
|
+
belowLabel(content) { this._slots.belowLabel = content; return this; }
|
|
267
|
+
beforeLabel(content) { this._slots.beforeLabel = content; return this; }
|
|
268
|
+
/** Content after the label, aligned to the end. */
|
|
269
|
+
afterLabel(content) { this._slots.afterLabel = content; return this; }
|
|
270
|
+
aboveContent(content) { this._slots.aboveContent = content; return this; }
|
|
271
|
+
belowContent(content) { this._slots.belowContent = content; return this; }
|
|
272
|
+
beforeContent(content) { this._slots.beforeContent = content; return this; }
|
|
273
|
+
/** Content after the entry content, aligned to the end. */
|
|
274
|
+
afterContent(content) { this._slots.afterContent = content; return this; }
|
|
275
|
+
async getLabel() { return (await evaluate(this._label, this.context())) ?? defaultLabel(this._name); }
|
|
22
276
|
async getState() {
|
|
23
277
|
const ctx = this.context();
|
|
278
|
+
let state;
|
|
24
279
|
if (this._state)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
280
|
+
state = await this._state(ctx);
|
|
281
|
+
else {
|
|
282
|
+
const record = ctx.record;
|
|
283
|
+
// `department.name`: flat `department_name` attribute (column computed by the adapter), otherwise a path into the object.
|
|
284
|
+
if (record && this._name.includes('.'))
|
|
285
|
+
state = record[this._name.replace(/\./g, '_')] ?? this._name.split('.').reduce((node, key) => node?.[key], record);
|
|
286
|
+
else
|
|
287
|
+
state = record?.[this._name];
|
|
288
|
+
}
|
|
289
|
+
return state === null || state === undefined ? (this._default ?? state) : state;
|
|
290
|
+
}
|
|
291
|
+
getExtraAttributesMap() { return escapeAttributes(this._extraAttributes); }
|
|
292
|
+
async getAlignment() { const alignment = await evaluate(this._alignment, this.context()); return alignment && ALIGNMENTS.has(alignment) ? alignment : null; }
|
|
293
|
+
getEmptyAttributes(base) { return mergeAttrs({ 'x-tooltip': tooltipJs(this._emptyTooltip) }, withClass(this.getExtraAttributesMap(), base)); }
|
|
294
|
+
emptyHtml(base) {
|
|
295
|
+
return `<div ${renderAttrs(this.getEmptyAttributes(base))}>${this._placeholder !== null ? `<p class="fi-in-placeholder">${e(this._placeholder)}</p>` : ''}</div>`;
|
|
296
|
+
}
|
|
297
|
+
/** Actions placed in the slots, so that they can be mounted (`schemaComponent` = entry key). */
|
|
298
|
+
getActions() {
|
|
299
|
+
const actions = [];
|
|
300
|
+
for (const slot of Object.values(this._slots)) {
|
|
301
|
+
let value = slot;
|
|
302
|
+
if (typeof value === 'function') {
|
|
303
|
+
try {
|
|
304
|
+
value = slot(this.context());
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (value instanceof Promise) {
|
|
310
|
+
value.catch(() => { });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const item of [].concat(value ?? [])) {
|
|
315
|
+
if (!item || typeof item !== 'object' || item instanceof Component || item instanceof HtmlString)
|
|
316
|
+
continue;
|
|
317
|
+
actions.push(...(typeof item.getActions === 'function' ? item.getActions() : [item]));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return actions;
|
|
321
|
+
}
|
|
322
|
+
async renderSlot(name) {
|
|
323
|
+
const slot = this._slots[name];
|
|
324
|
+
if (slot === undefined || slot === null)
|
|
325
|
+
return null;
|
|
326
|
+
const value = await evaluate(slot, this.context());
|
|
327
|
+
const items = [].concat(value ?? []).map((item) => (typeof item === 'string' || item instanceof HtmlString ? Text.make(item) : item));
|
|
328
|
+
return renderInlineItems(this, items, { alignment: name === 'afterLabel' || name === 'afterContent' ? 'end' : null, scope: { path: this.getStatePath(), containerPath: this.container.getStatePath() }, mountKey: this.getKey(), actionView: 'link' });
|
|
31
329
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
330
|
+
/** Places the content in the entry wrapper: label column, content column, slots and link. */
|
|
331
|
+
async wrapEmbeddedHtml(content) {
|
|
332
|
+
const ctx = this.context();
|
|
333
|
+
const label = await this.getLabel();
|
|
334
|
+
const hasLabel = label !== '';
|
|
335
|
+
const isLabelHidden = Boolean(await evaluate(this._hiddenLabel, ctx));
|
|
336
|
+
const hasInlineLabel = Boolean(await evaluate(this._inlineLabel, ctx));
|
|
337
|
+
const alignment = await this.getAlignment();
|
|
338
|
+
const url = await evaluate(this._url, ctx);
|
|
339
|
+
const href = typeof url === 'string' && url.trim() !== '' && isSafeUrl(url) ? url : null;
|
|
340
|
+
const [aboveLabel, belowLabel, beforeLabel, afterLabel, aboveContent, beforeContent, afterContent, belowContent] = await Promise.all(['aboveLabel', 'belowLabel', 'beforeLabel', 'afterLabel', 'aboveContent', 'beforeContent', 'afterContent', 'belowContent'].map((name) => this.renderSlot(name)));
|
|
341
|
+
const attributes = withClass(escapeAttributes(this._extraEntryWrapperAttributes), ['fi-in-entry', { 'fi-in-entry-has-inline-label': hasInlineLabel }]);
|
|
342
|
+
let html = `<div ${renderAttrs(attributes)}>`;
|
|
343
|
+
if (hasLabel && isLabelHidden)
|
|
344
|
+
html += `<div class="fi-in-entry-label fi-sr-only" role="term">${e(label)}</div>`;
|
|
345
|
+
if ((hasLabel && !isLabelHidden) || hasInlineLabel || aboveLabel || belowLabel || beforeLabel || afterLabel) {
|
|
346
|
+
html += `<div class="fi-in-entry-label-col">${aboveLabel ?? ''}`;
|
|
347
|
+
if ((hasLabel && !isLabelHidden) || beforeLabel || afterLabel) {
|
|
348
|
+
html += `<div class="fi-in-entry-label-ctn">${beforeLabel ?? ''}${hasLabel && !isLabelHidden ? `<div class="fi-in-entry-label" role="term">${e(label)}</div>` : ''}${afterLabel ?? ''}</div>`;
|
|
349
|
+
}
|
|
350
|
+
html += `${belowLabel ?? ''}</div>`;
|
|
351
|
+
}
|
|
352
|
+
html += '<div class="fi-in-entry-content-col">';
|
|
353
|
+
if (aboveContent !== null)
|
|
354
|
+
html += `<div class="fi-in-entry-content-slot">${aboveContent}</div>`;
|
|
355
|
+
html += '<div class="fi-in-entry-content-ctn" role="definition">';
|
|
356
|
+
if (beforeContent !== null)
|
|
357
|
+
html += `<div class="fi-in-entry-content-slot">${beforeContent}</div>`;
|
|
358
|
+
const contentClass = cls(['fi-in-entry-content', alignmentClass(alignment)]);
|
|
359
|
+
html += href !== null
|
|
360
|
+
? `<a href="${e(href)}"${(await evaluate(this._openUrlInNewTab, ctx)) ? ' target="_blank"' : ''} class="${contentClass}">${content}</a>`
|
|
361
|
+
: `<div class="${contentClass}">${content}</div>`;
|
|
362
|
+
html += `${afterContent ?? ''}</div>${belowContent ?? ''}</div></div>`;
|
|
363
|
+
return html;
|
|
364
|
+
}
|
|
365
|
+
/** `hiddenJs()` / `visibleJs()`: the grid wrapper becomes a `<fieldset>` toggled by Alpine, as in Filament. */
|
|
366
|
+
async toSchemaHtml(isVisible) {
|
|
367
|
+
const html = await super.toSchemaHtml(isVisible);
|
|
368
|
+
const hidden = this._hiddenJs?.trim() || null, visible = this._visibleJs?.trim() || null;
|
|
369
|
+
const visibility = hidden && visible ? `(! (${hidden})) && (${visible})` : hidden ? `! (${hidden})` : visible;
|
|
370
|
+
const start = html.indexOf('<div');
|
|
371
|
+
const end = /><div class="fi-sc-component[ "]/.exec(html)?.index;
|
|
372
|
+
if (!isVisible || visibility === null || start < 0 || end === undefined || html.slice(start, end).includes('fi-sc-visibility-fieldset'))
|
|
373
|
+
return html;
|
|
374
|
+
let tag = html.slice(start, end).replace(/^<div/, '<fieldset disabled role="none"');
|
|
375
|
+
const toggle = ` x-bind:class="${e(`{ 'fi-hidden': ! (${visibility}) }`)}" x-bind:disabled="${e(`! (${visibility})`)}" x-cloak`;
|
|
376
|
+
const dataStart = tag.indexOf(' x-data="');
|
|
377
|
+
if (dataStart >= 0) {
|
|
378
|
+
const dataEnd = tag.indexOf('"', dataStart + 9);
|
|
379
|
+
tag = `${tag.slice(0, dataEnd + 1)}${toggle}${tag.slice(dataEnd + 1)}`;
|
|
380
|
+
}
|
|
381
|
+
else
|
|
382
|
+
tag += toggle;
|
|
383
|
+
tag = /\sclass="/.test(tag) ? tag.replace(/(\sclass="[^"]*)"/, '$1 fi-sc-visibility-fieldset"') : `${tag} class="fi-sc-visibility-fieldset"`;
|
|
384
|
+
const close = html.lastIndexOf('</div>');
|
|
385
|
+
return `${html.slice(0, start)}${tag}>${html.slice(end + 1, close)}</fieldset>${html.slice(close + '</div>'.length)}`;
|
|
37
386
|
}
|
|
38
387
|
}
|
|
39
388
|
let htmlSanitizer = null;
|
|
40
389
|
const TIGHT_ITEM = new RegExp('<li><p>((?:(?!</?p>|</?li>)[^])*)</p></li>', 'g');
|
|
41
|
-
/**
|
|
390
|
+
/**
|
|
391
|
+
* Sanitizer applied to the HTML an entry renders as is (`html()`, `markdown()`, `prose()`).
|
|
392
|
+
* Without one, `sanitizeHtml()` from `@navette-js/support` is used.
|
|
393
|
+
*/
|
|
42
394
|
export const useHtmlSanitizer = (sanitizer) => { htmlSanitizer = sanitizer; };
|
|
43
|
-
|
|
395
|
+
const sanitize = (html) => (htmlSanitizer ?? sanitizeHtml)(html);
|
|
396
|
+
/** Read-only text: badge, color, icon, money, number, date, list, Markdown and HTML. */
|
|
44
397
|
export class TextEntry extends Entry {
|
|
45
398
|
_badge = false;
|
|
46
399
|
_color = null;
|
|
47
400
|
_icon = null;
|
|
401
|
+
_iconColor = null;
|
|
402
|
+
_iconPosition = 'before';
|
|
48
403
|
_format = null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
404
|
+
_formatter = null;
|
|
405
|
+
_formatterKind = null;
|
|
406
|
+
_isDate = false;
|
|
407
|
+
_isMoney = false;
|
|
408
|
+
_isNumeric = false;
|
|
409
|
+
_timezone = null;
|
|
410
|
+
_prefix = null;
|
|
411
|
+
_suffix = null;
|
|
412
|
+
_prose = false;
|
|
413
|
+
_markdown = false;
|
|
414
|
+
_html = false;
|
|
415
|
+
_characterLimit = null;
|
|
416
|
+
_wordLimit = null;
|
|
417
|
+
_listWithLineBreaks = false;
|
|
418
|
+
_bulleted = false;
|
|
419
|
+
_listLimit = null;
|
|
420
|
+
_expandableLimitedList = false;
|
|
421
|
+
_separator = null;
|
|
422
|
+
_copyable = false;
|
|
423
|
+
_copyableState = null;
|
|
424
|
+
_copyMessage = null;
|
|
425
|
+
_copyMessageDuration = 2000;
|
|
426
|
+
_size = null;
|
|
427
|
+
_weight = null;
|
|
428
|
+
_fontFamily = null;
|
|
429
|
+
_lineClamp = null;
|
|
430
|
+
_wrap = true;
|
|
52
431
|
static make(name) { return configured(new TextEntry(name)); }
|
|
53
432
|
badge(condition = true) { this._badge = condition; return this; }
|
|
433
|
+
/** Text (or badge) color: a name, or a function of the record and the state item. */
|
|
54
434
|
color(color) { this._color = color; return this; }
|
|
55
|
-
|
|
435
|
+
/** Heroicon before the text: a name, or a function of the record and the state item returning a name or `null`. */
|
|
436
|
+
icon(icon) { this._icon = icon; return this; }
|
|
437
|
+
iconColor(color) { this._iconColor = color; return this; }
|
|
438
|
+
iconPosition(position) { this._iconPosition = position === 'after' ? 'after' : 'before'; return this; }
|
|
439
|
+
/** Displayed text computed from the raw state; it takes precedence over date, number and money formatting. */
|
|
56
440
|
formatStateUsing(formatter) { this._format = formatter; return this; }
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
441
|
+
setFormatter(kind, formatter) {
|
|
442
|
+
if (formatter) {
|
|
443
|
+
this._formatter = formatter;
|
|
444
|
+
this._formatterKind = kind;
|
|
445
|
+
}
|
|
446
|
+
else if (this._formatterKind === kind) {
|
|
447
|
+
this._formatter = null;
|
|
448
|
+
this._formatterKind = null;
|
|
449
|
+
}
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
dateFormatter(kind, format, timezone) {
|
|
453
|
+
return (state) => {
|
|
454
|
+
const date = parseDate(state);
|
|
455
|
+
if (!date)
|
|
456
|
+
return String(state);
|
|
457
|
+
const zone = timezone ?? this._timezone ?? 'UTC';
|
|
458
|
+
return kind === 'since' ? since(date) : kind === 'iso' ? formatIsoDate(date, format, zone) : formatPhpDate(date, format, zone);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/** Date in a PHP format, `M j, Y` by default ("Mar 5, 2024"). `false` turns date formatting off. */
|
|
462
|
+
date(format = null, timezone = null) {
|
|
463
|
+
this._isDate = format !== false;
|
|
464
|
+
return this.setFormatter('date', format === false ? null : this.dateFormatter('php', typeof format === 'string' ? format : 'M j, Y', timezone));
|
|
465
|
+
}
|
|
466
|
+
/** Date and time, `M j, Y H:i:s` by default. */
|
|
467
|
+
dateTime(format = null, timezone = null) {
|
|
468
|
+
return this.setFormatter('date', format === false ? null : this.dateFormatter('php', typeof format === 'string' ? format : 'M j, Y H:i:s', timezone));
|
|
469
|
+
}
|
|
470
|
+
/** Time, `H:i:s` by default. */
|
|
471
|
+
time(format = null, timezone = null) { return this.setFormatter('date', this.dateFormatter('php', format ?? 'H:i:s', timezone)); }
|
|
472
|
+
/** Date in a Carbon `isoFormat()` format, `L` by default ("03/05/2024"). */
|
|
473
|
+
isoDate(format = null, timezone = null) { return this.setFormatter('date', this.dateFormatter('iso', format ?? 'L', timezone)); }
|
|
474
|
+
/** Date and time in an `isoFormat()` format, `LLL` by default ("March 5, 2024 10:11 AM"). */
|
|
475
|
+
isoDateTime(format = null, timezone = null) { return this.setFormatter('date', this.dateFormatter('iso', format ?? 'LLL', timezone)); }
|
|
476
|
+
/** Time in an `isoFormat()` format, `LT` by default ("10:11 AM"). */
|
|
477
|
+
isoTime(format = null, timezone = null) { return this.setFormatter('date', this.dateFormatter('iso', format ?? 'LT', timezone)); }
|
|
478
|
+
/** Relative date: "2 days ago". */
|
|
479
|
+
since(timezone = null) { return this.setFormatter('date', this.dateFormatter('since', '', timezone)); }
|
|
480
|
+
dateTooltipFor(kind, format, timezone) {
|
|
481
|
+
const formatter = this.dateFormatter(kind, format, timezone);
|
|
482
|
+
return this.tooltip((_record, state) => (isBlank(state) ? null : formatter(state)));
|
|
483
|
+
}
|
|
484
|
+
/** Tooltip with the date in a PHP format (`M j, Y` by default). */
|
|
485
|
+
dateTooltip(format = null, timezone = null) { return this.dateTooltipFor('php', format ?? 'M j, Y', timezone); }
|
|
486
|
+
dateTimeTooltip(format = null, timezone = null) { return this.dateTooltipFor('php', format ?? 'M j, Y H:i:s', timezone); }
|
|
487
|
+
timeTooltip(format = null, timezone = null) { return this.dateTooltipFor('php', format ?? 'H:i:s', timezone); }
|
|
488
|
+
isoDateTooltip(format = null, timezone = null) { return this.dateTooltipFor('iso', format ?? 'L', timezone); }
|
|
489
|
+
isoDateTimeTooltip(format = null, timezone = null) { return this.dateTooltipFor('iso', format ?? 'LLL', timezone); }
|
|
490
|
+
isoTimeTooltip(format = null, timezone = null) { return this.dateTooltipFor('iso', format ?? 'LT', timezone); }
|
|
491
|
+
sinceTooltip(timezone = null) { return this.dateTooltipFor('since', '', timezone); }
|
|
492
|
+
/** IANA time zone in which dates are displayed (`UTC` by default). */
|
|
493
|
+
timezone(timezone) { this._timezone = timezone; return this; }
|
|
494
|
+
/**
|
|
495
|
+
* Formatted number. `numeric(2)`: two decimal places. Options: `decimalPlaces`, `maxDecimalPlaces`, `locale` (`en-US` by default),
|
|
496
|
+
* or `decimalSeparator` / `thousandsSeparator` for fixed separators. `false` turns number formatting off.
|
|
497
|
+
*/
|
|
498
|
+
numeric(options = true) {
|
|
499
|
+
this._isNumeric = options !== false;
|
|
500
|
+
if (options === false)
|
|
501
|
+
return this.setFormatter('numeric', null);
|
|
502
|
+
const settings = typeof options === 'number' ? { decimalPlaces: options } : options && typeof options === 'object' ? options : {};
|
|
503
|
+
return this.setFormatter('numeric', (state) => {
|
|
504
|
+
if (!isNumericValue(state))
|
|
505
|
+
return String(state);
|
|
506
|
+
const value = Number(state);
|
|
507
|
+
const decimals = fractionDigits(settings.decimalPlaces);
|
|
508
|
+
if ((settings.decimalSeparator ?? null) !== null || (settings.thousandsSeparator ?? null) !== null)
|
|
509
|
+
return numberFormat(value, decimals ?? 0, settings.decimalSeparator ?? '.', settings.thousandsSeparator ?? ',');
|
|
510
|
+
const max = fractionDigits(settings.maxDecimalPlaces);
|
|
511
|
+
const digits = max !== null ? { maximumFractionDigits: max } : decimals !== null ? { minimumFractionDigits: decimals, maximumFractionDigits: decimals } : {};
|
|
512
|
+
return new Intl.NumberFormat(settings.locale ?? 'en-US', digits).format(value);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
70
515
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
516
|
+
* Amount of money in a currency (`usd` by default). `divideBy`: divides the state first (`100` for cents);
|
|
517
|
+
* `locale` (`en-US` by default) and `decimalPlaces` may be passed after it, or in an options object.
|
|
73
518
|
*/
|
|
519
|
+
money(currency = 'usd', divideBy = 0, locale = null, decimalPlaces = null) {
|
|
520
|
+
this._isMoney = true;
|
|
521
|
+
const settings = divideBy !== null && typeof divideBy === 'object' ? divideBy : { divideBy: divideBy, locale, decimalPlaces };
|
|
522
|
+
return this.setFormatter('money', (state, record) => {
|
|
523
|
+
if (!isNumericValue(state))
|
|
524
|
+
return String(state);
|
|
525
|
+
let value = Number(state);
|
|
526
|
+
if (settings.divideBy)
|
|
527
|
+
value /= settings.divideBy;
|
|
528
|
+
const code = (typeof currency === 'function' ? currency(record) : currency) || 'usd';
|
|
529
|
+
const decimals = fractionDigits(settings.decimalPlaces);
|
|
530
|
+
return new Intl.NumberFormat(settings.locale ?? 'en-US', { style: 'currency', currency: code.toUpperCase(), ...(decimals !== null ? { minimumFractionDigits: decimals, maximumFractionDigits: decimals } : {}) }).format(value);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** Text before the formatted state. A string is escaped; an `HtmlString` is inserted as is. */
|
|
534
|
+
prefix(text) { this._prefix = text; return this; }
|
|
535
|
+
/** Text after the formatted state. A string is escaped; an `HtmlString` is inserted as is. */
|
|
536
|
+
suffix(text) { this._suffix = text; return this; }
|
|
537
|
+
/** Truncates the text to `length` characters, followed by `end`. */
|
|
538
|
+
limit(length = 100, end = '...') { this._characterLimit = length && length > 0 ? { length: Math.floor(length), end } : null; return this; }
|
|
539
|
+
/** Truncates the text to a number of words, followed by `end`. */
|
|
540
|
+
words(words = 100, end = '...') { this._wordLimit = words && words > 0 ? { words: Math.floor(words), end } : null; return this; }
|
|
541
|
+
/** Clamps the text to a number of lines. */
|
|
542
|
+
lineClamp(lines) { this._lineClamp = lines && lines > 0 ? Math.floor(lines) : null; return this; }
|
|
543
|
+
/** `false`: long text does not wrap onto several lines. */
|
|
544
|
+
wrap(condition = true) { this._wrap = condition; return this; }
|
|
545
|
+
/** Text size: `xs`, `sm` (default), `md` (or `base`) or `lg`. */
|
|
546
|
+
size(size) { this._size = size; return this; }
|
|
547
|
+
/** Font weight: `thin`, `extralight`, `light`, `normal`, `medium`, `semibold`, `bold`, `extrabold` or `black`. */
|
|
548
|
+
weight(weight) { this._weight = weight; return this; }
|
|
549
|
+
/** Font family: `sans`, `serif` or `mono`. */
|
|
550
|
+
fontFamily(family) { this._fontFamily = family; return this; }
|
|
551
|
+
/** Typographic styling of rich content (`fi-prose`); the state is rendered as sanitized HTML. */
|
|
552
|
+
prose(condition = true) { this._prose = condition; return this; }
|
|
553
|
+
/** Renders Markdown as HTML. HTML in the state (from a rich editor) is sanitized instead. */
|
|
74
554
|
markdown(condition = true) { this._markdown = condition; return this; }
|
|
75
|
-
|
|
76
|
-
|
|
555
|
+
/** Renders the state as HTML, after it has been sanitized. */
|
|
556
|
+
html(condition = true) { this._html = condition; return this; }
|
|
557
|
+
/** Each value of a list on its own line. */
|
|
558
|
+
listWithLineBreaks(condition = true) { this._listWithLineBreaks = condition; return this; }
|
|
559
|
+
/** Bulleted list (implies line breaks). */
|
|
560
|
+
bulleted(condition = true) { this._bulleted = condition; return this; }
|
|
561
|
+
/** Shows only the first values of a list, followed by "and N more". */
|
|
562
|
+
limitList(limit = 3) { this._listLimit = limit && limit > 0 ? Math.floor(limit) : null; return this; }
|
|
563
|
+
/** With `limitList()` and line breaks: a link reveals the other values. */
|
|
564
|
+
expandableLimitedList(condition = true) { this._expandableLimitedList = condition; return this; }
|
|
565
|
+
/** Splits a string state into a list on this separator. */
|
|
566
|
+
separator(separator = ',') { this._separator = separator; return this; }
|
|
77
567
|
/** Clicking copies the value to the clipboard. */
|
|
78
568
|
copyable(condition = true) { this._copyable = condition; return this; }
|
|
79
|
-
/**
|
|
80
|
-
|
|
569
|
+
/** Value copied, when it differs from the displayed text. */
|
|
570
|
+
copyableState(state) { this._copyableState = state; return this; }
|
|
571
|
+
copyMessage(message) { this._copyMessage = message; return this; }
|
|
572
|
+
copyMessageDuration(milliseconds) { this._copyMessageDuration = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds)) : 2000; return this; }
|
|
573
|
+
/** Displayed text of a state item, and whether it is (sanitized) HTML. */
|
|
574
|
+
formatItem(item, record) {
|
|
575
|
+
let isHtml = this._html || this._markdown || this._prose;
|
|
576
|
+
const value = this._format ? this._format({ state: item, record }) : this._formatter && !isBlank(item) ? this._formatter(item, record) : item;
|
|
577
|
+
let text;
|
|
578
|
+
if (value instanceof HtmlString) {
|
|
579
|
+
isHtml = true;
|
|
580
|
+
text = value.html;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
text = value === null || value === undefined ? '' : value instanceof Date ? formatPhpDate(value, 'Y-m-d H:i:s') : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
584
|
+
// Markdown: content starting with a tag is HTML (from a rich editor), sanitized; otherwise HTML in it is shown as text.
|
|
585
|
+
if (isHtml)
|
|
586
|
+
text = this._markdown ? (/^\s*<[a-z][^>]*>/i.test(text) ? sanitize(text).replace(TIGHT_ITEM, '<li>$1</li>') : markdownToHtml(text)) : sanitize(text);
|
|
587
|
+
}
|
|
588
|
+
if (!isHtml) {
|
|
589
|
+
if (this._characterLimit && [...text].length > this._characterLimit.length)
|
|
590
|
+
text = `${[...text].slice(0, this._characterLimit.length).join('').trimEnd()}${this._characterLimit.end}`;
|
|
591
|
+
if (this._wordLimit) {
|
|
592
|
+
const match = new RegExp(`^\\s*(?:\\S+\\s*){1,${this._wordLimit.words}}`, 'u').exec(text);
|
|
593
|
+
if (match && match[0].length !== text.length)
|
|
594
|
+
text = `${match[0].trimEnd()}${this._wordLimit.end}`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!isHtml && (this._prefix instanceof HtmlString || this._suffix instanceof HtmlString)) {
|
|
598
|
+
isHtml = true;
|
|
599
|
+
text = e(text);
|
|
600
|
+
}
|
|
601
|
+
const affix = (value) => (value === null || value === '' ? '' : value instanceof HtmlString ? value.html : isHtml ? e(value) : value);
|
|
602
|
+
return { text: `${affix(this._prefix)}${text}${affix(this._suffix)}`, isHtml };
|
|
603
|
+
}
|
|
81
604
|
async toHtml() {
|
|
82
605
|
const record = this.context().record;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Empty value: bare container (with `fi-numeric` for money).
|
|
88
|
-
if (empty)
|
|
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
|
-
let shown = String(state);
|
|
91
|
-
if (this._numeric)
|
|
92
|
-
shown = new Intl.NumberFormat('en-US').format(Number(state));
|
|
93
|
-
if (this._money)
|
|
94
|
-
shown = new Intl.NumberFormat('en-US', { style: 'currency', currency: this._money }).format(Number(state));
|
|
95
|
-
if (this._date)
|
|
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()`: "Jan 5, 2024 13:45:00" (`M j, Y H:i:s`).
|
|
98
|
-
if (this._dateTime)
|
|
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')` or `time()` (`H:i:s`): the time part of an `H:i:s` or `Y-m-d H:i:s` value.
|
|
101
|
-
if (this._time)
|
|
102
|
-
shown = (shown.length > 8 ? shown.slice(11, 19) : shown).slice(0, this._time === 'H:i' ? 5 : 8);
|
|
103
|
-
if (this._format)
|
|
104
|
-
shown = this._format({ state, record });
|
|
105
|
-
if (this._suffix !== null)
|
|
106
|
-
shown += this._suffix;
|
|
107
|
-
if (this._markdown) {
|
|
108
|
-
const source = String(state);
|
|
109
|
-
const html = /<[a-z][^>]*>/i.test(source)
|
|
110
|
-
// Tight lists: a list item's single paragraph is unwrapped.
|
|
111
|
-
? (htmlSanitizer ? htmlSanitizer(source).replace(TIGHT_ITEM, '<li>$1</li>') : e(source))
|
|
112
|
-
: source.split(/\n{2,}/).map((paragraph) => `<p>${e(paragraph.trim()).replace(/\n/g, '<br />')}</p>`).join('');
|
|
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>`);
|
|
606
|
+
let state = await this.getState();
|
|
607
|
+
if (typeof state === 'string' && this._separator) {
|
|
608
|
+
const parts = state.split(this._separator);
|
|
609
|
+
state = parts.length === 1 && parts[0].trim() === '' ? [] : parts;
|
|
114
610
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
611
|
+
const isNumeric = this._isMoney || this._isNumeric;
|
|
612
|
+
if (isBlank(state)) {
|
|
613
|
+
// Without placeholder, Filament renders an empty item, except for money, date and badge entries reading the record.
|
|
614
|
+
if (this._placeholder === null && (this._state !== null || (!this._isMoney && !this._isDate && !this._badge)))
|
|
615
|
+
return this.wrapEmbeddedHtml(this.emptyHtml(['fi-in-text-item', 'fi-size-sm', 'fi-in-text', { 'fi-wrapped': this._wrap }]));
|
|
616
|
+
return this.wrapEmbeddedHtml(this.emptyHtml(['fi-in-text', { 'fi-numeric': isNumeric }]));
|
|
120
617
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
618
|
+
let items = Array.isArray(state) ? state : [state];
|
|
619
|
+
const stateCount = items.length;
|
|
620
|
+
let count = stateCount;
|
|
621
|
+
const hasLineBreaks = this._listWithLineBreaks || this._bulleted;
|
|
622
|
+
const listLimit = this._listLimit ?? count;
|
|
623
|
+
const overLimit = listLimit && count > listLimit ? count - listLimit : 0;
|
|
624
|
+
if (overLimit && (!hasLineBreaks || !this._expandableLimitedList))
|
|
625
|
+
items = items.slice(0, listLimit);
|
|
626
|
+
// Several values without line breaks: one comma-separated item.
|
|
627
|
+
let collapsed = null;
|
|
628
|
+
if (count > 1 && !hasLineBreaks && !this._badge) {
|
|
629
|
+
collapsed = items.map((item) => { const formatted = this.formatItem(item, record); return formatted.isHtml ? formatted.text : e(formatted.text); }).join(', ');
|
|
630
|
+
items = [collapsed];
|
|
631
|
+
count = 1;
|
|
126
632
|
}
|
|
633
|
+
const alignment = await this.getAlignment();
|
|
127
634
|
const tr = (key, params) => t(`filament::components/copyable.${key}`, params);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
635
|
+
const itemData = (item) => {
|
|
636
|
+
const formatted = collapsed !== null ? { text: collapsed, isHtml: true } : this.formatItem(item, record);
|
|
637
|
+
const plain = formatted.isHtml ? stripTags(formatted.text) : formatted.text;
|
|
638
|
+
const color = resolveItem(this._color, record, item) ?? (this._badge ? 'primary' : null);
|
|
639
|
+
const size = textSize(resolveItem(this._size, record, item));
|
|
640
|
+
const iconColor = resolveItem(this._iconColor, record, item);
|
|
641
|
+
const iconHtml = renderIcon(resolveItem(this._icon, record, item), { size: size === 'md' || size === 'lg' ? size : 'sm', attributes: new Attributes().class(iconColor && iconColor !== 'gray' ? ['fi-color', `fi-color-${safeName(iconColor)}`] : []) });
|
|
642
|
+
const isCopyable = Boolean(resolveItem(this._copyable, record, item));
|
|
643
|
+
const tooltip = resolveItem(this._tooltip, record, item);
|
|
644
|
+
let attributes = { class: cls(['fi-in-text-item', { 'fi-prose': this._prose || this._markdown }, fontClass(resolveItem(this._fontFamily, record, item), FONT_FAMILIES)]) };
|
|
645
|
+
if (!this._badge) {
|
|
646
|
+
attributes = withClass(attributes, [`fi-size-${size}`, fontClass(resolveItem(this._weight, record, item), FONT_WEIGHTS)]);
|
|
647
|
+
if (this._lineClamp)
|
|
648
|
+
attributes = withStyle(attributes, [`--line-clamp: ${this._lineClamp}`]);
|
|
649
|
+
attributes = withClass(attributes, itemColorClasses(color));
|
|
650
|
+
}
|
|
651
|
+
let content = null;
|
|
652
|
+
if (this._badge || isCopyable || !isBlank(tooltip)) {
|
|
653
|
+
let click = null, label = null;
|
|
654
|
+
if (isCopyable) {
|
|
655
|
+
const copied = js(resolveItem(this._copyableState, record, item) ?? formatted.text);
|
|
656
|
+
const message = js(this._copyMessage ?? tr('messages.copied'));
|
|
657
|
+
click = `if (isStandalone) {\n copy(${copied}, ${message}, ${this._copyMessageDuration}, ${js(tr('messages.failed'))})\n} else {\n window.navigator.clipboard.writeText(${copied})\n $tooltip(${message}, {\n theme: $store.theme,\n timeout: ${this._copyMessageDuration},\n })\n}`;
|
|
658
|
+
label = `isStandalone ? ${js(tr('label', { label: plain }))} : null`;
|
|
659
|
+
}
|
|
660
|
+
content = mergeAttrs({ 'x-data': isCopyable ? 'filamentCopyable' : null, 'x-bind': isCopyable ? 'bindings' : null, 'x-bind:aria-label': label, 'x-on:click': click, 'x-tooltip': tooltipJs(tooltip) }, {});
|
|
661
|
+
content = withClass(content, [{ 'fi-copyable': isCopyable }]);
|
|
662
|
+
if (this._badge)
|
|
663
|
+
content = withClass(content, ['fi-badge', `fi-size-${size}`, ...badgeColorClasses(color)]);
|
|
664
|
+
}
|
|
665
|
+
const html = formatted.isHtml ? formatted.text : e(formatted.text);
|
|
666
|
+
const inner = `${content ? `<span ${renderAttrs(content)}>` : ''}${this._iconPosition === 'before' ? iconHtml : ''}${html}${this._iconPosition === 'after' ? iconHtml : ''}${content ? '</span>' : ''}`;
|
|
667
|
+
return { attributes, inner };
|
|
668
|
+
};
|
|
669
|
+
let container = withClass(this.getExtraAttributesMap(), ['fi-in-text', { 'fi-numeric': isNumeric }]);
|
|
670
|
+
container = withClass(container, [{ 'fi-in-text-has-badges': this._badge }, { 'fi-wrapped': this._wrap }, alignmentClass(alignment)]);
|
|
671
|
+
if (count === 1 && !this._bulleted) {
|
|
672
|
+
const { attributes, inner } = itemData(items[0]);
|
|
673
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(mergeAttrs(attributes, container))}>${inner}</div>`);
|
|
674
|
+
}
|
|
675
|
+
container = withClass(container, [{ 'fi-bulleted': this._bulleted }, { 'fi-in-text-has-line-breaks': hasLineBreaks }]);
|
|
676
|
+
if (overLimit) {
|
|
677
|
+
const expandable = hasLineBreaks && this._expandableLimitedList;
|
|
678
|
+
container = withClass(mergeAttrs({ 'x-data': expandable ? '{ isLimited: true }' : null }, container), ['fi-in-text-list-limited']);
|
|
679
|
+
const listItems = items.map((item, index) => {
|
|
680
|
+
const { attributes, inner } = itemData(item);
|
|
681
|
+
return `<li ${index >= listLimit ? 'x-show="! isLimited" x-cloak x-transition ' : ''}${renderAttrs(attributes)}>${inner}</li>`;
|
|
682
|
+
}).join('');
|
|
683
|
+
const message = expandable
|
|
684
|
+
? `<div role="button" x-bind:aria-expanded="(! isLimited).toString()" x-on:click.prevent.stop="isLimited = false" x-show="isLimited" class="fi-link fi-size-xs">${e(tc('filament-infolists::components.entries.text.actions.expand_list', overLimit))}</div><div role="button" x-bind:aria-expanded="(! isLimited).toString()" x-on:click.prevent.stop="isLimited = true" x-cloak x-show="! isLimited" class="fi-link fi-size-xs">${e(tc('filament-infolists::components.entries.text.actions.collapse_list', overLimit))}</div>`
|
|
685
|
+
: e(tc('filament-infolists::components.entries.text.more_list_items', overLimit));
|
|
686
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(container)}><ul>${listItems}</ul><div class="fi-in-text-list-limited-message">${message}</div></div>`);
|
|
687
|
+
}
|
|
688
|
+
const listItems = items.map((item) => { const { attributes, inner } = itemData(item); return `<li ${renderAttrs(attributes)}>${inner}</li>`; }).join('');
|
|
689
|
+
return this.wrapEmbeddedHtml(`<ul ${renderAttrs(container)}>${listItems}</ul>`);
|
|
140
690
|
}
|
|
141
691
|
}
|
|
142
|
-
/**
|
|
692
|
+
/** Only hex or simple functional colors are injected into the style attribute. */
|
|
693
|
+
const sanitizeCssColor = (value) => (typeof value === 'string' && /^(#[0-9a-fA-F]{3,8}|(rgb|hsl|oklch)a?\([0-9.,%\s/deg-]+\))$/.test(value.trim()) ? value.trim() : null);
|
|
694
|
+
/** Color swatch: only hex or simple functional colors are displayed. */
|
|
143
695
|
export class ColorEntry extends Entry {
|
|
144
|
-
|
|
696
|
+
_copyable = false;
|
|
697
|
+
_copyableState = null;
|
|
698
|
+
_copyMessage = null;
|
|
699
|
+
_copyMessageDuration = 2000;
|
|
700
|
+
_wrap = true;
|
|
145
701
|
static make(name) { return configured(new ColorEntry(name)); }
|
|
146
|
-
|
|
702
|
+
/** Clicking the swatch copies the color to the clipboard. */
|
|
703
|
+
copyable(condition = true) { this._copyable = condition; return this; }
|
|
704
|
+
copyableState(state) { this._copyableState = state; return this; }
|
|
705
|
+
copyMessage(message) { this._copyMessage = message; return this; }
|
|
706
|
+
copyMessageDuration(milliseconds) { this._copyMessageDuration = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds)) : 2000; return this; }
|
|
707
|
+
wrap(condition = true) { this._wrap = condition; return this; }
|
|
147
708
|
async toHtml() {
|
|
709
|
+
const record = this.context().record;
|
|
148
710
|
const state = await this.getState();
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
711
|
+
const colors = (Array.isArray(state) ? state : [state]).map(sanitizeCssColor).filter((color) => color !== null);
|
|
712
|
+
if (!colors.length) {
|
|
713
|
+
// An empty or unsupported value: the wrapping container, with the placeholder if any.
|
|
714
|
+
return this.wrapEmbeddedHtml(this.emptyHtml(['fi-in-color', { 'fi-wrapped': this._wrap }]));
|
|
715
|
+
}
|
|
716
|
+
const alignment = await this.getAlignment();
|
|
717
|
+
const tr = (key, params) => t(`filament::components/copyable.${key}`, params);
|
|
718
|
+
const swatches = colors.map((color) => {
|
|
719
|
+
const isCopyable = Boolean(resolveItem(this._copyable, record, color));
|
|
720
|
+
const tooltip = resolveItem(this._tooltip, record, color);
|
|
721
|
+
const label = isCopyable ? null : !isBlank(tooltip) ? String(tooltip) : color;
|
|
722
|
+
const copied = js(resolveItem(this._copyableState, record, color) ?? color);
|
|
723
|
+
const message = js(this._copyMessage ?? tr('messages.copied'));
|
|
724
|
+
const attributes = withStyle(withClass(mergeAttrs({
|
|
725
|
+
role: label !== null ? 'img' : null,
|
|
726
|
+
'aria-label': label !== null ? e(label) : null,
|
|
727
|
+
'x-data': isCopyable ? 'filamentCopyable' : null,
|
|
728
|
+
'x-bind': isCopyable ? 'bindings' : null,
|
|
729
|
+
'x-bind:aria-label': isCopyable ? `isStandalone ? ${js(tr('label', { label: color }))} : null` : null,
|
|
730
|
+
'x-on:click': isCopyable ? `if (isStandalone) {\n copy(${copied}, ${message}, ${this._copyMessageDuration}, ${js(tr('messages.failed'))})\n} else {\n window.navigator.clipboard.writeText(${copied})\n $tooltip(${message}, {\n theme: $store.theme,\n timeout: ${this._copyMessageDuration},\n })\n}` : null,
|
|
731
|
+
'x-tooltip': tooltipJs(tooltip),
|
|
732
|
+
}, {}), ['fi-in-color-item', { 'fi-copyable': isCopyable }]), [`background-color: ${color}`]);
|
|
733
|
+
return `<div ${renderAttrs(attributes)}></div>`;
|
|
734
|
+
}).join('');
|
|
735
|
+
const container = withClass(this.getExtraAttributesMap(), ['fi-in-color', { 'fi-wrapped': this._wrap }, alignmentClass(alignment)]);
|
|
736
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(container)}>${swatches}</div>`);
|
|
153
737
|
}
|
|
154
738
|
}
|
|
155
|
-
/**
|
|
739
|
+
/** `<img>` attributes allowed in `extraImgAttributes()`: no event handler, style or source. */
|
|
740
|
+
const IMG_ATTRIBUTES = new Set(['class', 'id', 'title', 'loading', 'decoding', 'fetchpriority', 'referrerpolicy', 'crossorigin', 'width', 'height', 'draggable', 'role']);
|
|
741
|
+
const isAllowedImgAttribute = (name) => IMG_ATTRIBUTES.has(name.toLowerCase()) || /^(data|aria)-[a-z0-9_.-]+$/i.test(name);
|
|
742
|
+
/** Read-only images: only same-site (`/…`) or http(s) URLs are rendered. */
|
|
156
743
|
export class ImageEntry extends Entry {
|
|
157
|
-
_height =
|
|
744
|
+
_height = null;
|
|
745
|
+
_width = null;
|
|
158
746
|
_alt = null;
|
|
159
747
|
_circular = false;
|
|
748
|
+
_square = false;
|
|
749
|
+
_stacked = false;
|
|
750
|
+
_ring = null;
|
|
751
|
+
_overlap = null;
|
|
752
|
+
_limit = null;
|
|
753
|
+
_limitedRemainingText = false;
|
|
754
|
+
_limitedRemainingTextSize = null;
|
|
755
|
+
_defaultImageUrl = null;
|
|
756
|
+
_extraImgAttributes = {};
|
|
757
|
+
_wrap = true;
|
|
160
758
|
static make(name) { return configured(new ImageEntry(name)); }
|
|
161
|
-
/**
|
|
162
|
-
imageHeight(height) { if (
|
|
163
|
-
this._height =
|
|
759
|
+
/** Height of the images: a CSS length (`40px`, `2.5rem`…) or a number of pixels; `8rem` by default. Any other value is ignored. */
|
|
760
|
+
imageHeight(height) { if (height === null)
|
|
761
|
+
this._height = null;
|
|
762
|
+
else
|
|
763
|
+
this._height = cssLength(height) ?? this._height; return this; }
|
|
764
|
+
/** Width of the images: a CSS length or a number of pixels. Any other value is ignored. */
|
|
765
|
+
imageWidth(width) { if (width === null)
|
|
766
|
+
this._width = null;
|
|
767
|
+
else
|
|
768
|
+
this._width = cssLength(width) ?? this._width; return this; }
|
|
769
|
+
imageSize(size) { return this.imageWidth(size).imageHeight(size); }
|
|
164
770
|
alt(text) { this._alt = text; return this; }
|
|
165
771
|
circular(condition = true) { this._circular = condition; return this; }
|
|
772
|
+
/** Square images: the width is the height. */
|
|
773
|
+
square(condition = true) { this._square = condition; return this; }
|
|
774
|
+
/** Overlapping images (`2.5rem` high by default). */
|
|
775
|
+
stacked(condition = true) { this._stacked = condition; return this; }
|
|
776
|
+
/** Ring width of stacked images, from 0 to 8. */
|
|
777
|
+
ring(width) { this._ring = width === null ? null : Math.max(0, Math.min(8, Math.floor(width))); return this; }
|
|
778
|
+
/** Overlap of stacked images, from 0 to 8 (2 by default). */
|
|
779
|
+
overlap(overlap) { this._overlap = overlap === null ? null : Math.max(0, Math.min(8, Math.floor(overlap))); return this; }
|
|
780
|
+
/** Maximum number of images displayed. */
|
|
781
|
+
limit(limit = 3) { this._limit = limit && limit > 0 ? Math.floor(limit) : null; return this; }
|
|
782
|
+
/** Shows "+N" after the limited images; `size`: `xs`, `sm` (default), `md` or `lg`. */
|
|
783
|
+
limitedRemainingText(condition = true, size = null) { this._limitedRemainingText = condition; this._limitedRemainingTextSize = size; return this; }
|
|
784
|
+
/** Image displayed when the state is empty. */
|
|
785
|
+
defaultImageUrl(url) { this._defaultImageUrl = url; return this; }
|
|
786
|
+
/** Extra `<img>` attributes (`loading`, `class`, `data-*`, `aria-*`…). Event handlers, `style` and sources are ignored; values are escaped. */
|
|
787
|
+
extraImgAttributes(attributes) { Object.assign(this._extraImgAttributes, attributes); return this; }
|
|
788
|
+
wrap(condition = true) { this._wrap = condition; return this; }
|
|
166
789
|
async toHtml() {
|
|
790
|
+
const ctx = this.context();
|
|
167
791
|
const state = await this.getState();
|
|
168
|
-
const
|
|
169
|
-
|
|
792
|
+
const fallback = await evaluate(this._defaultImageUrl, ctx);
|
|
793
|
+
const defaultUrl = isImageUrl(fallback) ? fallback.trim() : null;
|
|
794
|
+
let sources = (isBlank(state) ? (defaultUrl ? [defaultUrl] : []) : Array.isArray(state) ? state : [state])
|
|
795
|
+
.map((source) => (isImageUrl(source) ? source.trim() : defaultUrl)).filter((source) => source !== null);
|
|
796
|
+
if (!sources.length)
|
|
797
|
+
return this.wrapEmbeddedHtml(this._placeholder !== null || this._emptyTooltip !== null ? this.emptyHtml(['fi-in-image']) : this.emptyHtml(['fi-in-image', { 'fi-wrapped': this._wrap }]));
|
|
798
|
+
const count = sources.length;
|
|
799
|
+
const overLimit = this._limit && count > this._limit ? count - this._limit : 0;
|
|
800
|
+
if (overLimit)
|
|
801
|
+
sources = sources.slice(0, this._limit);
|
|
802
|
+
const alignment = await this.getAlignment();
|
|
803
|
+
const height = this._height ?? (this._stacked ? '2.5rem' : '8rem');
|
|
804
|
+
const width = this._width ?? (this._circular || this._square ? height : null);
|
|
805
|
+
const size = [`height: ${height}`, width ? `width: ${width}` : null];
|
|
806
|
+
const extraImg = escapeAttributes(Object.fromEntries(Object.entries(this._extraImgAttributes).filter(([name]) => isAllowedImgAttribute(name))));
|
|
807
|
+
const images = sources.map((source) => {
|
|
808
|
+
const alt = resolveItem(this._alt, ctx.record, source);
|
|
809
|
+
return `<img ${renderAttrs(withStyle(mergeAttrs({ alt: e(alt ?? ''), src: e(source), 'x-tooltip': tooltipJs(resolveItem(this._tooltip, ctx.record, source)) }, extraImg), size))} />`;
|
|
810
|
+
}).join('');
|
|
811
|
+
const remaining = overLimit && this._limitedRemainingText
|
|
812
|
+
? `<div ${renderAttrs(withStyle({ class: cls(['fi-in-image-limited-remaining-text', this._limitedRemainingTextSize ? `fi-size-${textSize(this._limitedRemainingTextSize)}` : null]) }, size))}>+${overLimit}</div>`
|
|
813
|
+
: '';
|
|
814
|
+
const container = withClass(this.getExtraAttributesMap(), [
|
|
815
|
+
'fi-in-image', { 'fi-circular': this._circular, 'fi-wrapped': this._wrap, 'fi-stacked': this._stacked },
|
|
816
|
+
this._stacked && this._ring !== null ? `fi-in-image-ring fi-in-image-ring-${this._ring}` : null,
|
|
817
|
+
this._stacked && (this._overlap ?? 2) ? `fi-in-image-overlap-${this._overlap ?? 2}` : null,
|
|
818
|
+
alignmentClass(alignment),
|
|
819
|
+
]);
|
|
820
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(container)}>${images}${remaining}</div>`);
|
|
170
821
|
}
|
|
171
822
|
}
|
|
172
|
-
|
|
823
|
+
const ICON_SIZES = new Set(['xs', 'sm', 'md', 'lg', 'xl', '2xl']);
|
|
824
|
+
/** Icons chosen from the state; `boolean()` shows a green check or a red cross. */
|
|
173
825
|
export class IconEntry extends Entry {
|
|
174
826
|
_boolean = false;
|
|
827
|
+
_icon = null;
|
|
828
|
+
_color = null;
|
|
829
|
+
_size = null;
|
|
830
|
+
_trueIcon = null;
|
|
831
|
+
_falseIcon = null;
|
|
832
|
+
_trueColor = null;
|
|
833
|
+
_falseColor = null;
|
|
834
|
+
_listWithLineBreaks = false;
|
|
835
|
+
_wrap = true;
|
|
175
836
|
static make(name) { return configured(new IconEntry(name)); }
|
|
176
837
|
boolean(condition = true) { this._boolean = condition; return this; }
|
|
838
|
+
/** Icon: a Heroicon name, a map of state values to names, or a function of the record and the state item. */
|
|
839
|
+
icon(icon) { this._icon = icon; return this; }
|
|
840
|
+
/** Color: a name, a map of state values to colors, or a function of the record and the state item. */
|
|
841
|
+
color(color) { this._color = color; return this; }
|
|
842
|
+
/** Icon size: `xs`, `sm`, `md`, `lg` (default), `xl` or `2xl`. */
|
|
843
|
+
size(size) { this._size = size; return this; }
|
|
844
|
+
/** Icon of a true state (`o-check-circle` by default); `false`: no icon. Implies `boolean()`. */
|
|
845
|
+
trueIcon(icon) { this._boolean = true; this._trueIcon = icon; return this; }
|
|
846
|
+
/** Icon of a false state (`o-x-circle` by default); `false`: no icon. Implies `boolean()`. */
|
|
847
|
+
falseIcon(icon) { this._boolean = true; this._falseIcon = icon; return this; }
|
|
848
|
+
/** Color of a true state (`success` by default). Implies `boolean()`. */
|
|
849
|
+
trueColor(color) { this._boolean = true; this._trueColor = color; return this; }
|
|
850
|
+
/** Color of a false state (`danger` by default). Implies `boolean()`. */
|
|
851
|
+
falseColor(color) { this._boolean = true; this._falseColor = color; return this; }
|
|
852
|
+
/** Each icon of a list on its own line. */
|
|
853
|
+
listWithLineBreaks(condition = true) { this._listWithLineBreaks = condition; return this; }
|
|
854
|
+
wrap(condition = true) { this._wrap = condition; return this; }
|
|
177
855
|
async toHtml() {
|
|
856
|
+
const record = this.context().record;
|
|
178
857
|
const state = await this.getState();
|
|
179
|
-
if (
|
|
180
|
-
return this.
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
858
|
+
if (isBlank(state))
|
|
859
|
+
return this.wrapEmbeddedHtml(this._placeholder !== null || this._emptyTooltip !== null ? this.emptyHtml(['fi-in-icon']) : this.emptyHtml(['fi-in-icon', { 'fi-wrapped': this._wrap }]));
|
|
860
|
+
const alignment = await this.getAlignment();
|
|
861
|
+
const items = (Array.isArray(state) ? state : [state]).map((item) => {
|
|
862
|
+
const on = item === true || item === 1 || item === '1';
|
|
863
|
+
let name = resolveMap(this._icon, record, item);
|
|
864
|
+
if (!name && this._boolean && item !== null && item !== undefined)
|
|
865
|
+
name = on ? (this._trueIcon === false ? null : this._trueIcon ?? 'o-check-circle') : (this._falseIcon === false ? null : this._falseIcon ?? 'o-x-circle');
|
|
866
|
+
if (!name)
|
|
867
|
+
return '';
|
|
868
|
+
let color = resolveMap(this._color, record, item);
|
|
869
|
+
if (!color && this._boolean && item !== null && item !== undefined)
|
|
870
|
+
color = on ? this._trueColor ?? 'success' : this._falseColor ?? 'danger';
|
|
871
|
+
const size = resolveItem(this._size, record, item);
|
|
872
|
+
const tooltip = resolveItem(this._tooltip, record, item);
|
|
873
|
+
const attributes = new Attributes().merge({ 'x-tooltip': tooltipJs(tooltip) }).class(iconColorClasses(color));
|
|
874
|
+
const text = !isBlank(tooltip) ? String(tooltip) : this._boolean ? t(on ? 'filament-infolists::components.entries.icon.true' : 'filament-infolists::components.entries.icon.false') : ['string', 'number', 'boolean'].includes(typeof item) ? String(item) : '';
|
|
875
|
+
return `${renderIcon(name, { size: size && ICON_SIZES.has(size) ? size : 'lg', attributes })}${text !== '' ? `<span class="fi-sr-only">${e(text)}</span>` : ''}`;
|
|
876
|
+
}).join('');
|
|
877
|
+
const container = withClass(this.getExtraAttributesMap(), ['fi-in-icon', { 'fi-in-icon-has-line-breaks': this._listWithLineBreaks, 'fi-wrapped': this._wrap }, alignmentClass(alignment)]);
|
|
878
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(container)}>${items}</div>`);
|
|
184
879
|
}
|
|
185
880
|
}
|
|
186
881
|
/** Key/value pairs as a table: the state is an object (or its JSON form); a non-scalar value is shown as JSON. */
|
|
187
882
|
export class KeyValueEntry extends Entry {
|
|
188
883
|
_keyLabel = null;
|
|
189
884
|
_valueLabel = null;
|
|
190
|
-
_placeholder = null;
|
|
191
885
|
static make(name) { return configured(new KeyValueEntry(name)); }
|
|
192
886
|
keyLabel(label) { this._keyLabel = label; return this; }
|
|
193
887
|
valueLabel(label) { this._valueLabel = label; return this; }
|
|
194
|
-
/** Text shown when there are no pairs ("No entries" by default). */
|
|
195
|
-
placeholder(text) { this._placeholder = text; return this; }
|
|
196
888
|
/** @deprecated Use `placeholder()`. */
|
|
197
889
|
emptyMessage(text) { return this.placeholder(text); }
|
|
198
890
|
async toHtml() {
|
|
@@ -210,7 +902,7 @@ export class KeyValueEntry extends Entry {
|
|
|
210
902
|
const show = (value) => (value === null || value === undefined || ['string', 'number', 'boolean'].includes(typeof value) ? (value === true ? '1' : value === false ? '' : String(value ?? '')) : JSON.stringify(value));
|
|
211
903
|
const rows = entries.map(([key, value]) => `<tr><th scope="row">${e(key)}</th><td>${e(show(value))}</td></tr>`).join('');
|
|
212
904
|
const empty = entries.length ? '' : `<tr><td colspan="2" class="fi-in-placeholder">${e(this._placeholder ?? tr('placeholder'))}</td></tr>`;
|
|
213
|
-
return this.
|
|
905
|
+
return this.wrapEmbeddedHtml(`<table ${renderAttrs(withClass(this.getExtraAttributesMap(), ['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
906
|
}
|
|
215
907
|
}
|
|
216
908
|
/** Pretty-printed JSON: four-space indent, with `/` and non-ASCII characters escaped. */
|
|
@@ -225,7 +917,6 @@ export class CodeEntry extends Entry {
|
|
|
225
917
|
_grammar = null;
|
|
226
918
|
_lightTheme = null;
|
|
227
919
|
_darkTheme = null;
|
|
228
|
-
_placeholder = null;
|
|
229
920
|
_copyable = false;
|
|
230
921
|
_copyMessage = null;
|
|
231
922
|
_copyMessageDuration = 2000;
|
|
@@ -235,7 +926,6 @@ export class CodeEntry extends Entry {
|
|
|
235
926
|
/** Theme class names only: without a highlighter, colors stay those of the default themes. */
|
|
236
927
|
lightTheme(theme) { this._lightTheme = theme; return this; }
|
|
237
928
|
darkTheme(theme) { this._darkTheme = theme; return this; }
|
|
238
|
-
placeholder(text) { this._placeholder = text; return this; }
|
|
239
929
|
/** Clicking copies the code to the clipboard. */
|
|
240
930
|
copyable(condition = true) { this._copyable = condition; return this; }
|
|
241
931
|
copyMessage(message) { this._copyMessage = message; return this; }
|
|
@@ -243,7 +933,7 @@ export class CodeEntry extends Entry {
|
|
|
243
933
|
async toHtml() {
|
|
244
934
|
let state = await this.getState();
|
|
245
935
|
if (state === null || state === undefined || (typeof state === 'string' && state.trim() === '') || (Array.isArray(state) && !state.length)) {
|
|
246
|
-
return this.
|
|
936
|
+
return this.wrapEmbeddedHtml(`<div class="fi-in-code">${this._placeholder !== null ? `<p class="fi-in-placeholder">${e(this._placeholder)}</p>` : ''}</div>`);
|
|
247
937
|
}
|
|
248
938
|
let grammar = this._grammar;
|
|
249
939
|
if (typeof state === 'object') {
|
|
@@ -259,16 +949,16 @@ export class CodeEntry extends Entry {
|
|
|
259
949
|
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
950
|
const pre = `<pre class="phiki language-${language} ${light} phiki-themes ${dark}" data-language="${language}"${style}><code>${linesHtml}</code></pre>`;
|
|
261
951
|
if (!this._copyable)
|
|
262
|
-
return this.
|
|
952
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(withClass(this.getExtraAttributesMap(), ['fi-in-code']))}>${pre}</div>`);
|
|
263
953
|
const tr = (key, params) => t(`filament::components/copyable.${key}`, params);
|
|
264
954
|
const message = js(this._copyMessage ?? tr('messages.copied'));
|
|
265
955
|
const click = `if (isStandalone) {\n copy(${js(code)}, ${message}, ${this._copyMessageDuration}, ${js(tr('messages.failed'))})\n} else {\n window.navigator.clipboard.writeText(${js(code)})\n $tooltip(${message}, {\n theme: $store.theme,\n timeout: ${this._copyMessageDuration},\n })\n}`;
|
|
266
|
-
const attributes = new Attributes().merge({
|
|
956
|
+
const attributes = new Attributes(this.getExtraAttributesMap()).merge({
|
|
267
957
|
'x-data': 'filamentCopyable', 'x-bind': 'bindings', 'x-id': "['copyable-code']",
|
|
268
|
-
'x-bind:aria-label': `isStandalone ? ${js(tr('label', { label: this.
|
|
958
|
+
'x-bind:aria-label': `isStandalone ? ${js(tr('label', { label: await this.getLabel() }))} : null`,
|
|
269
959
|
'x-bind:aria-describedby': "isStandalone ? $id('copyable-code') : null", 'x-on:click': click,
|
|
270
960
|
}).class(['fi-in-code', 'fi-copyable']);
|
|
271
|
-
return this.
|
|
961
|
+
return this.wrapEmbeddedHtml(`<div ${attributes.toHtml()}><div x-bind:id="$id('copyable-code')">${pre}</div></div>`);
|
|
272
962
|
}
|
|
273
963
|
}
|
|
274
964
|
/**
|
|
@@ -283,37 +973,101 @@ export class ViewEntry extends Entry {
|
|
|
283
973
|
view(renderer) { this._view = renderer; return this; }
|
|
284
974
|
/** `false`: the function renders the whole entry, without the usual wrapper (label). */
|
|
285
975
|
wrapped(condition = true) { this._wrapped = condition; return this; }
|
|
976
|
+
/** Places custom content in the entry wrapper (label, slots, link): for entry classes extending `ViewEntry`. */
|
|
977
|
+
wrap(content) { return this.wrapEmbeddedHtml(content); }
|
|
286
978
|
async toHtml() {
|
|
287
979
|
if (!this._view)
|
|
288
980
|
throw new Error(`ViewEntry [${this._name}]: view() is required.`);
|
|
289
981
|
const html = await this._view({ ...this.context(), state: await this.getState(), entry: this });
|
|
290
|
-
return this._wrapped ? this.
|
|
982
|
+
return this._wrapped ? this.wrapEmbeddedHtml(html) : html;
|
|
291
983
|
}
|
|
292
984
|
}
|
|
985
|
+
/** Column of a table repeatable entry: a header label, an optional width and alignment. */
|
|
986
|
+
export class TableColumn {
|
|
987
|
+
label;
|
|
988
|
+
_width = null;
|
|
989
|
+
_hiddenHeaderLabel = false;
|
|
990
|
+
_wrapHeader = false;
|
|
991
|
+
_alignment = null;
|
|
992
|
+
constructor(label) { this.label = label; }
|
|
993
|
+
static make(label) { return configured(new TableColumn(label)); }
|
|
994
|
+
/** Fixed width: a number of pixels, or a CSS length (`10rem`, `20%`). Any other value is ignored. */
|
|
995
|
+
width(width) { this._width = width === null ? null : cssLength(width) ?? this._width; return this; }
|
|
996
|
+
/** Keeps the header label for screen readers only. */
|
|
997
|
+
hiddenHeaderLabel(condition = true) { this._hiddenHeaderLabel = condition; return this; }
|
|
998
|
+
/** Lets a long header label wrap onto several lines. */
|
|
999
|
+
wrapHeader(condition = true) { this._wrapHeader = condition; return this; }
|
|
1000
|
+
/** Header alignment: `start`, `center` or `end`. */
|
|
1001
|
+
alignment(alignment) { this._alignment = alignment; return this; }
|
|
1002
|
+
alignStart() { return this.alignment('start'); }
|
|
1003
|
+
alignCenter() { return this.alignment('center'); }
|
|
1004
|
+
alignEnd() { return this.alignment('end'); }
|
|
1005
|
+
getWidth() { return this._width; }
|
|
1006
|
+
isHeaderLabelHidden() { return this._hiddenHeaderLabel; }
|
|
1007
|
+
canHeaderWrap() { return this._wrapHeader; }
|
|
1008
|
+
getAlignment() { return this._alignment; }
|
|
1009
|
+
}
|
|
293
1010
|
/**
|
|
294
|
-
* Read-only list of items,
|
|
295
|
-
*
|
|
1011
|
+
* Read-only list of items read from the record: cards in a grid (default), or a table (`table()`) with one row per item
|
|
1012
|
+
* and one cell per entry.
|
|
296
1013
|
*/
|
|
297
1014
|
export class RepeatableEntry extends Entry {
|
|
298
|
-
|
|
1015
|
+
_tableColumns = null;
|
|
299
1016
|
_schema = () => [];
|
|
1017
|
+
_grid = null;
|
|
1018
|
+
_itemColumns = null;
|
|
1019
|
+
_contained = true;
|
|
300
1020
|
static make(name) { return configured(new RepeatableEntry(name)); }
|
|
301
|
-
table
|
|
302
|
-
|
|
1021
|
+
/** Renders the items as a table with these columns, in the order of the schema's entries. */
|
|
1022
|
+
table(columns) { this._tableColumns = columns; return this; }
|
|
1023
|
+
/** Components repeated for each item. A function returns fresh instances for each item. */
|
|
1024
|
+
schema(components) { this._schema = typeof components === 'function' ? components : () => components; return this; }
|
|
1025
|
+
/** Card layout: number of item columns (from `lg` for a number, per breakpoint for an object); 2 when called without argument. */
|
|
1026
|
+
grid(columns = 2) { this._grid = columns; return this; }
|
|
1027
|
+
/** Card layout: grid columns inside each item. */
|
|
1028
|
+
columns(columns) { this._itemColumns = columns; return this; }
|
|
1029
|
+
/** Card layout: `false` removes the card styling of the items. */
|
|
1030
|
+
contained(condition = true) { this._contained = condition; return this; }
|
|
1031
|
+
itemSchema(index, item) {
|
|
1032
|
+
const c = this.container;
|
|
1033
|
+
const components = this._schema();
|
|
1034
|
+
const schema = new Schema(c.livewire).key(`${this.getKey()}.${index}`).statePath(`${this.getStatePath()}.${index}`).operation(c.getOperation()).record(item).root(false).components(components);
|
|
1035
|
+
return { schema, components };
|
|
1036
|
+
}
|
|
303
1037
|
async toHtml() {
|
|
304
1038
|
const state = await this.getState();
|
|
305
1039
|
const items = Array.isArray(state) ? state : [];
|
|
306
|
-
const
|
|
1040
|
+
const columns = this._tableColumns ?? [];
|
|
1041
|
+
if (!columns.length) {
|
|
1042
|
+
if (!items.length)
|
|
1043
|
+
return this.wrapEmbeddedHtml(this.emptyHtml(['fi-in-repeatable', { 'fi-contained': this._contained }]));
|
|
1044
|
+
const list = new Attributes(this.getExtraAttributesMap()).class(['fi-in-repeatable', { 'fi-contained': this._contained }]).grid(this._grid);
|
|
1045
|
+
const cards = [];
|
|
1046
|
+
for (const [index, item] of items.entries()) {
|
|
1047
|
+
const { schema } = this.itemSchema(index, item);
|
|
1048
|
+
cards.push(`<li class="fi-in-repeatable-item">${await schema.columns(this._itemColumns ?? {}).toHtml()}</li>`);
|
|
1049
|
+
}
|
|
1050
|
+
return this.wrapEmbeddedHtml(`<ul ${list.toHtml()}>${cards.join('')}</ul>`);
|
|
1051
|
+
}
|
|
1052
|
+
if (!items.length)
|
|
1053
|
+
return this.wrapEmbeddedHtml(this.emptyHtml(['fi-in-table-repeatable']));
|
|
307
1054
|
const rows = [];
|
|
308
1055
|
for (const [index, item] of items.entries()) {
|
|
309
|
-
const
|
|
310
|
-
new Schema(c.livewire).key(`${this.getKey()}.${index}`).statePath(`${this.getStatePath()}.${index}`).operation(c.getOperation()).record(item).root(false).components(entries);
|
|
1056
|
+
const { components } = this.itemSchema(index, item);
|
|
311
1057
|
const cells = [];
|
|
312
|
-
for (const
|
|
313
|
-
cells.push(
|
|
1058
|
+
for (const component of components.slice(0, columns.length)) {
|
|
1059
|
+
cells.push(await component.isVisible()
|
|
1060
|
+
? `<td wire:key="${e(component.getLivewireKey() ?? '')}" x-data="filamentSchemaComponent(${e(js(component.getAlpineScopeConfiguration()))})">${await component.toHtml()}</td>`
|
|
1061
|
+
: '<td class="fi-hidden"></td>');
|
|
1062
|
+
}
|
|
314
1063
|
rows.push(`<tr>${cells.join('')}</tr>`);
|
|
315
1064
|
}
|
|
316
|
-
const head =
|
|
317
|
-
|
|
1065
|
+
const head = columns.map((column) => {
|
|
1066
|
+
const width = cssLength(column.getWidth());
|
|
1067
|
+
const classes = cls([{ 'fi-wrapped': column.canHeaderWrap?.() ?? false }, alignmentClass(column.getAlignment?.() ?? null)]);
|
|
1068
|
+
const label = column.isHeaderLabelHidden?.() ? `<span class="fi-sr-only">${e(column.label)}</span>` : e(column.label);
|
|
1069
|
+
return `<th scope="col" class="${classes}"${width ? ` style="width: ${e(width)}"` : ''}>${label}</th>`;
|
|
1070
|
+
}).join('');
|
|
1071
|
+
return this.wrapEmbeddedHtml(`<div ${renderAttrs(withClass(this.getExtraAttributesMap(), ['fi-in-table-repeatable']))}><table><thead><tr>${head}</tr></thead><tbody>${rows.join('')}</tbody></table></div>`);
|
|
318
1072
|
}
|
|
319
1073
|
}
|