feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { snapshotOrgChartDataset } from './snapshot.js';
|
||||
import type { OrgChartDataSource } from './types.js';
|
||||
|
||||
export type OrgChartSourceErrorCode = 'SOURCE_LOAD' | 'SOURCE_PARSE' | 'SOURCE_ABORT' | 'SOURCE_URL' | 'SOURCE_SNAPSHOT';
|
||||
export class OrgChartSourceError extends Error {
|
||||
readonly path = 'dataSource';
|
||||
constructor(readonly code: OrgChartSourceErrorCode, message: string) {
|
||||
super(message);
|
||||
this.name = 'OrgChartSourceError';
|
||||
}
|
||||
}
|
||||
export interface JsonOrgChartDataSourceOptions { fetch?: typeof globalThis.fetch }
|
||||
/** Inline JSON is captured at construction; URL sources load a fresh snapshot on each call. */
|
||||
export class JsonOrgChartDataSource implements OrgChartDataSource {
|
||||
private readonly inline: unknown;
|
||||
private readonly url: string | undefined;
|
||||
private readonly fetcher: typeof globalThis.fetch | undefined;
|
||||
constructor(input: unknown, options: JsonOrgChartDataSourceOptions = {}) {
|
||||
// Browser-native fetch may require the global Window receiver. Retaining it as
|
||||
// an instance property must not accidentally call it with this source as `this`.
|
||||
this.fetcher = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
||||
if (typeof input === 'string' || input instanceof URL) {
|
||||
const url = String(input);
|
||||
if (!safeSourceUrl(url)) throw new OrgChartSourceError('SOURCE_URL', 'Data source URL must use HTTP(S) or a relative JSON path.');
|
||||
this.url = url;
|
||||
} else {
|
||||
try { this.inline = snapshotOrgChartDataset(input); }
|
||||
catch { throw new OrgChartSourceError('SOURCE_SNAPSHOT', 'Data source input must be cloneable JSON.'); }
|
||||
}
|
||||
}
|
||||
async load(options: { signal?: AbortSignal } = {}): Promise<unknown> {
|
||||
const signal = options.signal;
|
||||
const aborted = () => { if (signal?.aborted) throw new OrgChartSourceError('SOURCE_ABORT', 'Data source request was cancelled.'); };
|
||||
aborted();
|
||||
if (this.url === undefined) return snapshotOrgChartDataset(this.inline);
|
||||
if (!this.fetcher) throw new OrgChartSourceError('SOURCE_LOAD', 'No fetch implementation is available.');
|
||||
let response: Response;
|
||||
try { response = await this.fetcher(this.url, { signal }); }
|
||||
catch {
|
||||
aborted();
|
||||
throw new OrgChartSourceError('SOURCE_LOAD', 'Data source could not be loaded.');
|
||||
}
|
||||
aborted();
|
||||
if (!response.ok) throw new OrgChartSourceError('SOURCE_LOAD', 'Data source returned an unsuccessful response.');
|
||||
let payload: unknown;
|
||||
try { payload = await response.json(); }
|
||||
catch {
|
||||
aborted();
|
||||
throw new OrgChartSourceError('SOURCE_PARSE', 'Data source response is not valid JSON.');
|
||||
}
|
||||
aborted();
|
||||
try { return snapshotOrgChartDataset(payload); }
|
||||
catch { throw new OrgChartSourceError('SOURCE_SNAPSHOT', 'Data source response must be cloneable JSON.'); }
|
||||
}
|
||||
}
|
||||
function safeSourceUrl(value: string): boolean {
|
||||
if (!value.trim() || /[\s\\<>\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(value) || value.startsWith('//')) return false;
|
||||
if (!/^[a-z][a-z\d+.-]*:/i.test(value)) return !/%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value);
|
||||
try { const parsed = new URL(value); return ['http:', 'https:'].includes(parsed.protocol) && !parsed.username && !parsed.password; }
|
||||
catch { return false; }
|
||||
}
|
||||
Reference in new issue
Block a user