@nascentdigital/funnel-core 4.4.15 → 4.4.17

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.
@@ -1,7 +1,7 @@
1
1
  import { Tagged } from 'type-fest';
2
2
  import { RequiredBy } from '../types';
3
3
  import type { Page } from './Page';
4
- export type PageSnapshot = RequiredBy<Pick<Page.Data, Exclude<Page.Data.Props.Id, 'id'> | Page.Data.Props.Content | 'createdOn' | 'updatedOn' | 'changeSetId' | 'publication'>, 'changeSetId'> & {
4
+ export type PageSnapshot = RequiredBy<Pick<Page.Data<any>, Exclude<Page.Data.Props.Id, 'id'> | Page.Data.Props.Content | 'createdOn' | 'updatedOn' | 'changeSetId' | 'publication'>, 'changeSetId'> & {
5
5
  id: PageSnapshot.Id;
6
6
  };
7
7
  export declare namespace PageSnapshot {
@@ -1,63 +1,44 @@
1
+ import { JsonObject } from 'type-fest';
2
+ import { PartialBy } from '../../types';
1
3
  export type BlockLayout = {
2
4
  name: string;
3
5
  root: BlockLayout.Root;
4
- tight?: BlockLayout.Tight;
5
- tightMobile?: BlockLayout.Tight;
6
- tightDesktop?: BlockLayout.Tight;
6
+ tight: BlockLayout.ResponsiveValue<BlockLayout.Tight>;
7
7
  };
8
8
  export declare namespace BlockLayout {
9
- type Tight = 'above' | 'below' | 'both' | 'none';
10
- namespace Tight {
11
- const values: ReadonlyArray<Tight>;
12
- function parse(value: string, fallback?: Tight): Tight;
13
- }
14
- type Ratio = '1/1' | '1/2' | '1/3' | '2/1' | '3/1' | '2/3' | '3/2';
15
- namespace Ratio {
16
- const values: ReadonlyArray<Ratio>;
17
- function parse(value: string, fallback?: Ratio): Ratio;
18
- }
19
- namespace FullBleed {
20
- function parse(value: string, fallback?: boolean): boolean;
21
- }
22
9
  type Root = Row | Column | Stack;
23
10
  type Container = Row | Row.Nested | Column | Column.Nested | Stack | Stack.Nested;
24
- type Row = {
11
+ namespace Container {
12
+ type Base = {
13
+ alignX?: Alignment;
14
+ alignY?: Alignment;
15
+ };
16
+ type Nested = {
17
+ items: ReadonlyArray<Panel>;
18
+ mobileOrder?: MobileOrder;
19
+ };
20
+ }
21
+ type Row = Container.Base & {
25
22
  type: 'row';
26
23
  items: ReadonlyArray<Column.Nested | Stack.Nested | Panel>;
27
- alignX?: Alignment;
28
- alignY?: Alignment;
29
- ratio?: Ratio;
30
- ratioDesktop?: Ratio;
24
+ ratio: ResponsiveValue<Ratio>;
31
25
  };
32
26
  namespace Row {
33
- type Nested = Omit<Row, 'items'> & {
34
- items: ReadonlyArray<Panel>;
35
- mobileOrder?: MobileOrder;
36
- };
27
+ type Nested = Omit<Row, 'items'> & Container.Nested;
37
28
  }
38
- type Column = {
29
+ type Column = Container.Base & {
39
30
  type: 'column';
40
31
  items: ReadonlyArray<Row.Nested | Stack.Nested | Panel>;
41
- alignX?: Alignment;
42
- alignY?: Alignment;
43
32
  };
44
33
  namespace Column {
45
- type Nested = Omit<Column, 'items'> & {
46
- items: ReadonlyArray<Panel>;
47
- mobileOrder?: MobileOrder;
48
- };
34
+ type Nested = Omit<Column, 'items'> & Container.Nested;
49
35
  }
50
- type Stack = {
36
+ type Stack = Container.Base & {
51
37
  type: 'stack';
52
38
  items: ReadonlyArray<Row.Nested | Column.Nested | Panel>;
53
- alignX?: Alignment;
54
- alignY?: Alignment;
55
39
  };
56
40
  namespace Stack {
57
- type Nested = Omit<Stack, 'items'> & {
58
- items: ReadonlyArray<Panel>;
59
- mobileOrder?: MobileOrder;
60
- };
41
+ type Nested = Omit<Stack, 'items'> & Container.Nested;
61
42
  }
62
43
  type NestedItem = Row.Nested | Column.Nested | Stack.Nested | Panel;
63
44
  type Panel = Message | Visual | List;
@@ -69,22 +50,16 @@ export declare namespace BlockLayout {
69
50
  alignX?: Alignment;
70
51
  alignY?: Alignment;
71
52
  size?: Size;
72
- fullBleed?: boolean;
73
- fullBleedMobile?: boolean;
74
- fullBleedDesktop?: boolean;
53
+ fullBleed: ResponsiveValue<boolean>;
75
54
  };
76
55
  }
77
56
  type Message = Panel.Base & {
78
57
  type: 'message';
79
- textAlign?: TextAlignment;
80
- textAlignMobile?: TextAlignment;
81
- textAlignDesktop?: TextAlignment;
58
+ textAlign: ResponsiveValue<TextAlignment>;
82
59
  };
83
60
  type Visual = Panel.Base & {
84
61
  type: 'visual';
85
- aspectRatio: AspectRatio;
86
- aspectRatioMobile?: AspectRatio;
87
- aspectRatioDesktop?: AspectRatio;
62
+ aspectRatio: ResponsiveValue<AspectRatio>;
88
63
  };
89
64
  type List = Panel.Base & {
90
65
  type: 'list';
@@ -97,7 +72,20 @@ export declare namespace BlockLayout {
97
72
  function parse(value: string, fallback?: ItemType): ItemType;
98
73
  }
99
74
  }
100
- type MobileOrder = 'first' | 'last';
75
+ type Tight = 'above' | 'below' | 'both' | 'none';
76
+ namespace Tight {
77
+ const values: ReadonlyArray<Tight>;
78
+ function parse(value: string, fallback?: Tight): Tight;
79
+ }
80
+ type Ratio = '1/1' | '1/2' | '1/3' | '2/1' | '3/1' | '2/3' | '3/2';
81
+ namespace Ratio {
82
+ const values: ReadonlyArray<Ratio>;
83
+ function parse(value: string, fallback?: Ratio): Ratio;
84
+ }
85
+ namespace FullBleed {
86
+ function parse(value: string, fallback?: boolean): boolean;
87
+ }
88
+ type MobileOrder = 'first' | 'last' | 'auto';
101
89
  namespace MobileOrder {
102
90
  const values: ReadonlyArray<MobileOrder>;
103
91
  function parse(value: string, fallback?: MobileOrder): MobileOrder | undefined;
@@ -122,5 +110,48 @@ export declare namespace BlockLayout {
122
110
  const values: ReadonlyArray<AspectRatio>;
123
111
  function parse(value: string, fallback?: AspectRatio): AspectRatio;
124
112
  }
113
+ type ResponsiveValue<T extends string | boolean = string | boolean> = {
114
+ base: T;
115
+ mobile: T;
116
+ desktop: T;
117
+ };
118
+ namespace ResponsiveValue {
119
+ type ParseArgs<T extends string | boolean> = {
120
+ element: ParsedElement;
121
+ property: string;
122
+ parser: (value: string, fallback: T) => T;
123
+ fallback: T | PartialBy<ResponsiveValue<T>, 'desktop' | 'mobile'>;
124
+ };
125
+ function parse<T extends string | boolean>({ element, property, parser, fallback: fallbackRaw }: ParseArgs<T>): ResponsiveValue<T>;
126
+ }
127
+ type ResponsiveClassNames<T extends ResponsiveValue> = T['base'] extends string ? ResponsiveClassNames.ForString<T, T['base']> : T['base'] extends boolean ? ResponsiveClassNames.ForBoolean<T, T['base']> : never;
128
+ namespace ResponsiveClassNames {
129
+ type ForString<T extends ResponsiveValue<V>, V extends string = string> = Record<keyof T, Record<V, string>> & {
130
+ value: T;
131
+ };
132
+ type ForBoolean<T extends ResponsiveValue<V>, V extends boolean = boolean> = Record<keyof T, Record<ForBoolean.Key, string>> & {
133
+ value: T;
134
+ };
135
+ namespace ForBoolean {
136
+ type Key = 'true' | 'false';
137
+ }
138
+ function parse<T extends ResponsiveValue>(args: ResponsiveClassNames<T>): string;
139
+ }
125
140
  function parse(xml: string): BlockLayout;
126
141
  }
142
+ type ParsedElement<TExpected extends ParsedElement.Tag = ParsedElement.Tag> = {
143
+ tag: TExpected;
144
+ attributes: Record<string, string>;
145
+ children: JsonObject[];
146
+ };
147
+ declare namespace ParsedElement {
148
+ const tags: readonly ["block", "row", "col", "stack", "message", "visual", "list"];
149
+ type Tag = typeof tags[number];
150
+ type ParseArgs<TExpected extends Tag = Tag> = {
151
+ data: JsonObject;
152
+ expectedTags?: ReadonlyArray<TExpected>;
153
+ expectedChildren?: number | false;
154
+ };
155
+ function parse<TExpected extends Tag = Tag>({ data, expectedTags: expectedTagsRaw, expectedChildren }: ParseArgs<TExpected>): ParsedElement<TExpected>;
156
+ }
157
+ export {};
@@ -10,13 +10,41 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  }
11
11
  return t;
12
12
  };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
13
16
  Object.defineProperty(exports, "__esModule", { value: true });
14
17
  exports.BlockLayout = void 0;
18
+ const classnames_1 = __importDefault(require("classnames"));
15
19
  const fast_xml_parser_1 = require("fast-xml-parser");
16
20
  const errors_1 = require("../../errors");
17
21
  // extension
18
22
  var BlockLayout;
19
23
  (function (BlockLayout) {
24
+ let List;
25
+ (function (List) {
26
+ let ItemType;
27
+ (function (ItemType) {
28
+ // values
29
+ ItemType.values = ['message', 'message+visual'];
30
+ // helper
31
+ function parse(value, fallback) {
32
+ // return if it's a match
33
+ if (ItemType.values.includes(value)) {
34
+ return value;
35
+ }
36
+ // or fallback if we have one
37
+ else if (fallback) {
38
+ return fallback;
39
+ }
40
+ // or throw
41
+ else {
42
+ throw new errors_1.ArgumentError(`Invalid BlockLayout.List.ItemType "${value}"`, 'value');
43
+ }
44
+ }
45
+ ItemType.parse = parse;
46
+ })(ItemType = List.ItemType || (List.ItemType = {}));
47
+ })(List = BlockLayout.List || (BlockLayout.List = {}));
20
48
  let Tight;
21
49
  (function (Tight) {
22
50
  // values
@@ -59,6 +87,7 @@ var BlockLayout;
59
87
  }
60
88
  Ratio.parse = parse;
61
89
  })(Ratio = BlockLayout.Ratio || (BlockLayout.Ratio = {}));
90
+ // fullbleed (for panels)
62
91
  let FullBleed;
63
92
  (function (FullBleed) {
64
93
  // helper
@@ -82,30 +111,6 @@ var BlockLayout;
82
111
  }
83
112
  FullBleed.parse = parse;
84
113
  })(FullBleed = BlockLayout.FullBleed || (BlockLayout.FullBleed = {}));
85
- let List;
86
- (function (List) {
87
- let ItemType;
88
- (function (ItemType) {
89
- // values
90
- ItemType.values = ['message', 'message+visual'];
91
- // helper
92
- function parse(value, fallback) {
93
- // return if it's a match
94
- if (ItemType.values.includes(value)) {
95
- return value;
96
- }
97
- // or fallback if we have one
98
- else if (fallback) {
99
- return fallback;
100
- }
101
- // or throw
102
- else {
103
- throw new errors_1.ArgumentError(`Invalid BlockLayout.List.ItemType "${value}"`, 'value');
104
- }
105
- }
106
- ItemType.parse = parse;
107
- })(ItemType = List.ItemType || (List.ItemType = {}));
108
- })(List = BlockLayout.List || (BlockLayout.List = {}));
109
114
  let MobileOrder;
110
115
  (function (MobileOrder) {
111
116
  // values
@@ -207,6 +212,50 @@ var BlockLayout;
207
212
  }
208
213
  AspectRatio.parse = parse;
209
214
  })(AspectRatio = BlockLayout.AspectRatio || (BlockLayout.AspectRatio = {}));
215
+ let ResponsiveValue;
216
+ (function (ResponsiveValue) {
217
+ function parse({ element, property, parser, fallback: fallbackRaw }) {
218
+ var _a, _b;
219
+ // capture base
220
+ const { attributes } = element;
221
+ const fallbackBase = typeof fallbackRaw === 'string' || typeof fallbackRaw === 'boolean'
222
+ ? fallbackRaw
223
+ : fallbackRaw.base;
224
+ const base = parser(attributes[property], fallbackBase);
225
+ // normalize the fallback
226
+ const fallback = typeof fallbackRaw === 'string' || typeof fallbackRaw === 'boolean'
227
+ ? { base, mobile: base, desktop: base }
228
+ : { base, mobile: (_a = fallbackRaw.mobile) !== null && _a !== void 0 ? _a : base, desktop: (_b = fallbackRaw.desktop) !== null && _b !== void 0 ? _b : base };
229
+ // fetch the values from the element
230
+ return {
231
+ base,
232
+ mobile: parser(attributes[`${property}Mobile`], fallback.mobile),
233
+ desktop: parser(attributes[`${property}Desktop`], fallback.desktop)
234
+ };
235
+ }
236
+ ResponsiveValue.parse = parse;
237
+ })(ResponsiveValue = BlockLayout.ResponsiveValue || (BlockLayout.ResponsiveValue = {}));
238
+ let ResponsiveClassNames;
239
+ (function (ResponsiveClassNames) {
240
+ // helpers
241
+ function parse(args) {
242
+ // handle string types
243
+ if (typeof args.value.base === 'string') {
244
+ // destructure args
245
+ const { value: { mobile, base, desktop }, mobile: mobileClasses, base: baseClasses, desktop: desktopClasses } = args;
246
+ // return classes
247
+ return (0, classnames_1.default)(mobileClasses[mobile], baseClasses[base], desktopClasses[desktop]);
248
+ }
249
+ // or boolean
250
+ else {
251
+ // destructure args
252
+ const { value: { mobile, base, desktop }, mobile: mobileClasses, base: baseClasses, desktop: desktopClasses } = args;
253
+ // return classes
254
+ return (0, classnames_1.default)(mobileClasses[mobile ? 'true' : 'false'], baseClasses[base ? 'true' : 'false'], desktopClasses[desktop ? 'true' : 'false']);
255
+ }
256
+ }
257
+ ResponsiveClassNames.parse = parse;
258
+ })(ResponsiveClassNames = BlockLayout.ResponsiveClassNames || (BlockLayout.ResponsiveClassNames = {}));
210
259
  // parser
211
260
  const xmlParser = new fast_xml_parser_1.XMLParser({
212
261
  ignoreDeclaration: true,
@@ -224,26 +273,34 @@ var BlockLayout;
224
273
  })(BlockLayout || (exports.BlockLayout = BlockLayout = {}));
225
274
  // helpers
226
275
  function parseBlock(json) {
227
- // parse block
228
- const block = ParsedElement.parse({ data: json, expectedTags: ['block'], expectedChildren: 1 });
229
- const tight = BlockLayout.Tight.parse(block.attributes['tight'], 'none');
276
+ // parse
277
+ const element = ParsedElement.parse({ data: json, expectedTags: ['block'], expectedChildren: 1 });
278
+ const tight = BlockLayout.ResponsiveValue.parse({
279
+ element,
280
+ property: 'tight',
281
+ parser: BlockLayout.Tight.parse,
282
+ fallback: 'none'
283
+ });
230
284
  // return block
231
285
  return {
232
- name: block.attributes['name'],
286
+ name: element.attributes['name'],
233
287
  tight,
234
- tightMobile: BlockLayout.Tight.parse(block.attributes['tightMobile'], tight),
235
- tightDesktop: BlockLayout.Tight.parse(block.attributes['tightDesktop'], tight),
236
- root: parseRoot(ParsedElement.parse({ data: block.children[0], expectedTags: ['row', 'col', 'stack'] })),
288
+ root: parseRoot(ParsedElement.parse({ data: element.children[0], expectedTags: ['row', 'col', 'stack'] })),
237
289
  };
238
290
  }
239
- function parseRoot({ tag, attributes, children }) {
240
- const ratio = BlockLayout.Ratio.parse(attributes['ratio'], '1/1');
241
- // parse attributes
291
+ function parseRoot(element) {
292
+ // parse
293
+ const { tag, attributes, children } = element;
242
294
  const _a = parseItemAttributes(attributes), { size } = _a, itemAttributes = __rest(_a, ["size"]);
243
295
  // create root
244
296
  switch (tag) {
245
297
  case 'row':
246
- return Object.assign(Object.assign({ type: 'row' }, itemAttributes), { ratio, ratioDesktop: BlockLayout.Ratio.parse(attributes['ratioDesktop'], ratio), items: children.map(data => parseNestedItem(ParsedElement.parse({
298
+ return Object.assign(Object.assign({ type: 'row' }, itemAttributes), { ratio: BlockLayout.ResponsiveValue.parse({
299
+ element,
300
+ property: 'ratio',
301
+ parser: BlockLayout.Ratio.parse,
302
+ fallback: '1/1'
303
+ }), items: children.map(data => parseNestedItem(ParsedElement.parse({
247
304
  data,
248
305
  expectedTags: ParsedElement.tags.filter(t => t !== tag)
249
306
  }))) });
@@ -261,14 +318,19 @@ function parseRoot({ tag, attributes, children }) {
261
318
  throw new errors_1.ArgumentError(`Invalid XML BlockLayout root encountered "${tag}" where instead of "row" or "col"`);
262
319
  }
263
320
  }
264
- function parseNestedItem({ tag, attributes, children }) {
265
- // parse attributes
321
+ function parseNestedItem(element) {
322
+ // parse
323
+ const { tag, attributes, children } = element;
266
324
  const _a = parseItemAttributes(attributes), { size } = _a, itemAttributes = __rest(_a, ["size"]);
267
- const ratio = BlockLayout.Ratio.parse(attributes['ratio'], '1/1');
268
325
  // create container
269
326
  switch (tag) {
270
327
  case 'row':
271
- return Object.assign(Object.assign({ type: 'row' }, itemAttributes), { ratio, ratioDesktop: BlockLayout.Ratio.parse(attributes['ratioDesktop'], ratio), items: children.map(data => parsePanel(Object.assign({}, ParsedElement.parse({
328
+ return Object.assign(Object.assign({ type: 'row' }, itemAttributes), { ratio: BlockLayout.ResponsiveValue.parse({
329
+ element,
330
+ property: 'ratio',
331
+ parser: BlockLayout.Ratio.parse,
332
+ fallback: '1/1'
333
+ }), items: children.map(data => parsePanel(Object.assign({}, ParsedElement.parse({
272
334
  data,
273
335
  expectedTags: ParsedElement.tags.filter(t => t !== 'row' && t !== 'col')
274
336
  })))) });
@@ -282,38 +344,46 @@ function parseNestedItem({ tag, attributes, children }) {
282
344
  return parsePanel({ tag, attributes, children });
283
345
  }
284
346
  }
285
- function parsePanel({ tag, attributes }) {
347
+ function parsePanel(element) {
286
348
  var _a;
349
+ // parse
350
+ const { tag, attributes } = element;
287
351
  const key = (_a = attributes['key']) !== null && _a !== void 0 ? _a : tag;
288
352
  const itemAttributes = parseItemAttributes(attributes);
289
- const textAlign = BlockLayout.TextAlignment.parse(attributes['textAlign'], 'left');
290
- const aspectRatio = BlockLayout.AspectRatio.parse(attributes['aspect'], 'auto');
291
- const fullBleed = BlockLayout.FullBleed.parse(attributes['fullBleed'], false);
292
- const fullBleedMobile = BlockLayout.FullBleed.parse(attributes['fullBleedMobile'], fullBleed);
293
- const fullBleedDesktop = BlockLayout.FullBleed.parse(attributes['fullBleedDesktop'], fullBleed);
353
+ const fullBleed = BlockLayout.ResponsiveValue.parse({
354
+ element,
355
+ property: 'fullBleed',
356
+ parser: BlockLayout.FullBleed.parse,
357
+ fallback: false
358
+ });
359
+ // create panel
294
360
  switch (tag) {
295
361
  case 'message':
296
- return Object.assign(Object.assign({ type: 'message', key }, itemAttributes), { textAlign, textAlignMobile: BlockLayout.TextAlignment.parse(attributes['textAlignMobile'], textAlign), textAlignDesktop: BlockLayout.TextAlignment.parse(attributes['textAlignDesktop'], textAlign), fullBleed,
297
- fullBleedMobile,
298
- fullBleedDesktop });
362
+ return Object.assign({ type: 'message', key, textAlign: BlockLayout.ResponsiveValue.parse({
363
+ element,
364
+ property: 'textAlign',
365
+ parser: BlockLayout.TextAlignment.parse,
366
+ fallback: 'left'
367
+ }), fullBleed }, itemAttributes);
299
368
  case 'visual':
300
- return Object.assign(Object.assign({ type: 'visual', key }, itemAttributes), { aspectRatio, aspectRatioMobile: BlockLayout.AspectRatio.parse(attributes['aspectMobile'], aspectRatio), aspectRatioDesktop: BlockLayout.AspectRatio.parse(attributes['aspectDesktop'], aspectRatio), fullBleed,
301
- fullBleedMobile,
302
- fullBleedDesktop });
369
+ return Object.assign({ type: 'visual', key, aspectRatio: BlockLayout.ResponsiveValue.parse({
370
+ element,
371
+ property: 'aspect',
372
+ parser: BlockLayout.AspectRatio.parse,
373
+ fallback: 'auto'
374
+ }), fullBleed }, itemAttributes);
303
375
  case 'list':
304
- return Object.assign(Object.assign({ type: 'list', key }, itemAttributes), { fullBleed,
305
- fullBleedMobile,
306
- fullBleedDesktop, itemType: BlockLayout.List.ItemType.parse(attributes['itemType'], 'message+visual') });
376
+ return Object.assign({ type: 'list', key, itemType: BlockLayout.List.ItemType.parse(attributes['itemType'], 'message+visual'), fullBleed }, itemAttributes);
307
377
  default:
308
378
  throw new errors_1.ArgumentError(`Invalid XML BlockLayout - encountered "${tag}" where instead of BlockLayout.Panel`);
309
379
  }
310
380
  }
311
381
  function parseItemAttributes(attributes) {
312
382
  return {
313
- mobileOrder: BlockLayout.MobileOrder.parse(attributes['mobileOrder']),
383
+ mobileOrder: BlockLayout.MobileOrder.parse(attributes['mobileOrder'], 'auto'),
314
384
  alignX: BlockLayout.Alignment.parse(attributes['alignX'], 'start'),
315
385
  alignY: BlockLayout.Alignment.parse(attributes['alignY'], 'start'),
316
- size: BlockLayout.Size.parse(attributes['size'], 'full'),
386
+ size: BlockLayout.Size.parse(attributes['size'], 'full')
317
387
  };
318
388
  }
319
389
  var ParsedElement;
@@ -17,6 +17,7 @@ export declare class Field<TSchema extends Schema.Field.Type = Schema.Field.Type
17
17
  private _sandboxed;
18
18
  private _sandboxedValue;
19
19
  private readonly _validator;
20
+ private _customValidator?;
20
21
  private _errors;
21
22
  private _optIn?;
22
23
  private _optOut?;
@@ -29,6 +30,8 @@ export declare class Field<TSchema extends Schema.Field.Type = Schema.Field.Type
29
30
  get error(): FieldValidator.Error | undefined;
30
31
  getErrors(level: FieldValidator.Error.Level): readonly FieldValidator.Error[];
31
32
  get errors(): ReadonlyArray<FieldValidator.Error>;
33
+ get customValidator(): FieldValidator | undefined;
34
+ set customValidator(validator: FieldValidator | undefined);
32
35
  get defaultValue(): Field.Value<TSchema> | undefined;
33
36
  get initialValue(): Field.Value<TSchema> | undefined;
34
37
  get hasValue(): boolean;
@@ -71,6 +74,7 @@ export declare namespace Field {
71
74
  publishedValue?: Value<TSchema>;
72
75
  initialValue?: Value<TSchema>;
73
76
  required: boolean;
77
+ customValidator?: FieldValidator;
74
78
  };
75
79
  type Value<TSchema extends Schema.Field.Type = Schema.Field.Type> = Schema.Field.Data<TSchema>;
76
80
  interface Event extends EventBus.Event<'Field', Event.Type> {
@@ -56,8 +56,12 @@ class Field {
56
56
  this._editing = false;
57
57
  this._sandboxed = false;
58
58
  this._validator = FieldValidator_1.FieldValidator.create(this);
59
+ this._customValidator = args.customValidator;
59
60
  // validate
60
61
  this._errors = this._validator.validate(this, this._initialValue);
62
+ if (this._customValidator) {
63
+ this._errors = [...this._errors, ...this._customValidator.validate(this, this._initialValue)];
64
+ }
61
65
  }
62
66
  get events$() {
63
67
  return this.eventBus.events$.pipe((0, rxjs_1.filter)(Field.Event.filter), (0, rxjs_1.filter)(e => e.field === this));
@@ -82,11 +86,32 @@ class Field {
82
86
  return FieldValidator_1.FieldValidator.Error.mostRelevant(this._errors);
83
87
  }
84
88
  getErrors(level) {
85
- return this._validator.filter(this._errors, level);
89
+ // filter with fixed validator
90
+ const { _errors, _validator, _customValidator } = this;
91
+ let errors = _validator.filter(_errors, level);
92
+ // then with custom (if available)
93
+ if (_customValidator) {
94
+ errors = _customValidator.filter(errors, level);
95
+ }
96
+ // return errors
97
+ return errors;
86
98
  }
87
99
  get errors() {
88
100
  return this._errors;
89
101
  }
102
+ get customValidator() {
103
+ return this._customValidator;
104
+ }
105
+ set customValidator(validator) {
106
+ // ignore if unchanged
107
+ if (this._customValidator === validator) {
108
+ return;
109
+ }
110
+ // update validator
111
+ this._customValidator = validator;
112
+ // revalidate
113
+ this.validate();
114
+ }
90
115
  get defaultValue() {
91
116
  // no default if it's not required
92
117
  const { schema } = this;
@@ -231,8 +256,13 @@ class Field {
231
256
  delete this._shouldFocus;
232
257
  }
233
258
  validate(silent = false) {
234
- // validate (skip if nothing has changed
235
- const errors = this._validator.validate(this, this._value);
259
+ // validate (use custom if available)
260
+ const { _value, _validator, customValidator } = this;
261
+ let errors = _validator.validate(this, _value);
262
+ if (customValidator) {
263
+ errors = [...errors, ...customValidator.validate(this, _value)];
264
+ }
265
+ // skip if nothing has changed
236
266
  if (utils_1.Value.equals(errors, this._errors)) {
237
267
  return this._errors;
238
268
  }
@@ -18,7 +18,8 @@
18
18
  "date.maxDate": "${label} cannot be after ${maxDate}",
19
19
  "timestamp": "${label} is not a valid date/time",
20
20
  "timestamp.minTime": "${label} cannot be before ${minTime}",
21
- "timestamp.maxTime": "${label} cannot be after ${maxTime}"
21
+ "timestamp.maxTime": "${label} cannot be after ${maxTime}",
22
+ "duplicate": "${label} is already in use"
22
23
  },
23
24
  "fr": {
24
25
  "required": "${label} is required",
@@ -39,6 +40,7 @@
39
40
  "date.maxDate": "${label} cannot be after ${maxDate}",
40
41
  "timestamp": "${label} is not a valid date/time",
41
42
  "timestamp.minTime": "${label} cannot be before ${minTime}",
42
- "timestamp.maxTime": "${label} cannot be after ${maxTime}"
43
+ "timestamp.maxTime": "${label} cannot be after ${maxTime}",
44
+ "duplicate": "${label} est déjà utilisé"
43
45
  }
44
46
  }
@@ -5,3 +5,4 @@ export * from './FieldModel';
5
5
  export * from './FieldSerializer';
6
6
  export * from './FieldValidator';
7
7
  export * from './ObjectField';
8
+ export * from './validators/ValidatorHelper';
@@ -21,3 +21,4 @@ __exportStar(require("./FieldModel"), exports);
21
21
  __exportStar(require("./FieldSerializer"), exports);
22
22
  __exportStar(require("./FieldValidator"), exports);
23
23
  __exportStar(require("./ObjectField"), exports);
24
+ __exportStar(require("./validators/ValidatorHelper"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nascentdigital/funnel-core",
3
- "version": "4.4.15",
3
+ "version": "4.4.17",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/nascentdigital/funnel.git",
6
6
  "directory": "libs/core"
@@ -37,6 +37,7 @@
37
37
  "@nodable/flexible-xml-parser": "^1",
38
38
  "blurhash": "^2",
39
39
  "byte-size": "^9",
40
+ "classnames": "^2",
40
41
  "clone-deep": "^4",
41
42
  "css.escape": "^1",
42
43
  "date-fns": "^4",
@@ -63,5 +64,5 @@
63
64
  "jest-tsd": "^0.2.2",
64
65
  "ts-jest": "^29.3.2"
65
66
  },
66
- "gitHead": "242a9e0979c03d4037eb2462c5b01b2a095e8e8f"
67
- }
67
+ "gitHead": "f343d2af0dff3dd7e8ae12a9ab83bceb93381ec2"
68
+ }