Navbar
@@ -48,9 +24,7 @@ const Navbar: Story = (args): StoryFnAureliaReturnType => ({
-
+
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
@@ -67,8 +41,19 @@ const Navbar: Story = (args): StoryFnAureliaReturnType => ({
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.
- `,
- props: args,
-});
+
`,
+ }),
+ argTypes: {
+ target: { control: 'text' },
+ smoothScroll: { control: 'boolean' },
+ },
+};
+
+export default meta;
-export { Navbar };
+export const Navbar = {
+ args: {
+ target: 'navbar-example2',
+ smoothScroll: false,
+ },
+};
diff --git a/packages-adapters/bootstrap/src/components/spinner/spinner.stories.ts b/packages-adapters/bootstrap/src/components/spinner/spinner.stories.ts
index 327dd14..a62ce0f 100644
--- a/packages-adapters/bootstrap/src/components/spinner/spinner.stories.ts
+++ b/packages-adapters/bootstrap/src/components/spinner/spinner.stories.ts
@@ -1,40 +1,45 @@
-import { createComponentTemplate, Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
-import { BsButton } from '../button';
-
import { BsSpinner } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Components / Spinner',
component: BsSpinner,
+ render: () => ({
+ template: `
`,
+ }),
argTypes: {
- type: selectControl(['border', 'grow']),
- size: selectControl(['', 'sm']),
+ type: {
+ control: 'select',
+ options: ['border', 'grow'],
+ },
+ size: {
+ control: 'select',
+ options: ['', 'sm'],
+ },
},
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
+export const Overview = {
+ args: {
+ type: 'border',
+ size: '',
+ },
+};
-const Buttons: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsButton],
- template: `
+export const Buttons = {
+ render: () => ({
+ template: `
- ${createComponentTemplate(BsSpinner)}
+
- ${createComponentTemplate(BsSpinner)} Loading...
+ Loading...
- `,
- props: args,
-});
-
-Buttons.args = {
- size: 'sm',
+
`,
+ }),
+ args: {
+ type: 'border',
+ size: 'sm',
+ },
};
-
-export { Buttons, Overview };
diff --git a/packages-adapters/bootstrap/src/components/toast/toast.stories.ts b/packages-adapters/bootstrap/src/components/toast/toast.stories.ts
index 2a753be..f2638f1 100644
--- a/packages-adapters/bootstrap/src/components/toast/toast.stories.ts
+++ b/packages-adapters/bootstrap/src/components/toast/toast.stories.ts
@@ -1,45 +1,51 @@
import 'bootstrap/dist/css/bootstrap-utilities.min.css';
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
import { VARIANTS } from '../../constants';
-import { BsButton } from '../button';
-import { BsCloseButton } from '../close-button';
-import { BsToast, BsToastContainer } from '.';
+import { BsToast } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Components / Toast',
component: BsToast,
+ render: () => ({
+ template: `
+ Open toast
+
+ Hello, world! This is a toast message.
+
+
`,
+ }),
parameters: {
actions: {
handles: ['hide.bs.toast', 'hidden.bs.toast', 'show.bs.toast', 'shown.bs.toast'],
},
},
- args: {
- header: 'Toast',
- },
argTypes: {
- variant: selectControl(VARIANTS),
+ header: { control: 'text' },
+ animation: { control: 'boolean' },
+ autohide: { control: 'boolean' },
+ delay: { control: 'number' },
+ variant: {
+ control: 'select',
+ options: VARIANTS,
+ },
},
};
export default meta;
-export const Overview: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsButton, BsCloseButton, BsToastContainer],
- template: `
-
Open toast
-
- Hello, world! This is a toast message.
-
- `,
- props: args,
-});
+export const Overview = {
+ args: {
+ header: 'Toast',
+ animation: false,
+ autohide: false,
+ delay: 2000,
+ variant: undefined,
+ },
+};
diff --git a/packages-adapters/bootstrap/src/components/toast/toast.ts b/packages-adapters/bootstrap/src/components/toast/toast.ts
index c8104a2..452e0ff 100644
--- a/packages-adapters/bootstrap/src/components/toast/toast.ts
+++ b/packages-adapters/bootstrap/src/components/toast/toast.ts
@@ -52,7 +52,7 @@ export class BsToast implements ICustomElementViewModel, Toast.Options {
return this.waitAnimation(false);
}
- isShown(): boolean {
+ isShown(): boolean | undefined {
return this.toast?.isShown();
}
diff --git a/packages-adapters/bootstrap/src/components/tooltip/tooltip.stories.ts b/packages-adapters/bootstrap/src/components/tooltip/tooltip.stories.ts
index 297a511..91cb2be 100644
--- a/packages-adapters/bootstrap/src/components/tooltip/tooltip.stories.ts
+++ b/packages-adapters/bootstrap/src/components/tooltip/tooltip.stories.ts
@@ -1,69 +1,78 @@
import './tooltip.stories.scss';
-import { createComponentTemplate, Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
import { TOOLTIP_PLACEMENTS, TOOLTIP_TRIGGERS } from '../../constants';
import { BsTooltip } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Components / Tooltip',
component: BsTooltip,
+ render: () => ({
+ template: `
+ Some text without tooltip,
+ some text with tooltip.
+
`,
+ }),
parameters: {
actions: {
handles: ['show.bs.tooltip', 'shown.bs.tooltip', 'hide.bs.tooltip', 'hidden.bs.tooltip', 'inserted.bs.tooltip'],
},
},
- args: {
- title: 'Default tooltip',
- },
argTypes: {
- placement: selectControl(TOOLTIP_PLACEMENTS),
- trigger: selectControl(TOOLTIP_TRIGGERS),
+ title: { control: 'text' },
+ placement: {
+ control: 'select',
+ options: TOOLTIP_PLACEMENTS,
+ },
+ trigger: {
+ control: 'select',
+ options: TOOLTIP_TRIGGERS,
+ },
+ html: { control: 'boolean' },
},
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- template: `
-
- Some text without tooltip,
- some text with tooltip.
-
- `,
- props: args,
-});
+export const Overview = {
+ args: {
+ title: 'Default tooltip',
+ placement: 'top',
+ trigger: 'hover focus',
+ html: false,
+ },
+};
-const HtmlDefaultTooltip: Story = (args): StoryFnAureliaReturnType => ({
- template: `
-
+export const HtmlDefaultTooltip = {
+ render: () => ({
+ template: `
Some text without tooltip,
-
some text with tooltip.
-
- `,
- props: args,
-});
-
-HtmlDefaultTooltip.args = {
- title: 'Allow HTML in the tooltip ',
- html: true,
+ some text with tooltip.
+
`,
+ }),
+ args: {
+ title: 'Allow
HTML in the tooltip ',
+ placement: 'top',
+ trigger: 'hover focus',
+ html: true,
+ },
};
-const CustomTooltip: Story = (args): StoryFnAureliaReturnType => ({
- template: `
-
+export const CustomTooltip = {
+ render: () => ({
+ template: `
Some text without tooltip,
- some text with tooltip.
-
- `,
- props: args,
-});
-
-CustomTooltip.args = {
- template:
- '
',
+
some text with tooltip.
+
`,
+ }),
+ args: {
+ title: 'Default tooltip',
+ placement: 'top',
+ trigger: 'hover focus',
+ template:
+ '
',
+ },
+ argTypes: {
+ template: { control: 'text' },
+ },
};
-
-export { CustomTooltip, HtmlDefaultTooltip, Overview };
diff --git a/packages-adapters/bootstrap/src/components/tooltip/tooltip.ts b/packages-adapters/bootstrap/src/components/tooltip/tooltip.ts
index 3160c2c..c4f0150 100644
--- a/packages-adapters/bootstrap/src/components/tooltip/tooltip.ts
+++ b/packages-adapters/bootstrap/src/components/tooltip/tooltip.ts
@@ -3,7 +3,6 @@ import './tooltip.scss';
import type * as Popper from '@popperjs/core';
-import { ICustomAttributeController } from '@aurelia/runtime-html';
import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, customAttribute, ICustomAttributeViewModel, resolve } from 'aurelia';
import { Tooltip } from 'bootstrap';
@@ -22,12 +21,12 @@ export type TooltipTrigger =
name: 'bs-tooltip',
defaultProperty: 'title',
})
-export class BsTooltip implements Tooltip.Options, ICustomAttributeViewModel {
+export class BsTooltip implements Partial
, ICustomAttributeViewModel {
@bindable(coerceBoolean)
animation: boolean = true;
@bindable()
- allowList: Record> | undefined;
+ allowList?: Record>;
@bindable()
boundary: Popper.Boundary = 'clippingParents';
@@ -60,7 +59,7 @@ export class BsTooltip implements Tooltip.Options, ICustomAttributeViewModel {
sanitize: boolean = true;
@bindable()
- sanitizeFn: () => void | null = null;
+ sanitizeFn?: () => void | null;
@bindable()
selector: string | false = false;
@@ -77,8 +76,6 @@ export class BsTooltip implements Tooltip.Options, ICustomAttributeViewModel {
@bindable()
trigger: TooltipTrigger = 'hover focus';
- readonly $controller: ICustomAttributeController;
-
protected tooltip?: Tooltip;
constructor(protected readonly element: HTMLElement = resolve(HTMLElement)) {}
@@ -146,9 +143,10 @@ export class BsTooltip implements Tooltip.Options, ICustomAttributeViewModel {
protected getOptions(): Partial {
const options: Partial = {};
- Object.keys(this.$controller.definition.bindables).forEach((name) => {
- if (this[name] !== undefined) {
- options[name] = this[name] as never;
+ Object.keys((this as ICustomAttributeViewModel).$controller!.definition.bindables).forEach((name) => {
+ if (this[name as keyof this] !== undefined) {
+ // @ts-ignore
+ options[name] = this[name as keyof this];
}
});
diff --git a/packages-adapters/bootstrap/src/config.ts b/packages-adapters/bootstrap/src/config.ts
deleted file mode 100644
index e489130..0000000
--- a/packages-adapters/bootstrap/src/config.ts
+++ /dev/null
@@ -1 +0,0 @@
-export class Config {}
diff --git a/packages-adapters/bootstrap/src/configuration.ts b/packages-adapters/bootstrap/src/configuration.ts
new file mode 100644
index 0000000..bcd210d
--- /dev/null
+++ b/packages-adapters/bootstrap/src/configuration.ts
@@ -0,0 +1,47 @@
+import { DI, IContainer, Registration } from 'aurelia';
+
+export interface IBootstrapOptions {
+ /**
+ * Use form floating labels
+ * https://getbootstrap.com/docs/5.3/forms/floating-labels/
+ */
+ floatingLabels: boolean;
+ /**
+ * Use native HTML validation messages as invalid feedback
+ */
+ htmlValidationMessages: boolean;
+ /**
+ * Auto register all available components. Disable if you need to manually register only subset of components
+ */
+ registerComponents: boolean;
+ /**
+ * Icons SVG sprite location. May be external or local, then bundler (Vite/Webpack) must be configured to include it
+ */
+ iconsSpritePath?: string;
+}
+
+const defaultOptions: IBootstrapOptions = {
+ floatingLabels: false,
+ htmlValidationMessages: true,
+ registerComponents: true,
+};
+
+export const IBootstrapOptions = DI.createInterface('IBootstrapOptions');
+
+export function createConfiguration(opts: Partial, resources: any[]) {
+ return {
+ register(container: IContainer): void {
+ const finalOptions = { ...defaultOptions, ...opts };
+
+ container.register(Registration.instance(IBootstrapOptions, finalOptions));
+
+ // Register other plugin resources
+ if (finalOptions.registerComponents) {
+ container.register(resources);
+ }
+ },
+ customize(options: Partial) {
+ return createConfiguration(options, resources);
+ },
+ };
+}
diff --git a/packages-adapters/bootstrap/src/content/table/table.stories.ts b/packages-adapters/bootstrap/src/content/table/table.stories.ts
index 1beee34..73d3d23 100644
--- a/packages-adapters/bootstrap/src/content/table/table.stories.ts
+++ b/packages-adapters/bootstrap/src/content/table/table.stories.ts
@@ -1,55 +1,73 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
-import { BREAKPOINTS, VARIANTS } from '../../constants';
-
import { BsTable } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Content / Table',
component: BsTable,
+ render: () => ({
+ template: `
+
+
+
+ #
+ First
+ Last
+ Handle
+
+
+
+
+ 1
+ Mark
+ Otto
+ @mdo
+
+
+ 2
+ Jacob
+ Thornton
+ @fat
+
+
+ 3
+ Larry the Bird
+ @twitter
+
+
+
+ `,
+ }),
argTypes: {
- responsive: selectControl(['', 'always', ...BREAKPOINTS]),
- variant: selectControl(['', ...VARIANTS]),
- size: selectControl(['', 'sm']),
+ bordered: { control: 'boolean' },
+ striped: { control: 'boolean' },
+ stripedColumns: { control: 'boolean' },
+ hover: { control: 'boolean' },
+ borderless: { control: 'boolean' },
+ size: {
+ control: 'select',
+ options: ['sm'],
+ },
+ variant: {
+ control: 'select',
+ options: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
+ },
+ responsive: {
+ control: 'select',
+ options: ['always', 'sm', 'md', 'lg', 'xl', 'xxl'],
+ },
},
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- innerHtml: `
-
-
-
- #
- First
- Last
- Handle
-
-
-
-
- 1
- Mark
- Otto
- @mdo
-
-
- 2
- Jacob
- Thornton
- @fat
-
-
- 3
- Larry the Bird
- @twitter
-
-
-
- `,
- props: args,
-});
-
-export { Overview };
+export const Overview = {
+ args: {},
+};
diff --git a/packages-adapters/bootstrap/src/content/table/table.ts b/packages-adapters/bootstrap/src/content/table/table.ts
index 1f292a5..a59c254 100644
--- a/packages-adapters/bootstrap/src/content/table/table.ts
+++ b/packages-adapters/bootstrap/src/content/table/table.ts
@@ -2,7 +2,6 @@ import template from './table.html';
import './table.scss';
-import { ICustomElementController } from '@aurelia/runtime-html';
import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, customElement, ICustomElementViewModel, resolve } from 'aurelia';
@@ -40,16 +39,14 @@ export class BsTable implements ICustomElementViewModel {
table!: HTMLTableElement;
- readonly $controller: ICustomElementController;
-
- constructor(private readonly element: HTMLElement = resolve(HTMLElement)) {}
+ readonly element = resolve(HTMLElement);
attached() {
this.table = this.element.querySelector('table')!;
this.table.classList.add('table');
- Object.keys(this.$controller.definition.bindables).forEach((name) => {
- this.propertyChanged(name as keyof this, this[name]);
+ Object.keys((this as ICustomElementViewModel).$controller!.definition.bindables).forEach((name) => {
+ this.propertyChanged(name as keyof this, this[name as keyof this]);
});
}
diff --git a/packages-adapters/bootstrap/src/forms/base-field.ts b/packages-adapters/bootstrap/src/forms/base-field.ts
index 923be6c..33d7e32 100644
--- a/packages-adapters/bootstrap/src/forms/base-field.ts
+++ b/packages-adapters/bootstrap/src/forms/base-field.ts
@@ -1,15 +1,36 @@
import './common.scss';
-import { coerceBoolean, uniqueId } from '@ekzo-dev/toolkit';
-import { bindable, ICustomElementViewModel } from 'aurelia';
+import { coerceBoolean } from '@ekzo-dev/toolkit';
+import { bindable, ICustomElementViewModel, queueTask, resolve } from 'aurelia';
+
+import { IBootstrapOptions } from '../configuration';
+import { uniqueId } from '../utils';
+
+const stringProperties = new Set([
+ 'name',
+ 'form',
+ 'title',
+ 'autocomplete',
+ 'placeholder',
+ 'type',
+ 'step',
+ 'min',
+ 'max',
+ 'size',
+ 'minlength',
+ 'maxlength',
+ 'pattern',
+ 'fileAccept',
+ 'inputmode',
+ 'rows',
+]);
+const booleanProperties = new Set(['disabled', 'required', 'readonly', 'multiple']);
+const allProperties = [...stringProperties, ...booleanProperties];
export class BaseField implements ICustomElementViewModel {
@bindable()
name?: string;
- @bindable()
- id?: string;
-
@bindable()
label?: string;
@@ -29,21 +50,122 @@ export class BaseField implements ICustomElementViewModel {
validFeedback?: string;
@bindable()
- invalidFeedback?: string;
+ get invalidFeedback(): string | undefined {
+ return this._invalidFeedback ?? this._validationMessage;
+ }
+ set invalidFeedback(value: string) {
+ this._invalidFeedback = value;
+ }
@bindable()
form?: string;
- binding() {
- if (!this.id) {
- this.id = uniqueId();
+ @bindable()
+ text?: string | HTMLElement;
+
+ readonly control!: HTMLInputElement;
+
+ readonly host = resolve(HTMLElement);
+
+ readonly id = uniqueId();
+
+ protected readonly config = resolve(IBootstrapOptions);
+
+ private _validationMessage?: string;
+ private _invalidFeedback?: string;
+
+ #textElement?: HTMLElement;
+
+ bound() {
+ allProperties.forEach((prop) => {
+ if (this[prop as keyof this] != null) {
+ this.propertyChanged(prop, this[prop as keyof this]);
+ }
+ });
+
+ if (this.config.htmlValidationMessages && this.control) {
+ this._validationMessage = this.control.validationMessage;
+ }
+ }
+
+ attaching() {
+ this.textChanged(this.text);
+ }
+
+ textChanged(value?: string | HTMLElement) {
+ const element = this.#textElement;
+
+ if (value && !element) {
+ const id = uniqueId();
+
+ this.#textElement = this.#createElement('div', { class: 'form-text', id }, value);
+ this.control?.setAttribute('aria-describedby', id);
+ } else if (value && element) {
+ this.#setElementContent(element, value);
+ } else if (!value && element) {
+ this.control?.removeAttribute('aria-describedby');
+ element.remove();
+ this.#textElement = undefined;
+ }
+ }
+
+ propertyChanged(key: PropertyKey, newValue: unknown, oldValue?: unknown) {
+ const { control } = this;
+ const prop = key.toString();
+
+ if (!control) return;
+
+ if (prop === 'value') {
+ if (this.config.htmlValidationMessages) {
+ queueTask(() => {
+ this._validationMessage = control.validationMessage;
+ });
+ }
+ } else if (stringProperties.has(prop)) {
+ const isEmpty = newValue == null || newValue === '';
+ // TODO: remove after https://github.com/aurelia/aurelia/issues/2383
+ const attr = prop === 'fileAccept' ? 'accept' : prop;
+
+ if (isEmpty && oldValue) {
+ control.removeAttribute(attr);
+ } else if (!isEmpty) {
+ control.setAttribute(attr, newValue.toString());
+ }
+ } else if (booleanProperties.has(prop)) {
+ if (newValue) {
+ control.setAttribute(prop, '');
+ } else {
+ control.removeAttribute(prop);
+ }
}
}
/**
- * Пустой метод для работы обработчика в дочерних классах, не удалять!
- * Без него дочерние обработчики почему-то не срабатывают
- * @param value
+ * Set a custom validity message for the element
+ * @param error
*/
- valueChanged(value: any): void {}
+ setCustomValidity(error: string) {
+ this.control?.setCustomValidity(error);
+ this._validationMessage = error;
+ }
+
+ #createElement(name: string, attributes: Record, content: string | HTMLElement) {
+ const elem = document.createElement(name);
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ elem.setAttribute(key, value);
+ });
+
+ this.#setElementContent(elem, content);
+
+ return this.host.appendChild(elem);
+ }
+
+ #setElementContent(elem: HTMLElement, content: string | HTMLElement) {
+ if (typeof content === 'string') {
+ elem.innerText = content;
+ } else {
+ elem.appendChild(content);
+ }
+ }
}
diff --git a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html
index 6e662a0..91a3a93 100644
--- a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html
+++ b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html
@@ -1,24 +1,17 @@
${label}
${invalidFeedback}
diff --git a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.stories.ts b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.stories.ts
index e905af5..dd14011 100644
--- a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.stories.ts
@@ -1,68 +1,123 @@
-import { Meta, Story } from '@storybook/aurelia';
-
-import { disableControl, selectControl } from '../../../../../.storybook/helpers';
-import { SIZES, VARIANTS } from '../../constants';
-
import { BsCheckbox } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Forms / Checkbox',
component: BsCheckbox,
- parameters: {
- actions: {
- handles: ['change', 'input'],
- },
- },
- args: {
- label: 'Some label',
- },
+ render: () => ({
+ template: ` `,
+ }),
argTypes: {
- mode: selectControl(['switch', 'button']),
- buttonVariant: selectControl([...VARIANTS, 'link', ...VARIANTS.map((v) => `outline-${v}`)]),
- buttonSize: selectControl(['', ...SIZES]),
+ // BsCheckbox properties
+ checked: { control: 'boolean' },
+ value: { control: 'object' },
+ matcher: { control: false },
+ inline: { control: 'boolean' },
+ mode: {
+ control: 'select',
+ options: ['switch', 'button'],
+ },
+ buttonSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
+ buttonVariant: {
+ control: 'select',
+ options: [
+ 'primary',
+ 'secondary',
+ 'success',
+ 'danger',
+ 'warning',
+ 'info',
+ 'light',
+ 'dark',
+ 'link',
+ 'outline-primary',
+ 'outline-secondary',
+ 'outline-success',
+ 'outline-danger',
+ 'outline-warning',
+ 'outline-info',
+ 'outline-light',
+ 'outline-dark',
+ ],
+ },
+ indeterminate: { control: 'boolean' },
+ reverse: { control: 'boolean' },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
};
export default meta;
-const Overview: Story = (args) => ({
- props: args,
-});
+export const Overview = {
+ args: {
+ checked: false,
+ label: 'Some label',
+ buttonVariant: 'primary',
+ },
+};
-const BindingToArray: Story = (args) => ({
- template: `
-
+export const BindingToArray = {
+ render: () => ({
+ template: `
-Selected: \${checked}
- `,
- props: args,
-});
-
-BindingToArray.argTypes = {
- label: disableControl,
- name: disableControl,
- id: disableControl,
- title: disableControl,
- disabled: disableControl,
- required: disableControl,
- valid: disableControl,
- validFeedback: disableControl,
- invalidFeedback: disableControl,
- model: disableControl,
- value: disableControl,
- matcher: disableControl,
- mode: disableControl,
- buttonSize: disableControl,
- buttonVariant: disableControl,
- indeterminate: disableControl,
- form: disableControl,
- inline: disableControl,
- reverse: disableControl,
-};
-BindingToArray.args = {
- checked: [],
+Selected: \${checked}`,
+ }),
+ argTypes: {
+ checked: { control: 'object' },
+ label: { control: false },
+ name: { control: false },
+ title: { control: false },
+ disabled: { control: false },
+ required: { control: false },
+ valid: { control: false },
+ validFeedback: { control: false },
+ invalidFeedback: { control: false },
+ value: { control: false },
+ matcher: { control: false },
+ mode: { control: false },
+ buttonSize: { control: false },
+ buttonVariant: { control: false },
+ indeterminate: { control: false },
+ form: { control: false },
+ inline: { control: false },
+ reverse: { control: false },
+ text: { control: false },
+ },
+ args: {
+ checked: ['1'],
+ },
};
-
-export { BindingToArray, Overview };
diff --git a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.ts b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.ts
index 505ddd2..1de6fdd 100644
--- a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.ts
+++ b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.ts
@@ -16,16 +16,13 @@ import { BaseField } from '../base-field';
})
export class BsCheckbox extends BaseField {
@bindable({ mode: BindingMode.twoWay })
- checked!: boolean | any[];
+ checked!: boolean | unknown[];
@bindable()
- model?: any;
+ value?: unknown;
@bindable()
- value?: string;
-
- @bindable()
- matcher?: (a: any, b: any) => boolean;
+ matcher?: (a: unknown, b: unknown) => boolean;
@bindable(coerceBoolean)
inline: boolean = false;
@@ -45,20 +42,21 @@ export class BsCheckbox extends BaseField {
@bindable(coerceBoolean)
reverse: boolean = false;
- readonly input!: HTMLInputElement;
+ bound() {
+ super.bound();
+ this.indeterminateChanged();
+ }
indeterminateChanged() {
- if (this.input) {
- this.input.indeterminate = this.indeterminate;
- }
+ this.control.indeterminate = this.indeterminate;
}
get classes(): string {
return [
- this.mode === 'button' ? null : 'form-check',
- this.mode === 'switch' ? 'form-switch' : null,
- this.inline ? 'form-check-inline' : null,
- this.reverse ? 'form-check-reverse' : null,
+ this.mode === 'button' ? '' : 'form-check',
+ this.mode === 'switch' ? 'form-switch' : '',
+ this.inline ? 'form-check-inline' : '',
+ this.reverse ? 'form-check-reverse' : '',
]
.filter(Boolean)
.join(' ');
diff --git a/packages-adapters/bootstrap/src/forms/common.scss b/packages-adapters/bootstrap/src/forms/common.scss
index 975a9fe..44dbaab 100644
--- a/packages-adapters/bootstrap/src/forms/common.scss
+++ b/packages-adapters/bootstrap/src/forms/common.scss
@@ -2,3 +2,4 @@
@import 'bootstrap/scss/forms/validation';
@import 'bootstrap/scss/forms/labels';
@import 'bootstrap/scss/forms/floating-labels';
+@import 'bootstrap/scss/forms/form-text';
diff --git a/packages-adapters/bootstrap/src/forms/input-group/input-group.stories.ts b/packages-adapters/bootstrap/src/forms/input-group/input-group.stories.ts
index f775461..2e3304e 100644
--- a/packages-adapters/bootstrap/src/forms/input-group/input-group.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/input-group/input-group.stories.ts
@@ -1,29 +1,25 @@
-import { createComponentTemplate, Meta, Story } from '@storybook/aurelia';
+import { BsInputGroup } from '.';
-import { selectControl } from '../../../../../.storybook/helpers';
-import { BsButton } from '../../components/button';
-import { SIZES } from '../../constants';
-import { BsInput } from '../input';
-
-import { BsInputGroup, BsInputGroupText } from '.';
-
-export default {
+const meta = {
title: 'Bootstrap / Forms / Input group',
component: BsInputGroup,
- argTypes: {
- size: selectControl(['', ...SIZES]),
- },
-} as Meta;
-
-export const Overview: Story = (args) => ({
- components: [BsInputGroupText, BsInput, BsButton],
- innerHtml: `
+ render: () => ({
+ template: `
Text
Button
- ${createComponentTemplate(BsInput)}
- ${createComponentTemplate(BsInput)}
- `,
- props: {
- ...args,
+
+ `,
+ }),
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
},
-});
+};
+
+export default meta;
+
+export const Overview = {
+ args: {},
+};
diff --git a/packages-adapters/bootstrap/src/forms/input/input.html b/packages-adapters/bootstrap/src/forms/input/input.html
index 075be75..1dc7afd 100644
--- a/packages-adapters/bootstrap/src/forms/input/input.html
+++ b/packages-adapters/bootstrap/src/forms/input/input.html
@@ -1,34 +1,9 @@
- ${label}
-
- ${label}
-
-
+ ${label}
+
+ ${label}
+
+
${invalidFeedback}
${validFeedback}
diff --git a/packages-adapters/bootstrap/src/forms/input/input.stories.ts b/packages-adapters/bootstrap/src/forms/input/input.stories.ts
index 2c9fbbd..bbe5664 100644
--- a/packages-adapters/bootstrap/src/forms/input/input.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/input/input.stories.ts
@@ -1,29 +1,104 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
-import { SIZES } from '../../constants';
-
import { BsInput } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Forms / Input',
component: BsInput,
- parameters: {
- actions: {
- handles: ['change', 'input'],
- },
- },
- args: {
- label: 'Label',
- },
+ render: () => ({
+ template: ` `,
+ }),
argTypes: {
- size: selectControl(['', ...SIZES]),
+ // BsInput properties
+ type: {
+ control: 'select',
+ options: [
+ 'text',
+ 'password',
+ 'email',
+ 'number',
+ 'tel',
+ 'url',
+ 'search',
+ 'date',
+ 'time',
+ 'datetime-local',
+ 'month',
+ 'week',
+ 'color',
+ 'file',
+ 'range',
+ ],
+ },
+ inputmode: {
+ control: 'select',
+ options: ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'],
+ },
+ value: { control: 'text' },
+ minlength: { control: 'number' },
+ maxlength: { control: 'number' },
+ min: { control: 'text' },
+ max: { control: 'text' },
+ step: { control: 'text' },
+ multiple: { control: 'boolean' },
+ pattern: { control: 'text' },
+ fileAccept: { control: 'text' },
+ floatingLabel: { control: 'boolean' },
+ placeholder: { control: 'text' },
+ readonly: { control: 'boolean' },
+ size: { control: 'number' },
+ bsSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
+ autocomplete: { control: 'text' },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
};
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
export default meta;
-export { Overview };
+
+export const Overview = {
+ args: {
+ type: 'text',
+ value: 'Hello from Storybook!',
+ label: 'Input Label',
+ },
+};
diff --git a/packages-adapters/bootstrap/src/forms/input/input.ts b/packages-adapters/bootstrap/src/forms/input/input.ts
index 7ff6862..9a8a40f 100644
--- a/packages-adapters/bootstrap/src/forms/input/input.ts
+++ b/packages-adapters/bootstrap/src/forms/input/input.ts
@@ -2,10 +2,11 @@ import template from './input.html';
import './input.scss';
-import { coerceBoolean, uniqueId } from '@ekzo-dev/toolkit';
+import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, BindingMode, customElement } from 'aurelia';
import { Size } from '../../types';
+import { uniqueId } from '../../utils';
import { BaseField } from '../base-field';
type HTMLInputBase = Partial & { form: string; autocomplete: string }>;
@@ -51,15 +52,24 @@ export class BsInput extends BaseField implements HTMLInputBase {
@bindable()
pattern?: string;
- /* property is named like this to avoid collision with IActivationHooks['accept'] */
+ /**
+ * property is named like this to avoid collision with IActivationHooks['accept']
+ * TODO: rename after https://github.com/aurelia/aurelia/issues/2383
+ */
@bindable()
fileAccept?: string;
- @bindable()
- placeholder?: string;
-
@bindable(coerceBoolean)
- floatingLabel: boolean = false;
+ floatingLabel: boolean = this.config.floatingLabels;
+
+ @bindable()
+ get placeholder(): string | undefined {
+ // https://getbootstrap.com/docs/5.3/forms/floating-labels/#example
+ return !this._placeholder && this.floatingLabel ? ' ' : this._placeholder;
+ }
+ set placeholder(value: string) {
+ this._placeholder = value;
+ }
@bindable(coerceBoolean)
readonly: boolean = false;
@@ -71,34 +81,31 @@ export class BsInput extends BaseField implements HTMLInputBase {
bsSize?: Size;
@bindable()
- datalist: string[] = [];
+ datalist?: string[];
@bindable()
autocomplete?: string;
- input!: HTMLInputElement;
-
- datalistId!: string;
+ datalistId: string = uniqueId();
- binding(): void {
- super.binding();
- this.#ensurePlaceholder();
+ private _placeholder?: string;
- this.datalistId = uniqueId();
+ bound() {
+ super.bound();
+ this.datalistChanged(this.datalist);
}
- placeholderChanged(): void {
- this.#ensurePlaceholder();
- }
-
- floatingLabelChanged(): void {
- this.#ensurePlaceholder();
+ datalistChanged(newValue?: string[], oldValue?: string[]): void {
+ if (newValue != null) {
+ this.control.setAttribute('datalist', this.datalistId);
+ } else if (oldValue) {
+ this.control.removeAttribute('datalist');
+ }
}
valueChanged(): void {
- // TODO: binding to file does not currently work on Aurelia 2 out of the box, need to investigate
- if (this.input.type === 'file') {
- this.files = this.input.files!;
+ if (this.control.type === 'file') {
+ this.files = this.control.files!;
}
}
@@ -112,12 +119,4 @@ export class BsInput extends BaseField implements HTMLInputBase {
.filter(Boolean)
.join(' ');
}
-
- #ensurePlaceholder(): void {
- // A placeholder is required on each as our method of CSS-only floating labels uses the
- // :placeholder-shown pseudo-element https://getbootstrap.com/docs/5.2/forms/floating-labels/#example
- if (this.floatingLabel && !this.placeholder) {
- this.placeholder = ' ';
- }
- }
}
diff --git a/packages-adapters/bootstrap/src/forms/radio/radio-group.ts b/packages-adapters/bootstrap/src/forms/radio/radio-group.ts
index 4c1ad22..d0a1236 100644
--- a/packages-adapters/bootstrap/src/forms/radio/radio-group.ts
+++ b/packages-adapters/bootstrap/src/forms/radio/radio-group.ts
@@ -2,11 +2,12 @@ import template from './radio-group.html';
import './radio.scss';
-import { coerceBoolean, Iterable, uniqueId } from '@ekzo-dev/toolkit';
+import { coerceBoolean, Iterable } from '@ekzo-dev/toolkit';
import { bindable, BindingMode, customElement } from 'aurelia';
import { ButtonVariant } from '../../components';
import { Size } from '../../types';
+import { uniqueId } from '../../utils';
import { BaseField } from '../base-field';
import { BsRadio } from './radio';
@@ -45,11 +46,7 @@ export class BsRadioGroup extends BaseField {
buttonVariant: ButtonVariant = 'primary';
binding() {
- super.binding();
-
- if (!this.name) {
- this.name = uniqueId();
- }
+ this.name ??= uniqueId();
}
get radioOptions(): IRadioOption[] {
@@ -61,6 +58,7 @@ export class BsRadioGroup extends BaseField {
}
// check entries
+ // @ts-ignore
if (Array.isArray(options[0])) {
return (options as Array).map(([k, v]) => ({
value: k,
diff --git a/packages-adapters/bootstrap/src/forms/radio/radio.html b/packages-adapters/bootstrap/src/forms/radio/radio.html
index d56bbe7..23d8837 100644
--- a/packages-adapters/bootstrap/src/forms/radio/radio.html
+++ b/packages-adapters/bootstrap/src/forms/radio/radio.html
@@ -1,20 +1,16 @@
${label}
diff --git a/packages-adapters/bootstrap/src/forms/radio/radio.stories.ts b/packages-adapters/bootstrap/src/forms/radio/radio.stories.ts
index 0e560d6..dd8b5fa 100644
--- a/packages-adapters/bootstrap/src/forms/radio/radio.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/radio/radio.stories.ts
@@ -1,45 +1,162 @@
-import { createComponentTemplate, Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { disableControl, selectControl } from '../../../../../.storybook/helpers';
-
import { BsRadio, BsRadioGroup } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Forms / Radio',
component: BsRadio,
- parameters: {
- actions: {
- handles: ['change', 'input'],
- },
- },
+ render: () => ({
+ template: ` `,
+ }),
argTypes: {
- mode: selectControl(['', 'button']),
+ // BsRadio properties
+ checked: { control: 'text' },
+ value: { control: 'text' },
+ matcher: { control: false },
+ inline: { control: 'boolean' },
+ mode: {
+ control: 'select',
+ options: ['button'],
+ },
+ buttonSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
+ buttonVariant: {
+ control: 'select',
+ options: [
+ 'primary',
+ 'secondary',
+ 'success',
+ 'danger',
+ 'warning',
+ 'info',
+ 'light',
+ 'dark',
+ 'link',
+ 'outline-primary',
+ 'outline-secondary',
+ 'outline-success',
+ 'outline-danger',
+ 'outline-warning',
+ 'outline-info',
+ 'outline-light',
+ 'outline-dark',
+ ],
+ },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
-Overview.args = {
- label: 'Default radio',
+export const Overview = {
+ args: {
+ label: 'Default radio',
+ buttonVariant: 'primary',
+ },
};
-const RadioGroup: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsRadioGroup],
- template: createComponentTemplate(BsRadioGroup),
- props: args,
-});
+export const RadioGroup = {
+ render: () => ({
+ template: ` `,
+ }),
+ argTypes: {
+ // BsRadioGroup properties
+ checked: { control: 'text' },
+ options: { control: 'object' },
+ matcher: { control: false },
+ inline: { control: 'boolean' },
+ mode: {
+ control: 'select',
+ options: ['button'],
+ },
+ buttonSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
+ buttonVariant: {
+ control: 'select',
+ options: [
+ 'primary',
+ 'secondary',
+ 'success',
+ 'danger',
+ 'warning',
+ 'info',
+ 'light',
+ 'dark',
+ 'link',
+ 'outline-primary',
+ 'outline-secondary',
+ 'outline-success',
+ 'outline-danger',
+ 'outline-warning',
+ 'outline-info',
+ 'outline-light',
+ 'outline-dark',
+ ],
+ },
-RadioGroup.args = {
- options: { '0': 'Default radio', '1': 'Default checked radio' },
- checked: '1',
-};
-RadioGroup.argTypes = {
- id: disableControl,
- title: disableControl,
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: false },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
+ value: { control: false },
+ },
+ args: {
+ options: { '0': 'Default radio', '1': 'Default checked radio' },
+ checked: '1',
+ buttonVariant: 'primary',
+ },
};
-
-export { Overview, RadioGroup };
diff --git a/packages-adapters/bootstrap/src/forms/select/select.html b/packages-adapters/bootstrap/src/forms/select/select.html
index d893475..2ba184a 100644
--- a/packages-adapters/bootstrap/src/forms/select/select.html
+++ b/packages-adapters/bootstrap/src/forms/select/select.html
@@ -1,39 +1,33 @@
- ${label}
+ ${label}
${option.text}
${option.text}
- ${label}
+ ${label}
${invalidFeedback}
${validFeedback}
diff --git a/packages-adapters/bootstrap/src/forms/select/select.stories.ts b/packages-adapters/bootstrap/src/forms/select/select.stories.ts
index d628d44..6774695 100644
--- a/packages-adapters/bootstrap/src/forms/select/select.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/select/select.stories.ts
@@ -1,18 +1,60 @@
-import { extractArgTypes, Meta, Story } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
-import { SIZES } from '../../constants';
-
import { BsSelect } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Forms / Select',
component: BsSelect,
- parameters: {
- actions: {
- handles: ['change', 'input'],
+ render: () => ({
+ template: ` `,
+ }),
+ argTypes: {
+ // BsSelect properties
+ value: { control: 'text' },
+ options: { control: 'object' },
+ multiple: { control: 'boolean' },
+ floatingLabel: { control: 'boolean' },
+ size: { control: 'number' },
+ bsSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
},
+ autocomplete: { control: 'text' },
+ matcher: { control: false },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
+};
+
+export default meta;
+
+export const Overview = {
args: {
label: 'Label',
options: [
@@ -21,16 +63,4 @@ const meta: Meta = {
{ value: '3', text: 'Three' },
],
},
- argTypes: {
- bsSize: {
- ...extractArgTypes(BsSelect).bsSize,
- ...selectControl(['', ...SIZES]),
- },
- },
};
-
-export default meta;
-
-export const Overview: Story = (args) => ({
- props: args,
-});
diff --git a/packages-adapters/bootstrap/src/forms/select/select.ts b/packages-adapters/bootstrap/src/forms/select/select.ts
index 4a2fdd7..d53e8e4 100644
--- a/packages-adapters/bootstrap/src/forms/select/select.ts
+++ b/packages-adapters/bootstrap/src/forms/select/select.ts
@@ -31,7 +31,7 @@ export class BsSelect extends BaseField {
multiple: boolean = false;
@bindable(coerceBoolean)
- floatingLabel: boolean = false;
+ floatingLabel: boolean = this.config.floatingLabels;
@bindable(coerceNumber)
size?: number;
@@ -45,7 +45,7 @@ export class BsSelect extends BaseField {
@bindable()
matcher?: (a: unknown, b: unknown) => boolean;
- getValue(key: unknown): string {
+ getValue(key: unknown): string | undefined {
return key == null || key === '' ? '' : undefined;
}
@@ -54,6 +54,7 @@ export class BsSelect extends BaseField {
const { options } = this;
// check object/entries
+ // @ts-ignore
if ((options instanceof Object && options.constructor === Object) || Array.isArray(options[0])) {
return result;
}
@@ -84,6 +85,7 @@ export class BsSelect extends BaseField {
}
// check entries
+ // @ts-ignore
if (Array.isArray(options[0])) {
return (options as Array).map(([k, v]) => ({
value: k,
diff --git a/packages-adapters/bootstrap/src/forms/textarea/textarea.html b/packages-adapters/bootstrap/src/forms/textarea/textarea.html
index 86b2f05..e15290f 100644
--- a/packages-adapters/bootstrap/src/forms/textarea/textarea.html
+++ b/packages-adapters/bootstrap/src/forms/textarea/textarea.html
@@ -1,21 +1,12 @@
- ${label}
+ ${label}
- ${label}
+ ${label}
${invalidFeedback}
${validFeedback}
diff --git a/packages-adapters/bootstrap/src/forms/textarea/textarea.stories.ts b/packages-adapters/bootstrap/src/forms/textarea/textarea.stories.ts
index c12d5f6..348aac0 100644
--- a/packages-adapters/bootstrap/src/forms/textarea/textarea.stories.ts
+++ b/packages-adapters/bootstrap/src/forms/textarea/textarea.stories.ts
@@ -1,29 +1,63 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
-import { SIZES } from '../../constants';
-
import { BsTextarea } from '.';
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Forms / Textarea',
component: BsTextarea,
- parameters: {
- actions: {
- handles: ['change', 'input'],
+ render: () => ({
+ template: ` `,
+ }),
+ argTypes: {
+ // BsTextarea properties
+ value: { control: 'text' },
+ rows: { control: 'number' },
+ floatingLabel: { control: 'boolean' },
+ placeholder: { control: 'text' },
+ maxlength: { control: 'number' },
+ minlength: { control: 'number' },
+ size: {
+ control: 'select',
+ options: ['sm', 'lg'],
},
+ autocomplete: { control: 'text' },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
+};
+
+export default meta;
+
+export const Overview = {
args: {
label: 'Label',
- },
- argTypes: {
- size: selectControl(['', ...SIZES]),
+ rows: 3,
},
};
-
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
-export default meta;
-export { Overview };
diff --git a/packages-adapters/bootstrap/src/forms/textarea/textarea.ts b/packages-adapters/bootstrap/src/forms/textarea/textarea.ts
index 3da0fc3..afb9025 100644
--- a/packages-adapters/bootstrap/src/forms/textarea/textarea.ts
+++ b/packages-adapters/bootstrap/src/forms/textarea/textarea.ts
@@ -19,11 +19,17 @@ export class BsTextarea extends BaseField {
@bindable()
rows: number = 3;
- @bindable()
- placeholder?: string;
-
@bindable(coerceBoolean)
- floatingLabel: boolean = false;
+ floatingLabel: boolean = this.config.floatingLabels;
+
+ @bindable()
+ get placeholder(): string | undefined {
+ // https://getbootstrap.com/docs/5.3/forms/floating-labels/#example
+ return !this._placeholder && this.floatingLabel ? ' ' : this._placeholder;
+ }
+ set placeholder(value: string) {
+ this._placeholder = value;
+ }
@bindable()
maxlength?: number;
@@ -37,24 +43,5 @@ export class BsTextarea extends BaseField {
@bindable()
autocomplete?: string;
- binding(): void {
- super.binding();
- this.#ensurePlaceholder();
- }
-
- placeholderChanged(): void {
- this.#ensurePlaceholder();
- }
-
- floatingLabelChanged(): void {
- this.#ensurePlaceholder();
- }
-
- #ensurePlaceholder(): void {
- // A placeholder is required on each as our method of CSS-only floating labels uses the
- // :placeholder-shown pseudo-element https://getbootstrap.com/docs/5.2/forms/floating-labels/#example
- if (this.floatingLabel && !this.placeholder) {
- this.placeholder = ' ';
- }
- }
+ private _placeholder?: string;
}
diff --git a/packages-adapters/bootstrap/src/icon/icon.html b/packages-adapters/bootstrap/src/icon/icon.html
index 6725282..8887e9e 100644
--- a/packages-adapters/bootstrap/src/icon/icon.html
+++ b/packages-adapters/bootstrap/src/icon/icon.html
@@ -1,10 +1,3 @@
-
-
-
-
-
-
diff --git a/packages-adapters/bootstrap/src/icon/icon.stories.ts b/packages-adapters/bootstrap/src/icon/icon.stories.ts
index b287233..6950b8f 100644
--- a/packages-adapters/bootstrap/src/icon/icon.stories.ts
+++ b/packages-adapters/bootstrap/src/icon/icon.stories.ts
@@ -1,8 +1,3 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { disableControl } from '../../../../.storybook/helpers';
-import { BsPagination } from '../components';
-
import { BsIcon } from '.';
const iconsList = [
@@ -1372,58 +1367,42 @@ const iconsList = [
'x-octagon',
'x-octagon-fill',
];
-// В инспекторе видно, что код иконки есть, но не отображается в Canvas
-const meta: Meta = {
+const meta = {
title: 'Bootstrap / Icons',
component: BsIcon,
+ render: () => ({
+ template: ` `,
+ }),
+ argTypes: {
+ name: {
+ control: 'select',
+ options: iconsList,
+ },
+ },
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: {
- ...args,
+export const Overview = {
+ args: {
+ name: 'alarm',
},
-});
-
-Overview.args = {
- name: 'activity',
};
-const iconsListPart1 = iconsList.slice(0, Math.floor(iconsList.length / 2));
-const iconsExample1: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsIcon, BsPagination],
- template: `
-
- \${icon}
-
-`,
- props: {
- ...args,
- iconsListPart1,
+export const AllIcons = {
+ render: () => ({
+ template: ``,
+ }),
+ argTypes: {
+ name: { control: false },
},
-});
-
-iconsExample1.argTypes = {
- name: disableControl,
-};
-
-const iconsListPart2 = iconsList.slice(Math.floor(iconsList.length / 2));
-const iconsExample2: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsIcon, BsPagination],
- template: `
-
- \${icon}
-
-`,
- props: {
- ...args,
- iconsListPart2,
+ args: {
+ iconsList,
},
-});
-
-iconsExample2.argTypes = {
- name: disableControl,
-};
-export { iconsExample1, iconsExample2, Overview };
+};
\ No newline at end of file
diff --git a/packages-adapters/bootstrap/src/icon/icon.ts b/packages-adapters/bootstrap/src/icon/icon.ts
index 12c8227..fb8982c 100644
--- a/packages-adapters/bootstrap/src/icon/icon.ts
+++ b/packages-adapters/bootstrap/src/icon/icon.ts
@@ -2,10 +2,10 @@ import template from './icon.html';
import './icon.scss';
-import { bindable, customElement } from 'aurelia';
+import { bindable, customElement, resolve } from 'aurelia';
+import iconsUrl from 'bootstrap-icons/bootstrap-icons.svg';
-// TODO make icons path configurable local/remote
-const spritePath = 'bootstrap-icons.svg';
+import { IBootstrapOptions } from '../configuration';
@customElement({
name: 'bs-icon',
@@ -17,19 +17,21 @@ export class BsIcon {
use!: HTMLElement;
- attaching() {
- this.setIcon();
+ private readonly _options = resolve(IBootstrapOptions);
+
+ bound() {
+ this.#setIcon();
}
nameChanged() {
- this.setIcon();
+ this.#setIcon();
}
- private setIcon(): void {
+ #setIcon(): void {
+ const url = this._options.iconsSpritePath ?? (iconsUrl as string);
+
// to support simple binding to SVG attributes from template one needs to include Aurelia SVGAnalyzer
// we use a simpler approach with custom setAttribute()
- this.use.setAttribute('xlink:href', `${spritePath}#${this.name}`);
- // SVG 2 removed the need for the xlink namespace, so instead of xlink:href you should use href
- this.use.setAttribute('href', `${spritePath}#${this.name}`);
+ this.use.setAttribute('xlink:href', `${url}#${this.name}`);
}
}
diff --git a/packages-adapters/bootstrap/src/index.ts b/packages-adapters/bootstrap/src/index.ts
index ca59c25..8fac0dc 100644
--- a/packages-adapters/bootstrap/src/index.ts
+++ b/packages-adapters/bootstrap/src/index.ts
@@ -1,35 +1,21 @@
-// import { Container, FrameworkConfiguration, PLATFORM } from 'aurelia-framework';
+import * as components from './components';
+import { createConfiguration, IBootstrapOptions } from './configuration';
+import * as content from './content';
+import * as forms from './forms';
+import { BsIcon } from './icon';
-// import {
-// CheckboxCustomElement,
-// FileFieldCustomElement,
-// RadioCustomElement,
-// TextFieldCustomElement,
-// SelectCustomElement,
-// SwitchCustomElement,
-// } from './elements/forms';
-
-// import { Config } from './config';
-//
-// export function configure(frameworkConfig: FrameworkConfiguration, callback) {
-// frameworkConfig.globalResources([
-// CheckboxCustomElement,
-// FileFieldCustomElement,
-// RadioCustomElement,
-// SelectCustomElement,
-// SwitchCustomElement,
-// TextFieldCustomElement,
-// ]);
-//
-// if (typeof callback === 'function') {
-// callback(Container.instance.get(Config));
-// }
-//
-// frameworkConfig.plugin(PLATFORM.moduleName('aurelia-inputmask'));
-// }
+const BootstrapConfiguration = createConfiguration({}, [
+ ...Object.values(components),
+ ...Object.values(forms),
+ ...Object.values(content),
+ BsIcon,
+]);
export * from './components';
+// modal and offcanvas dialog impls must be exported separately not to be registered as resources automatically
+export * from './components/modal/dialog-impl';
+export * from './components/offcanvas/dialog-impl';
export * from './content';
export * from './forms';
-export * from './icon';
export * from './types';
+export { BootstrapConfiguration, BsIcon, IBootstrapOptions };
diff --git a/packages-adapters/bootstrap/src/utils.ts b/packages-adapters/bootstrap/src/utils.ts
index 7756999..ce0d3c8 100644
--- a/packages-adapters/bootstrap/src/utils.ts
+++ b/packages-adapters/bootstrap/src/utils.ts
@@ -1,15 +1,16 @@
import { coerceBoolean } from '@ekzo-dev/toolkit';
-export const addEventListener = (element: Element, type: string, listener: EventListenerOrEventListenerObject) =>
- element.addEventListener(type, listener);
-
-export const removeEventListener = (element: Element, type: string, listener: EventListenerOrEventListenerObject) =>
- element.removeEventListener(type, listener);
-
+/**
+ * Generate unique identifier
+ */
export function uniqueId(): string {
return 'id' + Math.random().toString(36).substring(2, 9);
}
+/**
+ * Coerce value to a boolean if it does not match a provided string
+ * @param str
+ */
export const coerceBooleanOrString = (str: string) => ({
- set: (value: string | boolean) => (value === str ? str : coerceBoolean.set(value)),
+ set: (value: unknown): string | boolean | undefined => (value === str ? str : coerceBoolean.set(value)),
});
diff --git a/packages-adapters/bootstrap/tsconfig.build.json b/packages-adapters/bootstrap/tsconfig.build.json
deleted file mode 100644
index 815a4ce..0000000
--- a/packages-adapters/bootstrap/tsconfig.build.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": "../../tsconfig.build.json",
- "include": [
- "src"
- ]
-}
diff --git a/packages-adapters/bootstrap/tsconfig.json b/packages-adapters/bootstrap/tsconfig.json
index c2112ac..4082f16 100644
--- a/packages-adapters/bootstrap/tsconfig.json
+++ b/packages-adapters/bootstrap/tsconfig.json
@@ -1,6 +1,3 @@
{
- "extends": "../../tsconfig.json",
- "include": [
- "src"
- ]
+ "extends": "../../tsconfig.json"
}
diff --git a/packages-adapters/bs-stepper/package.json b/packages-adapters/bs-stepper/package.json
index c537ee7..dbe2e70 100644
--- a/packages-adapters/bs-stepper/package.json
+++ b/packages-adapters/bs-stepper/package.json
@@ -1,7 +1,7 @@
{
"name": "@ekzo-dev/bs-stepper",
"description": "Aurelia Bootstrap stepper adapter",
- "version": "1.7.1",
+ "version": "1.7.2",
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/bs-stepper",
"repository": {
"type": "git",
@@ -9,6 +9,7 @@
},
"license": "MIT",
"dependencies": {
+ "@ekzo-dev/toolkit": "^1.3.0",
"bs-stepper": "~1.7.0"
},
"peerDependencies": {
diff --git a/packages-adapters/bs-stepper/src/elements/bs-stepper.stories.ts b/packages-adapters/bs-stepper/src/elements/bs-stepper.stories.ts
index 92a55d7..4d0fdfd 100644
--- a/packages-adapters/bs-stepper/src/elements/bs-stepper.stories.ts
+++ b/packages-adapters/bs-stepper/src/elements/bs-stepper.stories.ts
@@ -1,48 +1,48 @@
-import { BsButton, BsInput } from '@ekzo-dev/bootstrap';
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { BsStepper } from './bs-stepper';
-import { BsStepperStep } from './bs-stepper-step';
-
-const meta: Meta = {
- title: 'BS stepper / Bootstrap stepper',
- component: BsStepper,
- parameters: {
- actions: {
- // TODO: bs-stepper events do not buddle, but Storybook catches events on the parent element (:
- handles: ['show.bs-stepper', 'shown.bs-stepper'],
- },
- },
-};
-
-export default meta;
-
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- components: [BsStepperStep, BsButton, BsInput],
- template: `
-
-
-
-
- Next
-
-
-
-
-
- Previous
- Next
-
-
-
-
- Previous
- Submit
-
-
-
- `,
- props: args,
-});
-
-export { Overview };
+// import { BsButton, BsInput } from '@ekzo-dev/bootstrap';
+// import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
+//
+// import { BsStepper } from './bs-stepper';
+// import { BsStepperStep } from './bs-stepper-step';
+//
+// const meta: Meta = {
+// title: 'BS stepper / Bootstrap stepper',
+// component: BsStepper,
+// parameters: {
+// actions: {
+// // TODO: bs-stepper events do not buddle, but Storybook catches events on the parent element (:
+// handles: ['show.bs-stepper', 'shown.bs-stepper'],
+// },
+// },
+// };
+//
+// export default meta;
+//
+// const Overview: Story = (args): StoryFnAureliaReturnType => ({
+// components: [BsStepperStep, BsButton, BsInput],
+// template: `
+//
+//
+//
+//
+// Next
+//
+//
+//
+//
+//
+// Previous
+// Next
+//
+//
+//
+//
+// Previous
+// Submit
+//
+//
+//
+// `,
+// props: args,
+// });
+//
+// export { Overview };
diff --git a/packages-adapters/bs-stepper/src/elements/bs-stepper.ts b/packages-adapters/bs-stepper/src/elements/bs-stepper.ts
index 4574d4c..6f1c91f 100644
--- a/packages-adapters/bs-stepper/src/elements/bs-stepper.ts
+++ b/packages-adapters/bs-stepper/src/elements/bs-stepper.ts
@@ -3,13 +3,12 @@ import template from './bs-stepper.html';
import 'bs-stepper/dist/css/bs-stepper.min.css';
import './bs-stepper.scss';
+import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, customElement, ICustomElementViewModel, observable, resolve } from 'aurelia';
import Stepper from 'bs-stepper';
import { type BsStepperStep } from '../index';
-import { coerceBoolean } from './utils';
-
export interface IBsStepperEventDetail {
to: number;
from: number;
diff --git a/packages-adapters/bs-stepper/src/elements/utils.ts b/packages-adapters/bs-stepper/src/elements/utils.ts
deleted file mode 100644
index 3c64f1c..0000000
--- a/packages-adapters/bs-stepper/src/elements/utils.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const coerceBoolean = { set: (v: string | boolean) => v === '' || v === true };
diff --git a/packages-adapters/bs-stepper/tsconfig.json b/packages-adapters/bs-stepper/tsconfig.json
index 63b5082..4082f16 100644
--- a/packages-adapters/bs-stepper/tsconfig.json
+++ b/packages-adapters/bs-stepper/tsconfig.json
@@ -1,3 +1,3 @@
{
- "extends": "../../tsconfig.json"
+ "extends": "../../tsconfig.json"
}
diff --git a/packages-adapters/gridstack/src/gs-grid.stories.ts b/packages-adapters/gridstack/src/gs-grid.stories.ts
index 06aee3c..e099f91 100644
--- a/packages-adapters/gridstack/src/gs-grid.stories.ts
+++ b/packages-adapters/gridstack/src/gs-grid.stories.ts
@@ -1,96 +1,96 @@
-import './gs-grid.stories.css';
-
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { GsGrid, GsItem } from '.';
-
-const meta: Meta = {
- title: 'gridstack.js / Grid',
- component: GsGrid,
- parameters: {
- actions: {
- handles: [
- 'added',
- 'change',
- 'disable',
- 'dragstart',
- 'drag',
- 'dragstop',
- 'dropped',
- 'enable',
- 'removed',
- 'resizestart',
- 'resize',
- 'resizestop',
- ],
- },
- },
-};
-
-export default meta;
-
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- components: [GsItem],
- innerHtml: `
- 1
- 2
- Drag me!
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- `,
- props: args,
-});
-
-Overview.args = {
- options: {
- cellHeight: 70,
- },
-};
-
-const Nested: Story = (args): StoryFnAureliaReturnType => ({
- components: [GsItem],
- innerHtml: `
- regular item
-
-
- 0
- 1
- 2
- 3
- 4
- 5
-
-
-
-
- 6
- 7
-
-
- `,
- props: args,
-});
-
-Nested.args = {
- options: {
- cellHeight: 50,
- margin: 5,
- minRow: 2, // don't collapse when empty
- disableOneColumnMode: true,
- acceptWidgets: true,
- },
- subGridOptions: {
- cellHeight: 50,
- column: 'auto',
- acceptWidgets: true,
- margin: 5,
- },
-};
-
-export { Nested, Overview };
+// import './gs-grid.stories.css';
+//
+// import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
+//
+// import { GsGrid, GsItem } from '.';
+//
+// const meta: Meta = {
+// title: 'gridstack.js / Grid',
+// component: GsGrid,
+// parameters: {
+// actions: {
+// handles: [
+// 'added',
+// 'change',
+// 'disable',
+// 'dragstart',
+// 'drag',
+// 'dragstop',
+// 'dropped',
+// 'enable',
+// 'removed',
+// 'resizestart',
+// 'resize',
+// 'resizestop',
+// ],
+// },
+// },
+// };
+//
+// export default meta;
+//
+// const Overview: Story = (args): StoryFnAureliaReturnType => ({
+// components: [GsItem],
+// innerHtml: `
+// 1
+// 2
+// Drag me!
+// 4
+// 5
+// 6
+// 7
+// 8
+// 9
+// 10
+// 11
+// `,
+// props: args,
+// });
+//
+// Overview.args = {
+// options: {
+// cellHeight: 70,
+// },
+// };
+//
+// const Nested: Story = (args): StoryFnAureliaReturnType => ({
+// components: [GsItem],
+// innerHtml: `
+// regular item
+//
+//
+// 0
+// 1
+// 2
+// 3
+// 4
+// 5
+//
+//
+//
+//
+// 6
+// 7
+//
+//
+// `,
+// props: args,
+// });
+//
+// Nested.args = {
+// options: {
+// cellHeight: 50,
+// margin: 5,
+// minRow: 2, // don't collapse when empty
+// disableOneColumnMode: true,
+// acceptWidgets: true,
+// },
+// subGridOptions: {
+// cellHeight: 50,
+// column: 'auto',
+// acceptWidgets: true,
+// margin: 5,
+// },
+// };
+//
+// export { Nested, Overview };
diff --git a/packages-adapters/gridstack/src/gs-grid.ts b/packages-adapters/gridstack/src/gs-grid.ts
index 9028143..f4db785 100644
--- a/packages-adapters/gridstack/src/gs-grid.ts
+++ b/packages-adapters/gridstack/src/gs-grid.ts
@@ -16,7 +16,7 @@ export class GsGrid {
options: GridStackOptions = {};
@slotted('gs-item')
- private slottedItems: GridItemHTMLElement[];
+ private slottedItems!: GridItemHTMLElement[];
grid?: GridStack;
@@ -49,11 +49,12 @@ export class GsGrid {
if (options.column === 'auto' && parentItem) {
autoColumn = true;
- options.column = parentItem.gridstackNode.w || 1;
+ options.column = parentItem.gridstackNode?.w || 1;
options.disableOneColumnMode = true;
}
this.grid = GridStack.init(options, this.element);
+ // @ts-ignore
this.grid['_autoColumn'] = autoColumn;
// init items
@@ -70,12 +71,12 @@ export class GsGrid {
if (!grid || !elements) return;
// start update transaction
- this.grid.batchUpdate();
+ this.grid?.batchUpdate();
// remove missing widgets
const removed = grid.engine.nodes.filter((x) => !elements.find((el) => el === x.el));
- removed.forEach((x) => grid.removeWidget(x.el, false));
+ removed.forEach((x) => grid.removeWidget(x.el!, false));
// add new widgets
elements.forEach((element) => {
@@ -86,7 +87,7 @@ export class GsGrid {
grid.addWidget(element, item.options);
if (element.dataset.hasSubgrid) {
- const subgrid = element.querySelector('gs-grid');
+ const subgrid = element.querySelector('gs-grid')!;
const component = CustomElement.for(subgrid).viewModel;
component.createGrid(element);
@@ -95,13 +96,13 @@ export class GsGrid {
// commit changes
grid.engine.removedNodes = removed;
- this.grid.batchUpdate(false);
+ this.grid?.batchUpdate(false);
}
private findParentItem(el: HTMLElement): GridItemHTMLElement | null {
while (el) {
if (el.nodeName === 'GS-ITEM') return el;
- el = el.parentElement;
+ el = el.parentElement!;
}
return null;
diff --git a/packages-adapters/gridstack/src/gs-item.ts b/packages-adapters/gridstack/src/gs-item.ts
index 549bcf6..660c6eb 100644
--- a/packages-adapters/gridstack/src/gs-item.ts
+++ b/packages-adapters/gridstack/src/gs-item.ts
@@ -10,9 +10,9 @@ export class GsItem implements ICustomElementViewModel {
@bindable()
options: GridStackWidget = {};
- constructor(public readonly element: GridItemHTMLElement = resolve(HTMLElement)) {}
+ readonly element: GridItemHTMLElement = resolve(HTMLElement);
optionsChanged() {
- this.element.gridstackNode?.grid.update(this.element, this.options);
+ this.element.gridstackNode?.grid?.update(this.element, this.options);
}
}
diff --git a/packages-adapters/json-schema-viewer/.eslintrc.js b/packages-adapters/json-schema-viewer/.eslintrc.js
deleted file mode 100644
index dc16c46..0000000
--- a/packages-adapters/json-schema-viewer/.eslintrc.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const path = require('path');
-const thisDir = path.resolve(__dirname);
-
-module.exports = {
- extends: ['../../.eslintrc.js'],
- parserOptions: {
- project: path.join(thisDir, 'tsconfig.json'),
- tsconfigRootDir: thisDir,
- },
- env: {
- browser: true,
- },
-};
diff --git a/packages-adapters/json-schema-viewer/CHANGELOG.md b/packages-adapters/json-schema-viewer/CHANGELOG.md
deleted file mode 100644
index 9bd8749..0000000
--- a/packages-adapters/json-schema-viewer/CHANGELOG.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [1.0.0](https://github.com/ekzo-dev/aurelia-components/compare/@ekzo-dev/json-schema-viewer@1.0.0-rc.0...@ekzo-dev/json-schema-viewer@1.0.0) (2023-11-08)
-
-**Note:** Version bump only for package @ekzo-dev/json-schema-viewer
-
-
-
-
-
-# 1.0.0-rc.0 (2023-11-06)
-
-
-### Features
-
-* **eslint-plugin-eslint-config:** lint all again ([fa273bc](https://github.com/ekzo-dev/aurelia-components/commit/fa273bcc466c81052e3ec5161862ebf9ec2330ca))
-* **eslint-plugin-eslint-config:** lint packages-adapters ([e2bbe4d](https://github.com/ekzo-dev/aurelia-components/commit/e2bbe4d0d3ff8449191861396e0db743935a5035))
-* **eslint-plugin-eslint-config:** revert lint fix ([4b22cb8](https://github.com/ekzo-dev/aurelia-components/commit/4b22cb80c1e38a5e6bc13d05c804941830606823))
-* **eslint-plugin-eslint-config:** update rules ([7031bfe](https://github.com/ekzo-dev/aurelia-components/commit/7031bfe1b1c5cab9ab283689b644b7758957e1ff))
-* **lint-staged:** configure ling-staged, update husky, add ncu to update deps ([c44b470](https://github.com/ekzo-dev/aurelia-components/commit/c44b4700bedc5ba2f4214311400b16b9bd679a45))
-* **lint:** add lint for css and html ([40bfdd7](https://github.com/ekzo-dev/aurelia-components/commit/40bfdd7122637e7e32659f1a9db233afb4bf3622))
diff --git a/packages-adapters/json-schema-viewer/README.md b/packages-adapters/json-schema-viewer/README.md
deleted file mode 100644
index f0ab69a..0000000
--- a/packages-adapters/json-schema-viewer/README.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# `print-form-manager`
-
-This project is bootstrapped by [aurelia-cli](https://github.com/aurelia/cli).
-
-This Aurelia plugin project has a built-in dev app (with CLI built-in bundler and RequireJS) to simplify development.
-
-1. The local `src/` folder, is the source code for the plugin.
-2. The local `dev-app/` folder, is the code for the dev app, just like a normal app bootstrapped by aurelia-cli.
-3. You can use normal `au run` and `au test` in development just like developing an app.
-4. You can use aurelia-testing to test your plugin, just like developing an app.
-5. To ensure compatibility to other apps, always use `PLATFORM.moduleName()` wrapper in files inside `src/`. You don't need to use the wrapper in `dev-app/` folder as CLI built-in bundler supports module name without the wrapper.
-
-Note aurelia-cli doesn't provide a plugin skeleton with Webpack setup (not yet), but this plugin can be consumed by any app using Webpack, or CLI built-in bundler, or jspm.
-
-## How to write an Aurelia plugin
-
-For a full length tutorial, visit [Aurelia plugin guide](https://aurelia.io/docs/plugins/write-new-plugin).
-
-Here is some basics. You can create new custom element, custom attribute, value converter or binding behavior manually, or use command `au generate` to help.
-```shell
-au generate element some-name
-au generate attribute some-name
-au generate value-converter some-name
-au generate binding-behavior some-name
-```
-
-By default, the cli generates command generates files in following folders:
-```
-src/elements
-src/attributes
-src/value-converters
-src/binding-behaviors
-```
-
-Note the folder structure is only to help you organising the files, it's not a requirement of Aurelia. You can manually create new element (or other thing) anywhere in `src/`.
-
-After you added some new file, you need to register it in `src/index.ts`. Like this:
-```js
-config.globalResources([
- // ...
- PLATFORM.moduleName('./path/to/new-file-without-ext')
-]);
-````
-
-The usage of `PLATFORM.moduleName` wrapper is mandatory. It's needed for your plugin to be consumed by any app using webpack, CLI built-in bundler, or jspm.
-
-## Resource import within the dev app
-
-In dev app, when you need to import something from the inner plugin (for example, importing a class for dependency injection), use special name `"resources"` to reference the inner plugin.
-
-```js
-import {autoinject} from 'aurelia-framework';
-// "resources" refers the inner plugin src/index.ts
-import {MyService} from 'resources';
-
-@autoinject()
-export class App {
- constructor(myService: MyService) {}
-}
-```
-
-## Manage dependencies
-
-By default, this plugin has no "dependencies" in package.json. Theoretically this plugin depends on at least `aurelia-pal` because `src/index.ts` imports it. It could also depends on more core Aurelia package like `aurelia-binding` or `aurelia-templating` if you build advanced components that reference them.
-
-Ideally you need to carefully add those `aurelia-pal` (`aurelia-binding`...) to "dependencies" in package.json. But in practice you don't have to. Because every app that consumes this plugin will have full Aurelia core packages installed.
-
-Furthermore, there are two benefits by leaving those dependencies out of plugin's package.json.
-1. ensure this plugin doesn't bring in a duplicated Aurelia core package to consumers' app. This is mainly for app built with webpack. We had been hit with `aurelia-binding` v1 and v2 conflicts due to 3rd party plugin asks for `aurelia-binding` v1.
-2. reduce the burden for npm/yarn when installing this plugin.
-
-If you are a perfectionist who could not stand leaving out dependencies, I recommend you to add `aurelia-pal` (`aurelia-binding`...) to "peerDependencies" in package.json. So at least it could not cause a duplicated Aurelia core package.
-
-If your plugin depends on other npm package, like `lodash` or `jquery`, **you have to add them to "dependencies" in package.json**.
-
-## Build Plugin
-
-Run `au build-plugin`. This will transpile all files from `src/` folder to `dist/native-modules/` and `dist/commonjs/`.
-
-For example, `src/index.ts` will become `dist/native-modules/index.js` and `dist/commonjs/index.js`.
-
-Note all other files in `dev-app/` folder are for the dev app, they would not appear in the published npm package.
-
-## Consume Plugin
-
-By default, the `dist/` folder is not committed to git. (We have `/dist` in `.gitignore`). But that would not prevent you from consuming this plugin through direct git reference.
-
-You can consume this plugin directly by:
-```shell
-npm i github:your_github_username/print-form-manager
-# or if you use bitbucket
-npm i bitbucket:your_github_username/print-form-manager
-# or if you use gitlab
-npm i gitlab:your_github_username/print-form-manager
-# or plain url
-npm i https:/github.com/your_github_username/print-form-manager.git
-```
-
-Then load the plugin in app's `main.ts` like this.
-```js
-aurelia.use.plugin('print-form-manager');
-// for webpack user, use PLATFORM.moduleName wrapper
-aurelia.use.plugin(PLATFORM.moduleName('print-form-manager'));
-```
-
-The missing `dist/` files will be filled up by npm through `"prepare": "npm run build"` (in `"scripts"` section of package.json).
-
-Yarn has a [bug](https://github.com/yarnpkg/yarn/issues/5235) that ignores `"prepare"` script. If you want to use yarn to consume your plugin through direct git reference, remove `/dist` from `.gitignore` and commit all the files. Note you don't need to commit `dist/` files if you only use yarn to consume this plugin through published npm package (`npm i print-form-manager`).
-
-## Publish npm package
-
-By default, `"private"` field in package.json has been turned on, this prevents you from accidentally publish a private plugin to npm.
-
-To publish the plugin to npm for public consumption:
-
-1. Remove `"private": true,` from package.json.
-2. Pump up project version. This will run through `au test` (in "preversion" in package.json) first.
-```shell
-npm version patch # or minor or major
-```
-3. Push up changes to your git server
-```shell
-git push && git push --tags
-```
-4. Then publish to npm, you need to have your npm account logged in.
-```shell
-npm publish
-```
-
-## Automate changelog, git push, and npm publish
-
-You can enable `npm version patch # or minor or major` to automatically update changelog, push commits and version tag to the git server, and publish to npm.
-
-Here is one simple setup.
-1. `npm i -D standard-changelog`. We use [`standard-changelog`](https://github.com/conventional-changelog/conventional-changelog) as a minimum example to support conventional changelog.
- * Alternatively you can use high level [standard-version](https://github.com/conventional-changelog/standard-version).
-2. Add two commands to `"scripts"` section of package.json.
-```
-"scripts": {
- // ...
- "version": "standard-changelog && git add CHANGELOG.md",
- "postversion": "git push && git push --tags && npm publish"
-},
-```
-3. you can remove `&& npm publish` if your project is private
-
-For more information, go to https://aurelia.io/docs/cli/cli-bundler
-
-## Run dev app
-
-Run `au run`, then open `http://localhost:9000`
-
-To open browser automatically, do `au run --open`.
-
-To change dev server port, do `au run --port 8888`.
-
-To change dev server host, do `au run --host 127.0.0.1`
-
-
-**PS:** You could mix all the flags as well, `au run --host 127.0.0.1 --port 7070 --open`
-
-
-## Unit tests
-
-Run `au test` (or `au jest`).
-
-To run in watch mode, `au test --watch` or `au jest --watch`.
diff --git a/packages-adapters/json-schema-viewer/package.json b/packages-adapters/json-schema-viewer/package.json
deleted file mode 100644
index 6ac9aa2..0000000
--- a/packages-adapters/json-schema-viewer/package.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "name": "@ekzo-dev/json-schema-viewer",
- "description": "Aurelia JSON Schema viewer",
- "version": "1.0.0",
- "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/json-schema-viewer",
- "repository": {
- "type": "git",
- "url": "https://github.com/ekzo-dev/aurelia-components.git"
- },
- "license": "MIT",
- "dependencies": {
- "d3": "^3.5.17",
- "highlight.js": "^11.9.0",
- "jquery": "^3.7.1",
- "jsonpointer.js": "^0.4.0",
- "tv4": "^1.3.0"
- },
- "devDependencies": {
- "@types/d3": "^3.5.47",
- "@types/json-schema": "^7.0.14"
- },
- "peerDependencies": {
- "aurelia": "^2.0.0-rc.0"
- },
- "main": "src/index.ts",
- "files": [
- "src"
- ],
- "scripts": {
- "lint:js": "eslint src --ext .js,.ts",
- "lint:css": "stylelint \"**/*.*css\" --allow-empty-input",
- "lint:html": "prettier \"**/*.html\" --no-error-on-unmatched-pattern",
- "lint:all": "npm run lint:js && npm run lint:html && npm run lint:css",
- "start": "webpack serve",
- "build": "rimraf dist && webpack --env production",
- "analyze": "rimraf dist && webpack --env production --analyze",
- "test": "jest"
- },
- "jest": {
- "testMatch": [
- "/test/**/*.spec.ts"
- ],
- "testEnvironment": "jsdom",
- "transform": {
- "\\.(css|less|sass|scss|styl|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "jest-transform-stub",
- "\\.(ts|html)$": "@aurelia/ts-jest"
- },
- "collectCoverage": true,
- "collectCoverageFrom": [
- "src/**/*.ts",
- "!src/**/*.d.ts"
- ],
- "globals": {
- "ts-jest": {
- "isolatedModules": true
- }
- }
- },
- "publishConfig": {
- "access": "public"
- }
-}
diff --git a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.html b/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.html
deleted file mode 100644
index 69d30c3..0000000
--- a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
diff --git a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.scss b/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.scss
deleted file mode 100644
index c9674f0..0000000
--- a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.scss
+++ /dev/null
@@ -1,130 +0,0 @@
-/* stylelint-disable */
-
-json-schema-viewer {
- #jsv-tree {
- height: 100%;
- }
-
- #loading {
- background-color: #fff;
- font-weight: 900;
- height: 100%;
- left: 0;
- padding-top: 5em;
- position: absolute;
- text-align: center;
- top: 0;
- width: 100%;
- z-index: 1000;
- }
- #main-body {
- margin: 15px;
- border: 2px #f1c15b solid;
- border-radius: 14px;
- height: 90%;
- position: relative;
- }
- a {
- color: #058adc;
- display: block;
- text-decoration: none;
- padding: 2px;
- font-weight: 900;
- font-size: 16px;
- }
- a:hover {
- color: #dc9705;
- }
- a,
- #legend {
- font-family: monospace;
- }
- #legend-container {
- background-color: #ffeecb;
- padding: 3px 0;
- border-radius: 14px;
- opacity: 0.9;
- font-size: 0.9em;
- position: absolute;
- right: 20px;
- top: 15px;
- width: 200px;
- text-align: center;
- }
- #legend-container hr {
- border: 0;
- height: 2px;
- background-color: #f1c15b;
- }
- #legend-container h3 {
- margin: 0.5em 0;
- }
- #zoom-controls {
- left: 10px;
- position: absolute;
- top: 15px;
- }
-
- .node {
- cursor: pointer;
- }
- .overlay {
- background-color: inherit;
- }
- .node circle,
- #legend circle {
- fill: #fff;
- stroke: steelblue;
- stroke-width: 1.5px;
- }
- .node circle.collapsed,
- #legend circle.collapsed {
- fill: lightsteelblue;
- }
- .node .abstract,
- #legend .abstract {
- font-style: italic;
- }
- .node.label {
- cursor: default;
- }
- .node.label circle,
- .node.label circle:hover {
- stroke: #ccc;
- stroke-width: 1.5px;
- }
- .node text {
- font-family: 'Courier New', monospace;
- font-size: 14px;
- }
- .node text:hover {
- text-decoration: underline;
- }
- .node.label text:hover {
- text-decoration: none;
- }
- .node.focus,
- #legend .focus {
- font-weight: bold;
- }
- .node.focus circle,
- #legend .focus circle {
- stroke: #0ecc43;
- stroke-width: 2px;
- }
- .node circle:hover,
- .focus circle:hover {
- stroke-width: 3px;
- }
- .link {
- fill: none;
- stroke: #ccc;
- stroke-width: 1.5px;
- }
- .deprecated {
- color: orange;
- }
- .deprecated text {
- fill: orange;
- }
-}
diff --git a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.ts b/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.ts
deleted file mode 100644
index c90a508..0000000
--- a/packages-adapters/json-schema-viewer/src/elements/json-schema-viewer.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import template from './json-schema-viewer.html';
-
-import './json-schema-viewer.scss';
-
-import type { JSONSchema7 } from 'json-schema';
-
-import { bindable, customElement, ICustomElementViewModel, resolve } from 'aurelia';
-
-import { JSV } from '../viewer';
-
-@customElement({
- name: 'json-schema-viewer',
- template,
-})
-export class JsonSchemaViewer implements ICustomElementViewModel {
- @bindable()
- schema!: JSONSchema7;
-
- loader?: HTMLDivElement;
-
- constructor(private readonly element: HTMLElement = resolve(HTMLElement)) {}
-
- attached() {
- JSV.init(
- {
- schema: this.schema,
- plain: true, //don't use JQM
- viewerHeight: 600, //set initial dimensions of SVG
- viewerWidth: this.element.getBoundingClientRect().width,
- },
- () => {
- document.getElementById('jsv-tree').style.width = '100%';
- //set diagram width to 100%, this DOES NOT resize the svg container
- //it will not adjust to window resize, needs a listener to support that
- JSV.resetViewer();
-
- this.loader.style.display = 'none';
- }
- );
- }
-}
diff --git a/packages-adapters/json-schema-viewer/src/index.ts b/packages-adapters/json-schema-viewer/src/index.ts
deleted file mode 100644
index 2d30771..0000000
--- a/packages-adapters/json-schema-viewer/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './elements/json-schema-viewer';
diff --git a/packages-adapters/json-schema-viewer/src/viewer.ts b/packages-adapters/json-schema-viewer/src/viewer.ts
deleted file mode 100644
index b3b37de..0000000
--- a/packages-adapters/json-schema-viewer/src/viewer.ts
+++ /dev/null
@@ -1,1521 +0,0 @@
-/* eslint-disable */
-import d3 from 'd3';
-import hljs from 'highlight.js';
-import $ from 'jquery';
-import jsonpointer from 'jsonpointer.js';
-import tv4 from 'tv4';
-
-/**
- * JSV namespace for JSON Schema Viewer.
- * @namespace
- */
-export const JSV = {
- schemaUri: 'base',
-
- /**
- * The root schema to load.
- */
- schema: '',
-
- /**
- * If true, render diagram only on init, without the jQuery Mobile UI.
- * The legend and nav tools will be rendered with any event listeners.
- */
- plain: false,
-
- /**
- * The version of the schema.
- */
- version: '',
-
- /**
- * Currently focused node
- */
- focusNode: null,
-
- /**
- * Currently loaded example
- */
- example: false,
-
- /**
- * @property {object} treeData The diagram nodes
- */
- treeData: null,
-
- /**
- * The initialization status of the viewer page
- */
- viewerInit: false,
-
- /**
- * The current viewer height
- */
- viewerHeight: 0,
-
- /**
- * The current viewer width
- */
- viewerWidth: 0,
-
- /**
- * The default duration of the node transitions
- */
- duration: 750,
-
- /**
- * Counter for generating unique ids
- */
- counter: 0,
-
- maxLabelLength: {},
-
- /**
- * Default maximum depth for recursive schemas
- */
- maxDepth: 20,
-
- /**
- * @property {object} labels Nodes to render as non-clickable in the tree. They will auto-expand if child nodes are present.
- */
- labels: {
- allOf: true,
- anyOf: true,
- oneOf: true,
- // 'object{ }': true,
- },
-
- /**
- * @property {array} baseSvg The base SVG element for the d3 diagram
- */
- baseSvg: null,
-
- /**
- * @property {array} svgGroup SVG group which holds all nodes and which the zoom Listener can act upon.
- */
- svgGroup: null,
-
- tree: null,
-
- /**
- * Initializes the viewer.
- *
- * @param {object} config The configuration.
- * @param {function} callback Function to run after schemas are loaded and
- * diagram is created.
- */
-
- init: function (config, callback) {
- let i;
-
- //apply config
- for (i in config) {
- if (JSV.hasOwnProperty(i)) {
- JSV[i] = config[i];
- }
- }
-
- if (this.plain) {
- JSV.createDiagram(callback);
- //setup controls
- d3.selectAll('#zoom-controls>a').on('click', JSV.zoomClick);
- d3.select('#tree-controls>a#reset-tree').on('click', JSV.resetViewer);
- JSV.viewerInit = true;
-
- return;
- }
-
- JSV.contentHeight();
- JSV.resizeViewer();
-
- $(document).on('pagecontainertransition', this.contentHeight);
- $(window).on('throttledresize orientationchange', this.contentHeight);
- $(window).on('resize', this.contentHeight);
-
- JSV.resizeBtn();
- $(document).on('pagecontainershow', JSV.resizeBtn);
- $(window).on('throttledresize', JSV.resizeBtn);
-
- const cb = function () {
- callback();
-
- //Setup search
- //Build array of nodes to search, this will need to be refreshed
- //if nodes are added/removed. Might be better to search
- //JSV.treeData directly.
- const items = [];
-
- JSV.visit(
- JSV.treeData,
- function (me) {
- if (me.isReal) {
- items.push(me.plainName + '|' + JSV.getNodePath(me).join('-'));
- }
- },
- function (me) {
- return me.children || me._children;
- }
- );
-
- items.sort();
-
- $('#viewer-page #search-result').on('filterablebeforefilter', function (e, data) {
- const $ul = $(this),
- $input = $(data.input),
- value = $input.val(),
- html = '';
-
- $ul.html('');
- $ul.on('click', function (e) {
- const path = $(e.target).attr('data-path');
- const node = JSV.expandNodePath(path.split('-'));
-
- JSV.flashNode(node);
- });
-
- if (value && value.length > 2) {
- $ul.html('Searching... ');
- $ul.listview('refresh');
-
- JSV.buildSearchList(items, value);
- }
- });
-
- $('#loading').fadeOut('slow');
- };
-
- JSV.createDiagram(cb);
-
- JSV.initValidator();
-
- //initialize error popup
- $('#popup-error').enhanceWithin().popup();
-
- ///highlight plugin
- $.fn.highlight = function (str, className, quote) {
- const string = quote ? '\\"\\b' + str + '\\b\\"' : '\\b' + str + '\\b',
- regex = new RegExp(string, 'g');
-
- return this.each(function () {
- this.innerHTML = this.innerHTML.replace(regex, function (matched) {
- return '' + matched + ' ';
- });
- });
- };
-
- //restore info-panel state
- $('body').on('pagecontainershow', function (event, ui) {
- const page = ui.toPage;
-
- if (page.attr('id') === 'viewer-page' && JSV.viewerInit) {
- if (page.jqmData('infoOpen')) {
- $('#info-panel').panel('open');
- }
-
- //TODO: add this to 'pagecontainercreate' handler on refactor???
- JSV.contentHeight();
-
- if ($('svg#jsv-tree').height() === 0) {
- $('svg#jsv-tree').attr('width', $('#main-body').width()).attr('height', $('#main-body').height());
- JSV.resizeViewer();
- JSV.resetViewer();
- }
- }
- });
-
- //store info-panel state
- $('body').on('pagecontainerbeforehide', function (event, ui) {
- const page = ui.prevPage;
-
- if (page.attr('id') === 'viewer-page') {
- page.jqmData('infoOpen', !!page.find('#info-panel.ui-panel-open').length);
- }
- });
-
- //resize viewer on panel open/close
- $('#info-panel').on('panelopen', function () {
- const focus = JSV.focusNode;
-
- JSV.resizeViewer();
-
- if (focus) {
- d3.select('#n-' + focus.id).classed('focus', true);
- JSV.setPermalink(focus);
- }
- });
-
- $('#info-panel').on('panelclose', function () {
- const focus = JSV.focusNode;
-
- JSV.resizeViewer();
-
- if (focus) {
- d3.select('#n-' + focus.id).classed('focus', false);
- $('#permalink').html('Select a Node...');
- $('#sharelink').val('');
- }
- });
-
- //scroll example/schema when tab is activated
- $('#info-panel').on('tabsactivate', function (event, ui) {
- const id = ui.newPanel.attr('id');
-
- if (id === 'info-tab-example' || id === 'info-tab-schema') {
- const pre = ui.newPanel.find('pre'),
- highEl = pre.find('span.highlight')[0];
-
- if (highEl) {
- pre.scrollTo(highEl, 900);
- }
- }
- });
-
- //setup example links
- $('.load-example').each(function (idx, link) {
- const ljq = $(link);
-
- ljq.on('click', function (evt) {
- evt.preventDefault();
- JSV.loadInputExample(link.href, ljq.data('target'));
- });
- });
-
- //setup controls
- d3.selectAll('#zoom-controls>a').on('click', JSV.zoomClick);
- d3.select('#tree-controls>a#reset-tree').on('click', JSV.resetViewer);
-
- $('#sharelink').on('click', function () {
- $(this).select();
- });
-
- JSV.viewerInit = true;
- },
-
- /**
- * (Re)set the viewer page height, set the diagram dimensions.
- */
- contentHeight: function () {
- const screen = $.mobile.getScreenHeight(),
- header = $('.ui-header').hasClass('ui-header-fixed')
- ? $('.ui-header').outerHeight() - 1
- : $('.ui-header').outerHeight(),
- footer = $('.ui-footer').hasClass('ui-footer-fixed')
- ? $('.ui-footer').outerHeight() - 1
- : $('.ui-footer').outerHeight(),
- contentCurrent = $('#main-body.ui-content').outerHeight() - $('#main-body.ui-content').height(),
- content = screen - header - footer - contentCurrent;
-
- $('#main-body.ui-content').css('min-height', content + 'px');
- },
-
- /**
- * Hides navbar button text on smaller window sizes.
- *
- * @param {number} minSize The navbar width breakpoint.
- */
- resizeBtn: function (minSize?) {
- const bp = typeof minSize === 'number' ? minSize : 800;
- const activePage = $.mobile.pageContainer.pagecontainer('getActivePage');
-
- if ($('.md-navbar', activePage).width() <= bp) {
- $('.md-navbar .md-flex-btn.ui-btn-icon-left').toggleClass('ui-btn-icon-notext ui-btn-icon-left');
- } else {
- $('.md-navbar .md-flex-btn.ui-btn-icon-notext').toggleClass('ui-btn-icon-left ui-btn-icon-notext');
- }
- },
-
- /**
- * Set version of the schema and the content
- * of any elemant with the class *schema-version*.
- *
- * @param {string} version
- */
- setVersion: function (version) {
- JSV.version = version;
-
- $('.schema-version').text(version);
- },
-
- /**
- * Display an error message.
- *
- * @param {string} msg The message to display.
- */
- showError: function (msg) {
- $('#popup-error .error-message').html(msg);
- $('#popup-error').popup('open');
- },
-
- initValidator: function () {
- const opts = {
- readAsDefault: 'Text',
- on: {
- load: function (e, file) {
- const data = e.currentTarget.result;
-
- try {
- $.parseJSON(data);
- //console.info(data);
- $('#textarea-json').val(data);
- } catch (err) {
- //JSV.showError('Unable to parse JSON: ' + e);
- JSV.showError(
- 'Failed to load ' + file.name + '. The file is not valid JSON. The error: ' + err + ' '
- );
- }
- },
- error: function (e, file) {
- const msg = 'Failed to load ' + file.name + '. ' + e.currentTarget.error.message;
-
- JSV.showError(msg);
- },
- },
- };
-
- $('#file-upload, #textarea-json').fileReaderJS(opts);
- $('body').fileClipboard(opts);
-
- $('#button-validate').click(function () {
- const result = JSV.validate();
-
- if (result) {
- JSV.showValResult(result);
- }
- //console.info(result);
- });
- },
-
- /**
- * Validate using tv4 and currently loaded schema(s).
- */
- validate: function () {
- let data;
-
- try {
- data = $.parseJSON($('#textarea-json').val());
- } catch (e) {
- JSV.showError('Unable to parse JSON: ' + e);
- }
-
- if (data) {
- const stop = $('#checkbox-stop').is(':checked'),
- strict = $('#checkbox-strict').is(':checked'),
- // schema = tv4.getSchemaMap()[JSV.schemaUri],
- result = {
- valid: true,
- errors: [],
- };
-
- // if (stop) {
- // var r = tv4.validate(data, schema, false, strict);
- // result = {
- // valid: r,
- // errors: !r ? [tv4.error] : [],
- // };
- // } else {
- // result = tv4.validateMultiple(data, schema, false, strict);
- // }
-
- return result;
- }
- },
-
- /**
- * Display the validation result
- *
- * @param {object} result A result object, ouput from [validate]{@link JSV.validate}
- */
- showValResult: function (result) {
- let cont = $('#validation-results'),
- ui;
-
- if (cont.children().length) {
- cont.css('opacity', 0);
- }
-
- if (result.valid) {
- cont.html('JSON is valid!
');
- } else {
- ui = cont.html('JSON is NOT valid!
');
- $.each(result.errors, function (i, err) {
- const me = JSV.buildValError(err, 'Error ' + (i + 1) + ': ');
-
- if (err.subErrors) {
- $.each(err.subErrors, function (i, sub) {
- me.append(JSV.buildValError(sub, 'SubError ' + (i + 1) + ': '));
- });
- }
-
- ui.children('.ui-content').first().append(me).enhanceWithin();
- });
- }
-
- cont.toggleClass('error', !result.valid);
- $('#validator-page').animate(
- {
- scrollTop: $('#validation-results').offset().top + 20,
- },
- 1000
- );
-
- cont.fadeTo(350, 1);
- },
-
- /**
- * Build a collapsible validation block.
- *
- * @param {object} err The error object
- * @param {string} title The title for the error block
- */
- buildValError: function (err, title) {
- const main =
- '' +
- '
' +
- (title || 'Error: ') +
- err.message +
- ' ' +
- '
Message: ' +
- err.message +
- ' ' +
- 'Data Path: ' +
- err.dataPath +
- ' ' +
- 'Schema Path: ' +
- err.schemaPath +
- ' ';
-
- return $(main);
- },
-
- /**
- * Set the content for the info panel.
- *
- * @param {object} node The d3 tree node.
- */
- setInfo: function (node) {
- const schema = $('#info-tab-schema');
- const def = $('#info-tab-def');
- const ex = $('#info-tab-example');
-
- const height =
- $('#info-panel').innerHeight() -
- $('#info-panel .ui-panel-inner').outerHeight() +
- $('#info-panel #info-tabs').height() -
- $('#info-panel #info-tabs-navbar').height() -
- (schema.outerHeight(true) - schema.height());
-
- $.each([schema, def, ex], function (i, e) {
- e.height(height);
- });
-
- $('#info-definition').html(node.description || 'No definition provided.');
- $('#info-type').html(node.displayType.toString());
-
- if (node.translation) {
- const trans = $('');
-
- $.each(node.translation, function (p, v) {
- const li = $('' + p + ' ');
- const ul = $('');
-
- $.each(v, function (i, e) {
- ul.append('' + e + ' ');
- });
-
- trans.append(li.append(ul));
- });
-
- $('#info-translation').html(trans);
- } else {
- $('#info-translation').html('No translations available.');
- }
-
- const parentDeps = node.parentSchema.dependencies && node.parentSchema.dependencies[node.name];
-
- if ($.isArray(parentDeps)) {
- const deps = $('');
-
- $.each(parentDeps, function (i, v) {
- const li = $('' + v + ' ');
-
- deps.append(li);
- });
-
- $('#info-dependencies').html(deps);
- } else if (parentDeps) {
- //assume schema object
- $('#info-dependencies').html('For schema dependencies see parent schema.');
- } else {
- $('#info-dependencies').html('No dependencies listed.');
- }
-
- JSV.createPre(schema, tv4.getSchema(node.schema), false, node.plainName);
-
- const example =
- !node.example && node.parent && node.parent.example && node.parent.type === 'object'
- ? node.parent.example
- : node.example;
-
- if (example) {
- if (example !== JSV.example) {
- $.getJSON(node.schema.match(/^(.*?)(?=[^\/]*\.json)/g) + example, function (data) {
- const pointer = example.split('#')[1];
-
- if (pointer) {
- data = jsonpointer.get(data, pointer);
- }
-
- JSV.createPre(ex, data, false, node.plainName);
- JSV.example = example;
- }).fail(function () {
- ex.html('No example found. ');
- JSV.example = false;
- });
- } else {
- let pre = ex.find('pre'),
- highEl;
-
- pre.find('span.highlight').removeClass('highlight');
-
- if (node.plainName) {
- pre.highlight(node.plainName, 'highlight', true);
- }
-
- //scroll to highlighted property
- highEl = pre.find('span.highlight')[0];
-
- if (highEl) {
- pre.scrollTo(highEl, 900);
- }
- }
- } else {
- ex.html('No example available. ');
- JSV.example = false;
- }
- },
-
- /**
- * Create a *pre* block and append it to the passed element.
- *
- * @param {object} el jQuery element
- * @param {object} obj The obj to stringify and display
- * @param {string} title The title for the new window
- * @param {string} exp The string to highlight
- */
- createPre: function (el, obj, title, exp) {
- const pre = $('' + JSON.stringify(obj, null, ' ') + ' ');
- const btn = $('Open in new window ').click(
- function () {
- const w = window.open('', 'pre', null);
-
- $(w.document.body).html($('').append(pre.clone().height('95%')).html());
- hljs.highlightBlock($(w.document.body).children('pre')[0]);
- $(w.document.body).append(
- '
'
- );
- w.document.title = title || 'JSON Schema Viewer';
- w.document.close();
- }
- );
-
- el.html(btn);
-
- if (exp) {
- pre.highlight(exp, 'highlight', true);
- }
-
- el.append(pre);
- pre.height(el.height() - btn.outerHeight(true) - (pre.outerHeight(true) - pre.height()));
-
- //scroll to highlighted property
- const highEl = pre.find('span.highlight')[0];
-
- if (highEl) {
- pre.scrollTo(highEl, 900);
- }
- },
-
- /**
- * Create a "breadcrumb" for the node.
- */
- compilePath: function (node, path?) {
- let p;
-
- if (node.parent) {
- p = path ? node.name + ' > ' + path : node.name;
-
- return JSV.compilePath(node.parent, p);
- } else {
- p = path ? node.name + ' > ' + path : node.name;
- }
-
- return p;
- },
-
- /**
- * Load an example in the specified input field.
- */
- loadInputExample: function (uri, target) {
- $.getJSON(uri)
- .done(function (fetched) {
- $('#' + target).val(JSON.stringify(fetched, null, ' '));
- })
- .fail(function (jqXHR, textStatus, errorThrown) {
- JSV.showError('Failed to load example: ' + errorThrown);
- });
- },
-
- /**
- * Create a "permalink" for the node.
- */
- setPermalink: function (node) {
- const uri = new URL(location.href),
- path = JSV.getNodePath(node).join('-');
-
- //uri.search({ v: path});
- uri.hash = $.mobile.activePage.attr('id') + '?v=' + path;
- $('#permalink').html(JSV.compilePath(node));
- $('#sharelink').val(uri.toString());
- },
-
- /**
- * Create an index-based path for the node from the root.
- */
- getNodePath: function (node, path = []) {
- const p = path,
- parent = node.parent;
-
- if (parent) {
- const children = parent.children || parent._children;
-
- p.unshift(children.indexOf(node));
-
- return JSV.getNodePath(parent, p);
- } else {
- return p;
- }
- },
-
- /**
- * Expand an index-based path for the node from the root.
- */
- expandNodePath: function (path) {
- let i,
- node = JSV.treeData; //start with root
-
- for (i = 0; i < path.length; i++) {
- if (node._children) {
- JSV.expand(node);
- }
-
- node = node.children[path[i]];
- }
-
- JSV.update(JSV.treeData);
- JSV.centerNode(node);
-
- return node;
- },
-
- /**
- * Build search result.
- *
- * @param {array} items The items to search
- * @param {string} val The search string
- */
- buildSearchList: function (items, val) {
- const ul = $('ul#search-result');
- const exp = new RegExp('^.*' + val + '.*\\|.+', 'i');
-
- $.each(items, function (i, v) {
- if (v.match(exp)) {
- const data = v.split('|');
- const li = $('
').attr('data-icon', 'false').appendTo(ul);
-
- $('
').attr('data-path', data[1]).text(data[0]).appendTo(li);
- }
- });
- },
-
- /**
- * Flash node text
- */
- flashNode: function (node, times = 4) {
- let t = times,
- text = $('#n-' + node.id + ' text');
-
- //flash node text
- while (t--) {
- text.fadeTo(350, 0).fadeTo(350, 1);
- }
- },
-
- /**
- * A recursive helper function for performing some setup by walking
- * through all nodes
- */
- visit: function (parent, visitFn, childrenFn) {
- if (!parent) {
- return;
- }
-
- visitFn(parent);
-
- const children = childrenFn(parent);
-
- if (children) {
- let count = children.length,
- i;
-
- for (i = 0; i < count; i++) {
- JSV.visit(children[i], visitFn, childrenFn);
- }
- }
- },
-
- /**
- * Create the tree data object from the schema(s)
- */
- compileData: function (schema, parent, name, real = false, depth = 0) {
- // Ensure healthy amount of recursion
- if (depth > this.maxDepth) {
- return;
- }
-
- let key,
- node,
- s = schema.$ref ? tv4.getSchema(schema.$ref) : schema,
- props = s.properties,
- items = s.items,
- owns = Object.prototype.hasOwnProperty,
- all: Record
= {},
- parentSchema = function (node) {
- const schema = node.id || node.$ref || node.schema;
-
- if (schema) {
- return schema;
- } else if (node.parentSchema) {
- return parentSchema(node.parentSchema);
- } else {
- return null;
- }
- };
-
- if (s.allOf) {
- all.allOf = s.allOf;
- }
-
- if (s.oneOf) {
- all.oneOf = s.oneOf;
- }
-
- if (s.anyOf) {
- all.anyOf = s.anyOf;
- }
-
- node = {
- description: schema.description || s.description,
- // name: (schema.$ref && real ? name : false) || s.title || name || 'schema',
- name: (schema.$ref && real ? name : false) || name || 'schema',
- isReal: real,
- plainName: name,
- type: s.type,
- displayType:
- s.type ||
- (s['enum'] ? 'enum: ' + s['enum'].join(', ') : s.items ? 'array' : s.properties ? 'object' : 'ambiguous'),
- translation: schema.translation || s.translation,
- example: schema.example || s.example,
- opacity: real ? 1 : 0.5,
- required: s.required,
- schema: s.id || schema.$ref || parentSchema(parent),
- parentSchema: parent,
- deprecated: schema.deprecated || s.deprecated,
- dependencies: s.dependencies,
- };
-
- node.require = parent && parent.required ? parent.required.indexOf(node.name) > -1 : false;
-
- if (parent) {
- if (node.name === 'item') {
- node.parent = parent;
-
- if (node.type) {
- node.name = node.type;
- parent.children.push(node);
- }
- } else if (parent.name === 'item') {
- parent.parent.children.push(node);
- } else {
- parent.children.push(node);
- }
- } else {
- JSV.treeData = node;
- }
-
- if (node.type === 'array') {
- node.name += '[' + (s.minItems || ' ') + ']';
- node.minItems = s.minItems;
- }
-
- if (node.type === 'object' && node.name !== 'item') {
- node.name += '{ }';
- }
-
- if (props || items || all) {
- node.children = [];
- }
-
- for (key in props) {
- if (!owns.call(props, key)) {
- continue;
- }
-
- JSV.compileData(props[key], node, key, true, depth + 1);
- }
-
- for (key in all) {
- if (!owns.call(all, key)) {
- continue;
- }
-
- if (!all[key]) {
- continue;
- }
-
- const allNode = {
- name: key,
- children: [],
- opacity: 0.5,
- parentSchema: parent,
- schema: schema.$ref || parentSchema(parent),
- };
-
- if (node.name === 'item') {
- node.parent.children.push(allNode);
- } else {
- node.children.push(allNode);
- }
-
- for (let i = 0; i < all[key].length; i++) {
- //JSV.compileData(all[key][i], allNode, s.title || all[key][i].type, false, depth + 1);
- JSV.compileData(all[key][i], allNode, all[key][i].type, false, depth + 1);
- }
- }
-
- if (Object.prototype.toString.call(items) === '[object Object]') {
- JSV.compileData(items, node, 'item', false, depth + 1);
- } else if (Object.prototype.toString.call(items) === '[object Array]') {
- items.forEach(function (itm, idx, arr) {
- JSV.compileData(itm, node, idx.toString(), false, depth + 1);
- });
- }
- },
-
- /**
- * Resize the diagram
- */
- resizeViewer: function () {
- JSV.viewerWidth = $('#main-body').width();
- JSV.viewerHeight = $('#main-body').height();
-
- if (JSV.focusNode) {
- JSV.centerNode(JSV.focusNode);
- }
- },
-
- /**
- * Reset the tree starting from the passed source.
- */
- resetTree: function (source, level) {
- JSV.visit(
- source,
- function (d) {
- if (d.children && d.children.length > 0 && d.depth > level && !JSV.labels[d.name]) {
- JSV.collapse(d);
- //d._children = d.children;
- //d.children = null;
- } else if (JSV.labels[d.name]) {
- JSV.expand(d);
- }
- },
- function (d) {
- if (d.children && d.children.length > 0) {
- return d.children;
- } else if (d._children && d._children.length > 0) {
- return d._children;
- } else {
- return null;
- }
- }
- );
- },
-
- /**
- * Reset and center the tree.
- */
- resetViewer: function () {
- //Firefox will choke if the viewer-page is not visible
- //TODO: fix on refactor to use pagecontainer event
- const page = $('#viewer-page');
-
- page.css('display', 'block');
-
- // Define the root
- const root = JSV.treeData;
-
- root.x0 = JSV.viewerHeight / 2;
- root.y0 = 0;
-
- // Layout the tree initially and center on the root node.
- // Call visit function to set initial depth
- JSV.tree.nodes(root);
- JSV.resetTree(root, 1);
- JSV.update(root);
-
- //reset the style for viewer-page
- page.css('display', '');
-
- JSV.centerNode(root, 4);
- },
-
- /**
- * Function to center node when clicked so node doesn't get lost when collapsing with large amount of children.
- */
- centerNode: function (source, ratioX = 2) {
- const zl = JSV.zoomListener,
- scale = zl.scale(),
- x = -source.y0 * scale + JSV.viewerWidth / ratioX,
- y = -source.x0 * scale + JSV.viewerHeight / 2;
-
- d3.select('g#node-group')
- .transition()
- .duration(JSV.duration)
- .attr('transform', 'translate(' + x + ',' + y + ')scale(' + scale + ')');
- zl.scale(scale);
- zl.translate([x, y]);
- },
-
- /**
- * Helper functions for collapsing nodes.
- */
- collapse: function (d) {
- if (d.children) {
- d._children = d.children;
- //d._children.forEach(collapse);
- d.children = null;
- }
- },
-
- /**
- * Helper functions for expanding nodes.
- */
- expand: function (d) {
- if (d._children) {
- d.children = d._children;
- //d.children.forEach(expand);
- d._children = null;
- }
-
- if (d.children) {
- let count = d.children.length,
- i;
-
- for (i = 0; i < count; i++) {
- if (JSV.labels[d.children[i].name]) {
- JSV.expand(d.children[i]);
- }
- }
- }
- },
-
- /**
- * Toggle children function
- */
- toggleChildren: function (d) {
- if (d.children) {
- JSV.collapse(d);
- } else if (d._children) {
- JSV.expand(d);
- }
-
- return d;
- },
-
- /**
- * Toggle children on node click.
- */
- click: function (d) {
- if (!JSV.labels[d.name]) {
- if ((d3.event as Event) && (d3.event as Event).defaultPrevented) {
- return;
- } // click suppressed
-
- d = JSV.toggleChildren(d);
- JSV.update(d);
- JSV.centerNode(d);
- }
- },
-
- /**
- * Show info on node title click.
- */
- clickTitle: function (d) {
- if (!JSV.labels[d.name]) {
- if ((d3.event as Event) && (d3.event as Event).defaultPrevented) {
- return;
- } // click suppressed
-
- const panel = $('#info-panel');
-
- if (JSV.focusNode) {
- d3.select('#n-' + JSV.focusNode.id).classed('focus', false);
- }
-
- JSV.focusNode = d;
- JSV.centerNode(d);
- d3.select('#n-' + d.id).classed('focus', true);
-
- if (!JSV.plain) {
- JSV.setPermalink(d);
-
- $('#info-title')
- .text('Info: ' + d.name)
- .toggleClass('deprecated', !!d.deprecated);
- JSV.setInfo(d);
- panel.panel('open');
- }
- }
- },
-
- /**
- * Zoom the tree
- */
- zoom: function () {
- JSV.svgGroup.attr(
- 'transform',
- 'translate(' + JSV.zoomListener.translate() + ')' + 'scale(' + JSV.zoomListener.scale() + ')'
- );
- },
-
- /**
- * Perform the d3 zoom based on position and scale
- */
- interpolateZoom: function (translate, scale) {
- return d3
- .transition()
- .duration(350)
- .tween('zoom', function () {
- const iTranslate = d3.interpolate(JSV.zoomListener.translate(), translate),
- iScale = d3.interpolate(JSV.zoomListener.scale(), scale);
-
- return function (t) {
- JSV.zoomListener.scale(iScale(t)).translate(iTranslate(t));
- JSV.zoom();
- };
- });
- },
-
- /**
- * Click handler for the zoom control
- */
- zoomClick: function () {
- let clicked = (d3.event as Event).target,
- direction = 1,
- factor = 0.2,
- target_zoom = 1,
- center = [JSV.viewerWidth / 2, JSV.viewerHeight / 2],
- zl = JSV.zoomListener,
- extent = zl.scaleExtent(),
- translate = zl.translate(),
- translate0 = [],
- l = [],
- view = { x: translate[0], y: translate[1], k: zl.scale() };
-
- (d3.event as Event).preventDefault();
- direction = this.id === 'zoom_in' ? 1 : -1;
- target_zoom = zl.scale() * (1 + factor * direction);
-
- if (target_zoom < extent[0] || target_zoom > extent[1]) {
- return false;
- }
-
- translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
- view.k = target_zoom;
- l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
-
- view.x += center[0] - l[0];
- view.y += center[1] - l[1];
-
- JSV.interpolateZoom([view.x, view.y], view.k);
- },
-
- /**
- * The zoomListener which calls the zoom function on the 'zoom' event constrained within the scaleExtents
- */
- zoomListener: null,
-
- /**
- * Sort the tree according to the node names
- */
- sortTree: function (tree) {
- tree.sort(function (a, b) {
- return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
- });
- },
-
- /**
- * The d3 diagonal projection for use by the node paths.
- */
- diagonal1: function (d) {
- let src = d.source,
- node = d3.select('#n-' + src.id)[0][0],
- dia,
- width = 0;
-
- if (node) {
- width = (node as any).getBBox().width;
- }
-
- dia =
- 'M' +
- (src.y + width) +
- ',' +
- src.x +
- 'H' +
- (d.target.y - 30) +
- 'V' +
- d.target.x +
- //+ (d.target.children ? '' : 'h' + 30);
- ('h' + 30);
-
- return dia;
- },
-
- /**
- * Update the tree, removing or adding nodes from/to the passed source node
- */
- update: function (source) {
- const duration = JSV.duration;
- const root = JSV.treeData;
- // Compute the new height, function counts total children of root node and sets tree height accordingly.
- // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
- // This makes the layout more consistent.
- const levelWidth = [1];
-
- const childCount = function (level, n) {
- if (n.children && n.children.length > 0) {
- if (levelWidth.length <= level + 1) {
- levelWidth.push(0);
- }
-
- levelWidth[level + 1] += n.children.length;
- n.children.forEach(function (d) {
- childCount(level + 1, d);
- });
- }
- };
-
- childCount(0, root);
- const newHeight = d3.max(levelWidth) * 45; // 25 pixels per line
-
- JSV.tree.size([newHeight, JSV.viewerWidth]);
-
- // Compute the new tree layout.
- const nodes = JSV.tree.nodes(root),
- links = JSV.tree.links(nodes);
-
- // Call JSV.visit function to establish maxLabelLength
- JSV.visit(
- JSV.treeData,
- function (d) {
- JSV.maxLabelLength[d.depth] = Math.max(
- d.name.length,
- JSV.maxLabelLength[d.depth] ? JSV.maxLabelLength[d.depth] : 0
- );
- },
- function (d) {
- return d.children && d.children.length > 0 ? d.children : null;
- }
- );
-
- // Set widths between levels based on maxLabelLength.
- nodes.forEach(function (d) {
- d.y = d.parent ? d.parent.y + JSV.maxLabelLength[d.parent.depth] * 8 + 100 : 0;
- // alternatively to keep a fixed scale one can set a fixed depth per level
- // Normalize for fixed-depth by commenting out below line
- // d.y = (d.depth * 500); //500px per level.
- });
- // Update the nodes…
- const node = JSV.svgGroup.selectAll('g.node').data(nodes, function (d) {
- return d.id || (d.id = ++JSV.counter);
- });
-
- // Enter any new nodes at the parent's previous position.
- const nodeEnter = node
- .enter()
- .append('g')
- .attr('class', function (d) {
- return JSV.labels[d.name] ? 'node label' : 'node';
- })
- .classed('deprecated', function (d) {
- return d.deprecated;
- })
- .attr('id', function (d, i) {
- return 'n-' + d.id;
- })
- .attr('transform', function (d) {
- return 'translate(' + source.y0 + ',' + source.x0 + ')';
- });
-
- nodeEnter
- .append('circle')
- //.attr('class', 'nodeCircle')
- .attr('r', 0)
- .classed('collapsed', function (d) {
- return d._children ? true : false;
- })
- .on('click', JSV.click);
-
- nodeEnter
- .append('text')
- .attr('x', function (d) {
- return 10;
- // return d.children || d._children ? -10 : 10;
- })
- .attr('dy', '.35em')
- .attr('class', function (d) {
- return d.children || d._children ? 'node-text node-branch' : 'node-text';
- })
- .classed('abstract', function (d) {
- return d.opacity < 1;
- })
- .attr('text-anchor', function (d) {
- //return d.children || d._children ? 'end' : 'start';
- return 'start';
- })
- .text(function (d) {
- return d.name + (d.require ? '*' : '');
- })
- .style('fill-opacity', 0)
- .on('click', JSV.clickTitle)
- .on('dblclick', function (d) {
- JSV.click(d);
- JSV.clickTitle(d);
- (d3.event as Event).stopPropagation();
- });
-
- // Change the circle fill depending on whether it has children and is collapsed
- node
- .select('.node circle')
- .attr('r', 6.5)
- .classed('collapsed', function (d) {
- return d._children ? true : false;
- });
-
- // Transition nodes to their new position.
- const nodeUpdate = node
- .transition()
- .duration(duration)
- .attr('transform', function (d) {
- return 'translate(' + d.y + ',' + d.x + ')';
- });
-
- // Fade the text in
- nodeUpdate.select('text').style('fill-opacity', function (d) {
- return d.opacity || 1;
- });
-
- // Transition exiting nodes to the parent's new position.
- const nodeExit = node
- .exit()
- .transition()
- .duration(duration)
- .attr('transform', function (d) {
- return 'translate(' + source.y + ',' + source.x + ')';
- })
- .remove();
-
- nodeExit.select('circle').attr('r', 0);
-
- nodeExit.select('text').style('fill-opacity', 0);
-
- // Update the links…
- const link = JSV.svgGroup.selectAll('path.link').data(links, function (d) {
- return d.target.id;
- });
-
- // Enter any new links at the parent's previous position.
- link
- .enter()
- .insert('path', 'g')
- .attr('class', 'link')
- .attr('d', function (d) {
- const o = {
- x: source.x0,
- y: source.y0,
- };
-
- //console.info(d3.select('#n-'+d.source.id)[0][0].getBBox());
-
- return JSV.diagonal1({
- source: o,
- target: o,
- });
- });
-
- // Transition links to their new position.
- link.transition().duration(duration).attr('d', JSV.diagonal1);
-
- // Transition exiting nodes to the parent's new position.
- link
- .exit()
- .transition()
- .duration(duration)
- .attr('d', function (d) {
- const o = {
- x: source.x,
- y: source.y,
- };
-
- return JSV.diagonal1({
- source: o,
- target: o,
- });
- })
- .remove();
-
- // Stash the old positions for transition.
- nodes.forEach(function (d) {
- d.x0 = d.x;
- d.y0 = d.y;
- });
- },
-
- /**
- * Create the d3 diagram.
- *
- * @param {function} callback Function to run after the diagram is created
- */
- createDiagram: function (callback) {
- tv4.addSchema(JSV.schemaUri, JSV.schema);
- JSV.compileData(tv4.getSchema(JSV.schemaUri), false, 'schema');
-
- // Calculate total nodes, max label length
- // panning variables
- //var panSpeed = 200;
- //var panBoundary = 20; // Within 20px from edges will pan when dragging.
-
- // size of the diagram
- const viewerWidth = JSV.viewerWidth;
- const viewerHeight = JSV.viewerHeight;
-
- JSV.zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on('zoom', JSV.zoom);
-
- JSV.baseSvg = d3
- .select('#main-body')
- .append('svg')
- .attr('id', 'jsv-tree')
- .attr('class', 'overlay')
- .attr('width', viewerWidth)
- .attr('height', viewerHeight)
- .call(JSV.zoomListener);
-
- JSV.tree = d3.layout.tree().size([viewerHeight, viewerWidth]);
-
- // Sort the tree initially in case the JSON isn't in a sorted order.
- //JSV.sortTree();
-
- JSV.svgGroup = JSV.baseSvg.append('g').attr('id', 'node-group');
-
- // Layout the tree initially and center on the root node.
- JSV.resetViewer();
-
- JSV.centerNode(JSV.treeData, 4);
-
- // define the legend svg, attaching a class for styling
- const legendData = [
- {
- text: 'Expanded',
- y: 20,
- },
- {
- text: 'Collapsed',
- iconCls: 'collapsed',
- y: 40,
- },
- {
- text: 'Selected',
- itemCls: 'focus',
- y: 60,
- },
- {
- text: 'Required*',
- y: 80,
- },
- {
- text: 'Object{ }',
- iconCls: 'collapsed',
- y: 100,
- },
- {
- text: 'Array[minimum #]',
- iconCls: 'collapsed',
- y: 120,
- },
- {
- text: 'Abstract Property',
- itemCls: 'abstract',
- y: 140,
- opacity: 0.5,
- },
- {
- text: 'Deprecated',
- itemCls: 'deprecated',
- y: 160,
- },
- ];
-
- const legendSvg = d3.select('#legend-items').append('svg').attr('width', 170).attr('height', 180);
-
- // Update the nodes…
- const legendItem = legendSvg
- .selectAll('g.item-group')
- .data(legendData)
- .enter()
- .append('g')
- .attr('class', function (d) {
- let cls = 'item-group ';
-
- cls += d.itemCls || '';
-
- return cls;
- })
- .attr('transform', function (d) {
- return 'translate(10, ' + d.y + ')';
- });
-
- legendItem
- .append('circle')
- .attr('r', 6.5)
- .attr('class', function (d) {
- return d.iconCls;
- });
-
- legendItem
- .append('text')
- .attr('x', 15)
- .attr('dy', '.35em')
- .attr('class', 'item-text')
- .attr('text-anchor', 'start')
- .style('fill-opacity', function (d) {
- return d.opacity || 1;
- })
- .text(function (d) {
- return d.text;
- });
-
- if (typeof callback === 'function') {
- callback();
- }
- },
-};
diff --git a/packages-adapters/json-schema-viewer/tsconfig.json b/packages-adapters/json-schema-viewer/tsconfig.json
deleted file mode 100644
index 63b5082..0000000
--- a/packages-adapters/json-schema-viewer/tsconfig.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": "../../tsconfig.json"
-}
diff --git a/packages-adapters/monaco-editor/package.json b/packages-adapters/monaco-editor/package.json
index 226b82a..1d19343 100644
--- a/packages-adapters/monaco-editor/package.json
+++ b/packages-adapters/monaco-editor/package.json
@@ -1,7 +1,7 @@
{
"name": "@ekzo-dev/monaco-editor",
"description": "Aurelia Monaco Editor adapter",
- "version": "0.55.1",
+ "version": "0.55.2",
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/monaco-editor",
"repository": {
"type": "git",
@@ -9,6 +9,7 @@
},
"license": "MIT",
"dependencies": {
+ "@ekzo-dev/toolkit": "^1.3.0",
"monaco-editor": "~0.55.1"
},
"peerDependencies": {
diff --git a/packages-adapters/monaco-editor/src/elements/monaco-editor-element.ts b/packages-adapters/monaco-editor/src/elements/monaco-editor-element.ts
index bb5ed46..6bf7572 100644
--- a/packages-adapters/monaco-editor/src/elements/monaco-editor-element.ts
+++ b/packages-adapters/monaco-editor/src/elements/monaco-editor-element.ts
@@ -4,10 +4,9 @@ import './monaco-editor.css';
import type { editor } from 'monaco-editor';
+import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, BindingMode, customElement, ICustomElementViewModel, resolve } from 'aurelia';
-import { coerceBoolean } from '../utils';
-
export type EditorModule = typeof import('monaco-editor');
@customElement({
@@ -16,7 +15,7 @@ export type EditorModule = typeof import('monaco-editor');
})
export class MonacoEditor implements ICustomElementViewModel, editor.IStandaloneEditorConstructionOptions {
@bindable({ mode: BindingMode.twoWay })
- value: string;
+ value?: string;
@bindable()
language?: string;
@@ -28,11 +27,11 @@ export class MonacoEditor implements ICustomElementViewModel, editor.IStandalone
loading: boolean = true;
- #editorInstance: editor.IStandaloneCodeEditor;
+ #editorInstance?: editor.IStandaloneCodeEditor;
#valueCache?: string;
- #editor: typeof editor;
+ #editor?: typeof editor;
attached() {
void this.#createEditor();
@@ -52,7 +51,7 @@ export class MonacoEditor implements ICustomElementViewModel, editor.IStandalone
break;
case 'language':
- this.#editor.setModelLanguage(this.#editorInstance.getModel(), value as string);
+ this.#editor?.setModelLanguage(this.#editorInstance!.getModel()!, value as string);
break;
}
@@ -71,7 +70,7 @@ export class MonacoEditor implements ICustomElementViewModel, editor.IStandalone
readOnly: this.readOnly,
});
this.#editorInstance.onDidChangeModelContent(() => {
- const val = this.#editorInstance.getValue();
+ const val = this.#editorInstance?.getValue();
this.#valueCache = val;
this.value = val;
diff --git a/packages-adapters/monaco-editor/src/utils.ts b/packages-adapters/monaco-editor/src/utils.ts
deleted file mode 100644
index ce546a5..0000000
--- a/packages-adapters/monaco-editor/src/utils.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const coerceBoolean = {
- set: (v: string | boolean) => (v === '' || v === true ? true : v === 'false' || v === false ? false : undefined),
-};
diff --git a/packages-adapters/sortable/package.json b/packages-adapters/sortable/package.json
index b97c53b..8618e30 100644
--- a/packages-adapters/sortable/package.json
+++ b/packages-adapters/sortable/package.json
@@ -1,7 +1,7 @@
{
"name": "@ekzo-dev/sortable",
"description": "Aurelia Sortable library adapter",
- "version": "1.15.0",
+ "version": "1.15.1",
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/vanilla-json-editor",
"repository": {
"type": "git",
diff --git a/packages-adapters/sortable/src/attributes/sortable.ts b/packages-adapters/sortable/src/attributes/sortable.ts
index f716556..6367044 100644
--- a/packages-adapters/sortable/src/attributes/sortable.ts
+++ b/packages-adapters/sortable/src/attributes/sortable.ts
@@ -1,6 +1,5 @@
import type { GroupOptions, Options } from 'sortablejs';
-import { ICustomAttributeController } from '@aurelia/runtime-html';
import { bindable, customAttribute, ICustomAttributeViewModel, resolve } from 'aurelia';
import SortableLib from 'sortablejs';
@@ -81,8 +80,6 @@ export class Sortable implements ICustomAttributeViewModel, Options {
readonly host = resolve(HTMLElement);
- readonly $controller!: ICustomAttributeController;
-
attached() {
this.#createSortable();
}
@@ -92,15 +89,16 @@ export class Sortable implements ICustomAttributeViewModel, Options {
}
propertyChanged(name: keyof Options, value: Options[keyof Options]): void {
- this.sortable.option(name, value);
+ this.sortable?.option(name, value);
}
#createSortable() {
// prepare options from bindables
const options: Options = {};
- Object.keys(this.$controller.definition.bindables).forEach((name) => {
+ Object.keys((this as ICustomAttributeViewModel).$controller!.definition.bindables).forEach((name) => {
if (this[name as keyof this] !== undefined) {
+ // @ts-ignore
options[name] = this[name as keyof this];
}
});
diff --git a/packages-adapters/tinymce/src/elements/tinymce-editor.stories.ts b/packages-adapters/tinymce/src/elements/tinymce-editor.stories.ts
index 55a7274..29941b8 100644
--- a/packages-adapters/tinymce/src/elements/tinymce-editor.stories.ts
+++ b/packages-adapters/tinymce/src/elements/tinymce-editor.stories.ts
@@ -1,10 +1,10 @@
-import { Meta } from '@storybook/aurelia';
-
-import { TinymceEditor } from './tinymce-editor';
-
-export default {
- title: 'TinyMCE / Editor',
- component: TinymceEditor,
-} as Meta;
-
-export { Default } from '../../../../.storybook/helpers';
+// import { Meta } from '@storybook/aurelia';
+//
+// import { TinymceEditor } from './tinymce-editor';
+//
+// export default {
+// title: 'TinyMCE / Editor',
+// component: TinymceEditor,
+// } as Meta;
+//
+// export { Default } from '../../../../.storybook/helpers';
diff --git a/packages-adapters/tinymce/src/elements/tinymce-editor.ts b/packages-adapters/tinymce/src/elements/tinymce-editor.ts
index b6a26f8..c0e746d 100644
--- a/packages-adapters/tinymce/src/elements/tinymce-editor.ts
+++ b/packages-adapters/tinymce/src/elements/tinymce-editor.ts
@@ -122,7 +122,7 @@ export class TinymceEditor {
}
private destroyEditor() {
- tinymce.remove(this.editor);
+ tinymce.remove(this.editor!);
this.editor = undefined;
}
diff --git a/packages-adapters/tinymce/tsconfig.json b/packages-adapters/tinymce/tsconfig.json
index 63b5082..4082f16 100644
--- a/packages-adapters/tinymce/tsconfig.json
+++ b/packages-adapters/tinymce/tsconfig.json
@@ -1,3 +1,3 @@
{
- "extends": "../../tsconfig.json"
+ "extends": "../../tsconfig.json"
}
diff --git a/packages-adapters/vanilla-jsoneditor/package.json b/packages-adapters/vanilla-jsoneditor/package.json
index 1dc8ad0..8d2c35e 100644
--- a/packages-adapters/vanilla-jsoneditor/package.json
+++ b/packages-adapters/vanilla-jsoneditor/package.json
@@ -1,7 +1,7 @@
{
"name": "@ekzo-dev/vanilla-jsoneditor",
"description": "Aurelia JSON Editor adapter",
- "version": "3.10.0",
+ "version": "3.11.1",
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/vanilla-json-editor",
"repository": {
"type": "git",
@@ -9,9 +9,10 @@
},
"license": "MIT",
"dependencies": {
+ "@ekzo-dev/toolkit": "^1.3.0",
"@types/json-schema": "^7.0.14",
"immutable-json-patch": "^6.0.1",
- "vanilla-jsoneditor": "~3.10.0"
+ "vanilla-jsoneditor": "~3.11.0"
},
"peerDependencies": {
"aurelia": "^2.0.0-rc.0"
diff --git a/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.stories.ts b/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.stories.ts
index 1b80ecf..c1c08e6 100644
--- a/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.stories.ts
+++ b/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.stories.ts
@@ -1,24 +1,30 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../.storybook/helpers';
-
-import { JsonEditor } from './json-editor';
-
-const meta: Meta = {
- title: 'VanillaJsoneditor / JsonEditor',
- component: JsonEditor,
- argTypes: {
- theme: selectControl(['default', 'dark']),
- mode: selectControl(['tree', 'text', 'table']),
- },
-};
-
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: {
- ...args,
- onRenderValue: undefined, // must be reset here, otherwise editor breaks because callback required a return value
- },
-});
-
-export default meta;
-export { Overview };
+// import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
+//
+// import { selectControl } from '../../../../.storybook/helpers';
+//
+// import { JsonEditor } from './json-editor';
+//
+// const meta: Meta = {
+// title: 'VanillaJsoneditor / JsonEditor',
+// component: JsonEditor,
+// argTypes: {
+// theme: {
+// control: 'select',
+// options: ['default', 'dark'],
+// },
+// mode: {
+// control: 'select',
+// options: ['tree', 'text', 'table'],
+// },
+// },
+// };
+//
+// const Overview: Story = (args): StoryFnAureliaReturnType => ({
+// props: {
+// ...args,
+// onRenderValue: undefined, // must be reset here, otherwise editor breaks because callback required a return value
+// },
+// });
+//
+// export default meta;
+// export { Overview };
diff --git a/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.ts b/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.ts
index 8be696e..ce11443 100644
--- a/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.ts
+++ b/packages-adapters/vanilla-jsoneditor/src/elements/json-editor.ts
@@ -23,11 +23,9 @@ import type {
Validator,
} from 'vanilla-jsoneditor';
-import { ICustomElementController } from '@aurelia/runtime-html';
+import { coerceBoolean } from '@ekzo-dev/toolkit';
import { bindable, BindingMode, customElement, ICustomElementViewModel, resolve } from 'aurelia';
-import { coerceBoolean } from '../utils';
-
@customElement({
name: 'json-editor',
template,
@@ -37,7 +35,7 @@ export class JsonEditor implements ICustomElementViewModel, Omit RenderValueComponentDescription[];
@bindable()
- onSelect?: (selection: JSONEditorSelection | null) => void;
+ onSelect?: (selection: JSONEditorSelection | undefined) => void;
@bindable()
onRenderMenu?: (items: MenuItem[], context: RenderMenuContext) => MenuItem[] | undefined;
@@ -127,13 +125,11 @@ export class JsonEditor implements ICustomElementViewModel, Omit;
-
protected readonly host = resolve(HTMLElement);
#contentCache?: Content;
- get(): Content {
+ get(): Content | undefined {
return this.editor?.get();
}
@@ -141,7 +137,7 @@ export class JsonEditor implements ICustomElementViewModel, Omit {
+ scrollTo(path: JSONPath): Promise | undefined {
return this.editor?.scrollTo(path);
}
@@ -161,11 +157,11 @@ export class JsonEditor implements ICustomElementViewModel, Omit {
+ refresh(): Promise | undefined {
return this.editor?.refresh();
}
@@ -207,9 +203,9 @@ export class JsonEditor implements ICustomElementViewModel, Omit = {};
- Object.keys(this.$controller.definition.bindables).forEach((name) => {
- if (this[name] !== undefined) {
- props[name] = this[name];
+ Object.keys((this as ICustomElementViewModel).$controller!.definition.bindables).forEach((name) => {
+ if (this[name as keyof this] !== undefined) {
+ props[name] = this[name as keyof this];
}
});
diff --git a/packages-adapters/vanilla-jsoneditor/src/utils.ts b/packages-adapters/vanilla-jsoneditor/src/utils.ts
deleted file mode 100644
index ce546a5..0000000
--- a/packages-adapters/vanilla-jsoneditor/src/utils.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const coerceBoolean = {
- set: (v: string | boolean) => (v === '' || v === true ? true : v === 'false' || v === false ? false : undefined),
-};
diff --git a/packages-adapters/vanilla-jsoneditor/tsconfig.json b/packages-adapters/vanilla-jsoneditor/tsconfig.json
index 63b5082..4082f16 100644
--- a/packages-adapters/vanilla-jsoneditor/tsconfig.json
+++ b/packages-adapters/vanilla-jsoneditor/tsconfig.json
@@ -1,3 +1,3 @@
{
- "extends": "../../tsconfig.json"
+ "extends": "../../tsconfig.json"
}
diff --git a/packages/bootstrap-addons/README.md b/packages/bootstrap-addons/README.md
index b0f1891..465b631 100644
--- a/packages/bootstrap-addons/README.md
+++ b/packages/bootstrap-addons/README.md
@@ -1,181 +1,109 @@
-# `ui-utils`
+# @ekzo-dev/bootstrap-addons
-This project is bootstrapped by [aurelia-cli](https://github.com/aurelia/cli).
+Additional Bootstrap form components for Aurelia 2 applications.
-This Aurelia plugin project has a built-in dev app (with CLI built-in bundler and RequireJS) to simplify development.
+This package extends [@ekzo-dev/bootstrap](https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/bootstrap) with advanced form components that provide enhanced functionality beyond the standard Bootstrap form controls.
-1. The local `src/` folder, is the source code for the plugin.
-2. The local `dev-app/` folder, is the code for the dev app, just like a normal app bootstrapped by aurelia-cli.
-3. You can use normal `au run` and `au test` in development just like developing an app.
-4. You can use aurelia-testing to test your plugin, just like developing an app.
-5. To ensure compatibility to other apps, always use `PLATFORM.moduleName()` wrapper in files inside `src/`. You don't need to use the wrapper in `dev-app/` folder as CLI built-in bundler supports module name without the wrapper.
+## Installation
-Note aurelia-cli doesn't provide a plugin skeleton with Webpack setup (not yet), but this plugin can be consumed by any app using Webpack, or CLI built-in bundler, or jspm.
-
-## How to write an Aurelia plugin
-
-For a full length tutorial, visit [Aurelia plugin guide](https://aurelia.io/docs/plugins/write-new-plugin).
-
-Here is some basics. You can create new custom element, custom attribute, value converter or binding behavior manually, or use command `au generate` to help.
-
-```shell
-au generate element some-name
-au generate attribute some-name
-au generate value-converter some-name
-au generate binding-behavior some-name
-```
-
-By default, the cli generates command generates files in following folders:
-
-```
-src/elements
-src/attributes
-src/value-converters
-src/binding-behaviors
+```bash
+npm install @ekzo-dev/bootstrap-addons
```
-Note the folder structure is only to help you organising the files, it's not a requirement of Aurelia. You can manually create new element (or other thing) anywhere in `src/`.
+### Peer Dependencies
-After you added some new file, you need to register it in `src/index.ts`. Like this:
-
-```js
-config.globalResources([
- // ...
- PLATFORM.moduleName('./path/to/new-file-without-ext'),
-]);
-```
+This package requires the following peer dependencies:
-The usage of `PLATFORM.moduleName` wrapper is mandatory. It's needed for your plugin to be consumed by any app using webpack, CLI built-in bundler, or jspm.
+- `aurelia` ^2.0.0
+- `bootstrap` ~5.3.7
+- `@popperjs/core` ^2.11.8
+- `vanilla-jsoneditor` ~3.11.0
+- `immutable-json-patch` ^6.0.1
-## Resource import within the dev app
+## Usage
-In dev app, when you need to import something from the inner plugin (for example, importing a class for dependency injection), use special name `"resources"` to reference the inner plugin.
+Register the plugin in your Aurelia application:
-```js
-import {autoinject} from 'aurelia-framework';
-// "resources" refers the inner plugin src/index.ts
-import {MyService} from 'resources';
+```typescript
+import Aurelia from 'aurelia';
+import { BootstrapAddonsConfiguration } from '@ekzo-dev/bootstrap-addons';
-@autoinject()
-export class App {
- constructor(myService: MyService) {}
-}
+Aurelia
+ .register(BootstrapAddonsConfiguration)
+ .app(MyApp)
+ .start();
```
-## Manage dependencies
-
-By default, this plugin has no "dependencies" in package.json. Theoretically this plugin depends on at least `aurelia-pal` because `src/index.ts` imports it. It could also depends on more core Aurelia package like `aurelia-binding` or `aurelia-templating` if you build advanced components that reference them.
-
-Ideally you need to carefully add those `aurelia-pal` (`aurelia-binding`...) to "dependencies" in package.json. But in practice you don't have to. Because every app that consumes this plugin will have full Aurelia core packages installed.
-
-Furthermore, there are two benefits by leaving those dependencies out of plugin's package.json.
-
-1. ensure this plugin doesn't bring in a duplicated Aurelia core package to consumers' app. This is mainly for app built with webpack. We had been hit with `aurelia-binding` v1 and v2 conflicts due to 3rd party plugin asks for `aurelia-binding` v1.
-2. reduce the burden for npm/yarn when installing this plugin.
-
-If you are a perfectionist who could not stand leaving out dependencies, I recommend you to add `aurelia-pal` (`aurelia-binding`...) to "peerDependencies" in package.json. So at least it could not cause a duplicated Aurelia core package.
-
-If your plugin depends on other npm package, like `lodash` or `jquery`, **you have to add them to "dependencies" in package.json**.
-
-## Build Plugin
-
-Run `au build-plugin`. This will transpile all files from `src/` folder to `dist/native-modules/` and `dist/commonjs/`.
-
-For example, `src/index.ts` will become `dist/native-modules/index.js` and `dist/commonjs/index.js`.
-
-Note all other files in `dev-app/` folder are for the dev app, they would not appear in the published npm package.
+## Components
-## Consume Plugin
+| Component | Description | Documentation |
+|-----------|-------------|---------------|
+| **Duration Input** | Form control for entering time durations in ISO 8601 format | [View docs](./src/forms/duration-input/README.md) |
+| **JSON Input** | Powerful JSON editor with schema validation support | [View docs](./src/forms/json-input/README.md) |
+| **Select Dropdown** | Enhanced select component with improved styling and functionality | [View docs](./src/forms/select-dropdown/README.md) |
-By default, the `dist/` folder is not committed to git. (We have `/dist` in `.gitignore`). But that would not prevent you from consuming this plugin through direct git reference.
+## Quick Examples
-You can consume this plugin directly by:
+### Duration Input
-```shell
-npm i github:your_github_username/ui-utils
-# or if you use bitbucket
-npm i bitbucket:your_github_username/ui-utils
-# or if you use gitlab
-npm i gitlab:your_github_username/ui-utils
-# or plain url
-npm i https:/github.com/your_github_username/ui-utils.git
+```html
+
```
-Then load the plugin in app's `main.ts` like this.
+### JSON Input
-```js
-aurelia.use.plugin('ui-utils');
-// for webpack user, use PLATFORM.moduleName wrapper
-aurelia.use.plugin(PLATFORM.moduleName('ui-utils'));
+```html
+
```
-The missing `dist/` files will be filled up by npm through `"prepare": "npm run build"` (in `"scripts"` section of package.json).
+### Select Dropdown
-Yarn has a [bug](https://github.com/yarnpkg/yarn/issues/5235) that ignores `"prepare"` script. If you want to use yarn to consume your plugin through direct git reference, remove `/dist` from `.gitignore` and commit all the files. Note you don't need to commit `dist/` files if you only use yarn to consume this plugin through published npm package (`npm i ui-utils`).
-
-## Publish npm package
-
-By default, `"private"` field in package.json has been turned on, this prevents you from accidentally publish a private plugin to npm.
-
-To publish the plugin to npm for public consumption:
-
-1. Remove `"private": true,` from package.json.
-2. Pump up project version. This will run through `au test` (in "preversion" in package.json) first.
-
-```shell
-npm version patch # or minor or major
+```html
+
```
-3. Push up changes to your git server
-
-```shell
-git push && git push --tags
-```
-
-4. Then publish to npm, you need to have your npm account logged in.
-
-```shell
-npm publish
-```
-
-## Automate changelog, git push, and npm publish
-
-You can enable `npm version patch # or minor or major` to automatically update changelog, push commits and version tag to the git server, and publish to npm.
-
-Here is one simple setup.
-
-1. `npm i -D standard-changelog`. We use [`standard-changelog`](https://github.com/conventional-changelog/conventional-changelog) as a minimum example to support conventional changelog.
-
-- Alternatively you can use high level [standard-version](https://github.com/conventional-changelog/standard-version).
-
-2. Add two commands to `"scripts"` section of package.json.
-
-```
-"scripts": {
- // ...
- "version": "standard-changelog && git add CHANGELOG.md",
- "postversion": "git push && git push --tags && npm publish"
-},
-```
+## Dependencies
-3. you can remove `&& npm publish` if your project is private
+This package uses:
-For more information, go to https://aurelia.io/docs/cli/cli-bundler
+- **@ekzo-dev/bootstrap** - Core Bootstrap components for Aurelia 2
+- **@ekzo-dev/vanilla-jsoneditor** - Aurelia 2 adapter for vanilla-jsoneditor
+- **@ekzo-dev/toolkit** - Utility functions and helpers
+- **vanilla-jsoneditor** - JSON editor component
+- **json-schema-library** - JSON Schema validation
+- **ajv** - Another JSON Schema validator
+- **@js-temporal/polyfill** - Temporal API polyfill for date/time handling
-## Run dev app
+## Browser Support
-Run `au run`, then open `http://localhost:9000`
+This package supports all modern browsers that support ES2015+ and the following features:
-To open browser automatically, do `au run --open`.
+- Custom Elements (Web Components)
+- ES Modules
+- Temporal API (polyfilled)
-To change dev server port, do `au run --port 8888`.
+## Contributing
-To change dev server host, do `au run --host 127.0.0.1`
+Contributions are welcome! Please read the [contributing guidelines](https://github.com/ekzo-dev/aurelia-components/blob/main/CONTRIBUTING.md) first.
-**PS:** You could mix all the flags as well, `au run --host 127.0.0.1 --port 7070 --open`
+## License
-## Unit tests
+MIT © [Ekzo](https://github.com/ekzo-dev)
-Run `au test` (or `au jest`).
+## Links
-To run in watch mode, `au test --watch` or `au jest --watch`.
+- [GitHub Repository](https://github.com/ekzo-dev/aurelia-components)
+- [Issue Tracker](https://github.com/ekzo-dev/aurelia-components/issues)
+- [Aurelia 2 Documentation](https://docs.aurelia.io)
+- [Bootstrap 5 Documentation](https://getbootstrap.com/docs/5.3/)
diff --git a/packages/bootstrap-addons/package.json b/packages/bootstrap-addons/package.json
index 4ff2fdb..47c3333 100644
--- a/packages/bootstrap-addons/package.json
+++ b/packages/bootstrap-addons/package.json
@@ -1,7 +1,7 @@
{
"name": "@ekzo-dev/bootstrap-addons",
"description": "Aurelia Bootstrap additional component",
- "version": "5.3.11",
+ "version": "5.3.21",
"homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons",
"repository": {
"type": "git",
@@ -9,21 +9,21 @@
},
"license": "MIT",
"dependencies": {
- "@ekzo-dev/bootstrap": "~5.3.0",
- "@ekzo-dev/vanilla-jsoneditor": "^3.10.0",
- "@ekzo-dev/toolkit": "^1.2.4",
- "@fortawesome/free-solid-svg-icons": "^6.5.2",
+ "@ekzo-dev/bootstrap": "~5.3.6",
+ "@ekzo-dev/vanilla-jsoneditor": "~3.11.0",
+ "@ekzo-dev/toolkit": "^1.3.0",
+ "@fortawesome/free-solid-svg-icons": "^7.0.1",
"@types/json-schema": "^7.0.14",
"@js-temporal/polyfill": "^0.5.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "json-schema-library": "^10.5.1"
+ "json-schema-library": "^11.0.0"
},
"peerDependencies": {
"aurelia": "^2.0.0-rc.0",
"bootstrap": "~5.3.7",
"@popperjs/core": "^2.11.8",
- "vanilla-jsoneditor": "~3.7.0",
+ "vanilla-jsoneditor": "~3.11.0",
"immutable-json-patch": "^6.0.1"
},
"main": "src/index.ts",
diff --git a/packages/bootstrap-addons/src/forms/duration-input/README.md b/packages/bootstrap-addons/src/forms/duration-input/README.md
new file mode 100644
index 0000000..5bf7b49
--- /dev/null
+++ b/packages/bootstrap-addons/src/forms/duration-input/README.md
@@ -0,0 +1,153 @@
+# Duration Input
+
+A form control for entering time durations in ISO 8601 format.
+
+## Overview
+
+The Duration Input component allows users to input time durations using separate fields for days, hours, minutes, and seconds. The component automatically converts the input to ISO 8601 duration format (e.g., `P5DT1H` for 5 days and 1 hour).
+
+## Features
+
+- ISO 8601 duration format support
+- Separate inputs for days, hours, minutes, seconds
+- Integration with Bootstrap form validation
+- Floating label support
+- Size variants (sm, lg)
+- Full Bootstrap styling
+- Inherits all BaseField functionality
+
+## Basic Usage
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ duration = 'P5DT1H30M'; // 5 days, 1 hour, 30 minutes
+}
+```
+
+## Examples
+
+### With Validation
+
+```html
+
+```
+
+### With Floating Label
+
+```html
+
+```
+
+### Custom Validation
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ duration = '';
+
+ get isValid() {
+ if (!this.duration) return undefined;
+ // Check if duration is at least 1 hour
+ const match = this.duration.match(/PT(\d+)H/);
+ return match && parseInt(match[1]) >= 1;
+ }
+}
+```
+
+## Bindable Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `value` | `string` | - | Two-way bound ISO 8601 duration string (e.g., `P5DT1H30M`) |
+| `bsSize` | `'sm' \| 'lg'` | - | Bootstrap size variant |
+| `floatingLabel` | `boolean` | `false` | Enable floating label style |
+
+### Inherited from BaseField
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `name` | `string` | - | Input name attribute |
+| `label` | `string` | - | Label text |
+| `title` | `string` | - | Title attribute |
+| `disabled` | `boolean` | `false` | Disable the input |
+| `required` | `boolean` | `false` | Mark as required |
+| `valid` | `boolean` | - | Validation state (undefined = not validated, true = valid, false = invalid) |
+| `validFeedback` | `string` | - | Valid feedback message |
+| `invalidFeedback` | `string` | - | Invalid feedback message |
+| `form` | `string` | - | Associated form id |
+| `text` | `string \| HTMLElement` | - | Helper text displayed below the input |
+
+## ISO 8601 Duration Format
+
+The component uses ISO 8601 duration format:
+
+- `P` - Period designator (required)
+- `nD` - Number of days
+- `T` - Time designator (required if hours/minutes/seconds are present)
+- `nH` - Number of hours
+- `nM` - Number of minutes
+- `nS` - Number of seconds
+
+### Examples
+
+- `P5D` - 5 days
+- `PT2H` - 2 hours
+- `PT30M` - 30 minutes
+- `P1DT6H30M` - 1 day, 6 hours, 30 minutes
+- `PT1H30M45S` - 1 hour, 30 minutes, 45 seconds
+
+## Styling
+
+The component uses standard Bootstrap form control classes and can be styled using Bootstrap utilities:
+
+```html
+
+```
+
+## Accessibility
+
+The component follows accessibility best practices:
+
+- Proper label association
+- ARIA attributes for validation states
+- Keyboard navigation support
+- Screen reader friendly
+
+## Browser Support
+
+Requires browsers that support:
+
+- ES2015+
+- Custom Elements
+- Temporal API (polyfilled)
diff --git a/packages/bootstrap-addons/src/forms/duration-input/duration-input.html b/packages/bootstrap-addons/src/forms/duration-input/duration-input.html
index ceccc3f..9922ce7 100644
--- a/packages/bootstrap-addons/src/forms/duration-input/duration-input.html
+++ b/packages/bootstrap-addons/src/forms/duration-input/duration-input.html
@@ -1,14 +1,8 @@
- ${label}
+ ${label}
-
+
+
${labels.years}
@@ -22,7 +16,7 @@
${labels.seconds}
- ${label}
+ ${label}
${invalidFeedback}
${validFeedback}
diff --git a/packages/bootstrap-addons/src/forms/duration-input/duration-input.stories.ts b/packages/bootstrap-addons/src/forms/duration-input/duration-input.stories.ts
index b5dc032..45777c2 100644
--- a/packages/bootstrap-addons/src/forms/duration-input/duration-input.stories.ts
+++ b/packages/bootstrap-addons/src/forms/duration-input/duration-input.stories.ts
@@ -1,39 +1,84 @@
import { BsButton } from '@ekzo-dev/bootstrap';
-import { createComponentTemplate, Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
-import { selectControl } from '../../../../../.storybook/helpers';
import { BsDurationInput } from '.';
-const meta: Meta = {
- title: 'Ekzo / Bootstrap Addons / Forms / Duration input',
+const meta = {
+ title: 'Bootstrap Addons / Forms / Duration input',
component: BsDurationInput,
- args: {
- value: 'P5DT1Hasds',
- label: 'Duration',
- },
+ render: () => ({
+ template: ` `,
+ }),
argTypes: {
- bsSize: selectControl(['', 'sm', 'lg']),
+ // BsDurationInput properties
+ value: { control: 'text' },
+ bsSize: {
+ control: 'select',
+ options: ['sm', 'lg'],
+ },
+ floatingLabel: { control: 'boolean' },
+
+ // BaseField properties
+ name: { control: 'text' },
+ label: { control: 'text' },
+ title: { control: 'text' },
+ disabled: { control: 'boolean' },
+ required: { control: 'boolean' },
+ valid: { control: 'boolean' },
+ validFeedback: { control: 'text' },
+ invalidFeedback: { control: 'text' },
+ form: { control: 'text' },
+ text: { control: 'text' },
},
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
-const Validation: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
- template: ``,
- components: [BsButton],
-});
-
-Validation.args = {
- required: true,
+export const Overview = {
+ args: {
+ value: 'P5DT1H',
+ label: 'Duration',
+ },
};
-// eslint-disable-next-line
-export { Overview, Validation };
+export const Validation = {
+ render: () => ({
+ template: ``,
+ components: [BsButton],
+ }),
+ args: {
+ label: 'Duration',
+ required: true,
+ },
+};
diff --git a/packages/bootstrap-addons/src/forms/duration-input/duration-input.ts b/packages/bootstrap-addons/src/forms/duration-input/duration-input.ts
index 49ca2e4..dd055a7 100644
--- a/packages/bootstrap-addons/src/forms/duration-input/duration-input.ts
+++ b/packages/bootstrap-addons/src/forms/duration-input/duration-input.ts
@@ -5,13 +5,20 @@ import './duration-input.scss';
import { BaseField, Size } from '@ekzo-dev/bootstrap';
import { coerceBoolean } from '@ekzo-dev/toolkit';
import { Temporal } from '@js-temporal/polyfill';
-import { bindable, BindingMode, customElement, resolve } from 'aurelia';
+import { bindable, BindingMode, customElement } from 'aurelia';
-type Duration = Partial>;
+type Writable = {
+ -readonly [P in keyof T]: T[P];
+};
+type Duration = Writable<
+ Partial>
+>;
type DurationLabels = {
[K in keyof Duration]: string;
};
+const durationKeys: (keyof Duration)[] = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] as const;
+
/**
* https://github.com/whatwg/html/issues/5488
* https://github.com/tc39/proposal-intl-duration-format
@@ -25,7 +32,7 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
get value(): string {
const { duration } = this;
- if (Object.values(duration).every((v) => v == null || v.toString() === '')) {
+ if (durationKeys.every((v) => duration[v] == null || duration[v].toString() === '')) {
return '';
}
@@ -43,9 +50,7 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
bsSize?: Size;
@bindable(coerceBoolean)
- floatingLabel: boolean = false;
-
- readonly host = resolve(HTMLElement);
+ floatingLabel: boolean = this.config.floatingLabels;
duration: Duration = {};
@@ -65,6 +70,7 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
}
attaching() {
+ super.attaching();
this.controls = this.host.querySelectorAll('input[type=number]');
this.controls.forEach((control) => {
control.addEventListener('keypress', this);
@@ -80,7 +86,7 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
}
handleEvent(event: KeyboardEvent | ClipboardEvent): void {
- const data = event instanceof KeyboardEvent ? event.key : event.clipboardData.getData('text');
+ const data = event instanceof KeyboardEvent ? event.key : event.clipboardData!.getData('text');
// don't allow non-numeric values
if (!/^\d+$/.test(data)) {
@@ -100,15 +106,12 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
private _parseDuration(value: string) {
try {
const duration = Temporal.Duration.from(value);
+ const result: Duration = {};
- this.duration = {
- years: duration.years,
- months: duration.months,
- days: duration.days,
- hours: duration.hours,
- minutes: duration.minutes,
- seconds: duration.seconds,
- };
+ durationKeys.forEach((key) => {
+ result[key] = duration[key] ? duration[key] : undefined;
+ });
+ this.duration = result;
} catch (error) {
if (error instanceof RangeError) {
console.warn(`[bs-duration-input] ${error.message}`);
@@ -120,6 +123,7 @@ export class BsDurationInput extends BaseField implements EventListenerObject {
}
#getLabels(): DurationLabels {
+ // @ts-ignore
const str: string = new Intl['DurationFormat'](navigator.language, { style: 'narrow' }).format({
years: 1,
months: 1,
diff --git a/packages/bootstrap-addons/src/forms/json-input/README.md b/packages/bootstrap-addons/src/forms/json-input/README.md
new file mode 100644
index 0000000..75a07f3
--- /dev/null
+++ b/packages/bootstrap-addons/src/forms/json-input/README.md
@@ -0,0 +1,301 @@
+# JSON Input
+
+A powerful JSON editor with schema validation support, powered by [vanilla-jsoneditor](https://github.com/josdejong/svelte-jsoneditor).
+
+## Overview
+
+The JSON Input component provides a rich editing experience for JSON data with support for JSON Schema validation, syntax highlighting, and multiple editing modes.
+
+## Features
+
+- JSON Schema validation
+- Tree and text editing modes
+- Syntax highlighting
+- Error highlighting and validation messages
+- Auto-completion based on schema
+- Search and replace functionality
+- Undo/redo support
+- Copy/paste/cut operations
+- Keyboard shortcuts
+- Dark mode support
+
+## Basic Usage
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ jsonData = {
+ name: 'John Doe',
+ age: 30,
+ email: 'john@example.com'
+ };
+}
+```
+
+## Examples
+
+### With JSON Schema Validation
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ userData = {
+ name: 'John Doe',
+ age: 30,
+ email: 'john@example.com'
+ };
+
+ userSchema = {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ minLength: 1
+ },
+ age: {
+ type: 'number',
+ minimum: 0,
+ maximum: 120
+ },
+ email: {
+ type: 'string',
+ format: 'email'
+ }
+ },
+ required: ['name', 'email']
+ };
+}
+```
+
+### JSON Schema Editor Mode
+
+Use `json-schema.bind="true"` to enable JSON Schema editor mode:
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ schema = {
+ type: 'object',
+ properties: {
+ name: { type: 'string' }
+ }
+ };
+}
+```
+
+### With Custom Editor Options
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ jsonData = { /* ... */ };
+
+ editorOptions = {
+ mode: 'tree', // or 'text'
+ mainMenuBar: true,
+ navigationBar: true,
+ statusBar: true,
+ readOnly: false
+ };
+}
+```
+
+### Disabled State
+
+```html
+
+```
+
+### Required Field
+
+```html
+
+```
+
+## Bindable Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `value` | `any` | - | Two-way bound JSON value |
+| `jsonSchema` | `object \| SchemaNode \| boolean` | - | JSON Schema for validation. Use `true` for schema editor mode |
+| `required` | `boolean` | `false` | Mark as required field |
+| `disabled` | `boolean` | `false` | Disable the editor (read-only mode) |
+| `validatorOptions` | `object` | - | Options for json-schema-library validator |
+| `jsonEditorOptions` | `object` | - | Options for vanilla-jsoneditor (see [docs](https://github.com/josdejong/svelte-jsoneditor#api)) |
+
+## JSON Editor Options
+
+The `jsonEditorOptions` property accepts all options from vanilla-jsoneditor:
+
+```typescript
+interface JSONEditorOptions {
+ mode?: 'tree' | 'text';
+ mainMenuBar?: boolean;
+ navigationBar?: boolean;
+ statusBar?: boolean;
+ readOnly?: boolean;
+ indentation?: number | string;
+ tabSize?: number;
+ escapeControlCharacters?: boolean;
+ escapeUnicodeCharacters?: boolean;
+ flattenColumns?: boolean;
+ // ... and more
+}
+```
+
+See [vanilla-jsoneditor API documentation](https://github.com/josdejong/svelte-jsoneditor#api) for full list of options.
+
+## Validator Options
+
+The `validatorOptions` property accepts options for json-schema-library validator:
+
+```typescript
+export class MyComponent {
+ validatorOptions = {
+ // Custom format validators
+ formats: {
+ phone: /^\+?[0-9]{10,15}$/
+ }
+ };
+}
+```
+
+## JSON Schema Support
+
+The component uses [json-schema-library](https://github.com/sagold/json-schema-library) for validation, which supports:
+
+- Draft-04, Draft-06, Draft-07, Draft 2019-09
+- All standard validation keywords
+- Custom formats
+- `$ref` resolution
+- `allOf`, `anyOf`, `oneOf`
+- Conditional schemas (`if`, `then`, `else`)
+
+### Schema Example
+
+```typescript
+const schema = {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 100
+ },
+ age: {
+ type: 'integer',
+ minimum: 0,
+ maximum: 120
+ },
+ email: {
+ type: 'string',
+ format: 'email'
+ },
+ address: {
+ type: 'object',
+ properties: {
+ street: { type: 'string' },
+ city: { type: 'string' },
+ zipCode: { type: 'string', pattern: '^[0-9]{5}$' }
+ },
+ required: ['street', 'city']
+ },
+ tags: {
+ type: 'array',
+ items: { type: 'string' },
+ minItems: 1,
+ uniqueItems: true
+ }
+ },
+ required: ['name', 'email']
+};
+```
+
+## Keyboard Shortcuts
+
+The editor supports standard keyboard shortcuts:
+
+- `Ctrl+Z` / `Cmd+Z` - Undo
+- `Ctrl+Shift+Z` / `Cmd+Shift+Z` - Redo
+- `Ctrl+F` / `Cmd+F` - Search
+- `Ctrl+H` / `Cmd+H` - Replace
+- `Ctrl+C` / `Cmd+C` - Copy
+- `Ctrl+V` / `Cmd+V` - Paste
+- `Ctrl+X` / `Cmd+X` - Cut
+
+## Styling
+
+The component can be styled using CSS custom properties:
+
+```css
+bs-json-input {
+ --jse-theme-color: #4299e1;
+ --jse-background-color: #ffffff;
+ --jse-text-color: #1a202c;
+}
+```
+
+## Events
+
+The component emits standard change events that can be handled in Aurelia:
+
+```html
+
+```
+
+## Accessibility
+
+The component follows accessibility best practices:
+
+- Keyboard navigation support
+- Screen reader friendly
+- ARIA labels and roles
+- Focus management
+
+## Performance
+
+For large JSON documents:
+
+- Use text mode for better performance
+- Consider limiting depth in tree mode
+- Use `flattenColumns` option for wide objects
+
+## Browser Support
+
+Requires browsers that support:
+
+- ES2015+
+- Custom Elements
+- Web Components
diff --git a/packages/bootstrap-addons/src/forms/json-input/json-input.stories.ts b/packages/bootstrap-addons/src/forms/json-input/json-input.stories.ts
index d6b32eb..dcf86e5 100644
--- a/packages/bootstrap-addons/src/forms/json-input/json-input.stories.ts
+++ b/packages/bootstrap-addons/src/forms/json-input/json-input.stories.ts
@@ -1,59 +1,69 @@
-import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
-
import { BsJsonInput } from '.';
-const meta: Meta = {
- title: 'Ekzo / Bootstrap Addons / Forms / Json input',
+const meta = {
+ title: 'Bootstrap Addons / Forms / Json input',
component: BsJsonInput,
+ render: () => ({
+ template: ` `,
+ }),
+ argTypes: {
+ // BsJsonInput properties
+ value: { control: 'object' },
+ required: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ jsonSchema: { control: 'object' },
+ validatorOptions: { control: 'object' },
+ jsonEditorOptions: { control: 'object' },
+ },
};
export default meta;
-const Overview: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
-Overview.args = {
- jsonSchema: {
- $schema: 'https://json-schema.org/draft/2020-12/schema',
- type: 'object',
- properties: {
- enum: {
- type: 'string',
- enum: ['1', '2', '3'],
- },
- boolean: {
- type: 'boolean',
- },
- email: {
- type: 'string',
- format: 'email',
- },
- number: {
- type: 'number',
- multipleOf: 0.0001,
+export const Overview = {
+ args: {
+ jsonSchema: {
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
+ type: 'object',
+ properties: {
+ enum: {
+ type: 'string',
+ enum: ['1', '2', '3'],
+ },
+ boolean: {
+ type: 'boolean',
+ },
+ email: {
+ type: 'string',
+ format: 'email',
+ },
+ number: {
+ type: 'number',
+ multipleOf: 0.0001,
+ },
},
+ required: ['enum'],
+ },
+ value: {
+ number: 1.11,
+ enum: '1',
},
- required: ['enum'],
- },
- value: {
- number: 1.11,
- enum: '1',
},
};
-const JsonSchemaEditor: Story = (args): StoryFnAureliaReturnType => ({
- props: args,
-});
-
-JsonSchemaEditor.args = {
- jsonSchema: true,
- value: {
- $schema: 'http://json-schema.org/draft-07/schema#',
- type: 'object',
- properties: {},
+export const JsonSchemaEditor = {
+ args: {
+ jsonSchema: true,
+ value: {
+ $schema: 'http://json-schema.org/draft-07/schema#',
+ type: 'object',
+ properties: {},
+ },
},
};
-
-// eslint-disable-next-line
-export { Overview, JsonSchemaEditor };
diff --git a/packages/bootstrap-addons/src/forms/json-input/json-input.ts b/packages/bootstrap-addons/src/forms/json-input/json-input.ts
index 4f94131..1d66f09 100644
--- a/packages/bootstrap-addons/src/forms/json-input/json-input.ts
+++ b/packages/bootstrap-addons/src/forms/json-input/json-input.ts
@@ -17,16 +17,17 @@ import type {
Validator,
} from 'vanilla-jsoneditor';
+import { ProxyObservable } from '@aurelia/runtime';
import { coerceBoolean } from '@ekzo-dev/toolkit';
import { JsonEditor } from '@ekzo-dev/vanilla-jsoneditor';
import { faUpRightAndDownLeftFromCenter } from '@fortawesome/free-solid-svg-icons/faUpRightAndDownLeftFromCenter';
-import Ajv, { ErrorObject, Options, ValidateFunction } from 'ajv';
+import Ajv, { type AnySchema, type ErrorObject, type Options } from 'ajv';
import Ajv2019 from 'ajv/dist/2019';
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import { bindable, BindingMode, customElement } from 'aurelia';
import { parsePath } from 'immutable-json-patch';
-import { compileSchema, JsonError, JsonSchema, SchemaNode } from 'json-schema-library';
+import { CompileOptions, compileSchema, JsonError, SchemaNode } from 'json-schema-library';
const patternMap: Record = {
'^[A-Za-z_][-A-Za-z0-9._]*$': '^[A-Za-z_][\\-A-Za-z0-9._]*$',
@@ -48,11 +49,11 @@ export class BsJsonInput {
@bindable(coerceBoolean)
disabled: boolean = false;
- @bindable({ set: (v) => (v === '' || v === true || v === 'true' ? true : v) })
- jsonSchema?: JSONSchema | boolean;
+ @bindable({ set: (v: unknown) => (v === '' || v === true || v === 'true' ? true : v) })
+ jsonSchema?: JSONSchema | SchemaNode | boolean;
@bindable()
- ajvOptions: Options = {};
+ validatorOptions?: CompileOptions;
@bindable()
jsonEditorOptions: JSONEditorPropsOptional = {};
@@ -76,7 +77,7 @@ export class BsJsonInput {
}
onRenderValue = (props: RenderValueProps): RenderValueComponentDescription[] => {
- let result: RenderValueComponentDescription[] | null;
+ // let result: RenderValueComponentDescription[] | null;
// const { jsonSchema } = this;
const { editorModule } = this.editorComponent;
@@ -85,7 +86,7 @@ export class BsJsonInput {
// result = editorModule.renderJSONSchemaEnum(props, jsonSchema, this.#getSchemaDefinitions(jsonSchema));
// }
- return result ?? editorModule.renderValue(props);
+ return editorModule!.renderValue(props);
};
onRenderMenu = (items: MenuItem[]): MenuItem[] | undefined => {
@@ -139,67 +140,39 @@ export class BsJsonInput {
return this.required && (this.value == null || this.value === '');
}
- get schemaVersion(): string {
+ get schemaVersion(): string | undefined {
if (this.jsonSchema !== true) return undefined;
- const { value } = this;
+ const value = this.value as JSONSchema;
- return value == null || value['$schema'] == null ? '' : (value['$schema'] as string);
+ return value == null || value.$schema == null ? '' : (value.$schema as string);
}
get validator(): Validator | undefined {
- const { schemaVersion, disabled, ajvOptions, jsonSchema } = this;
- // use raw object because proxies don't work with private properties
- const rawThis = this['__raw__'] as BsJsonInput;
- // use jsonSchema from raw object to pass original (non-proxied) object to AJV
- const { jsonSchema: rawJsonSchema } = rawThis;
+ const { schemaVersion, disabled, jsonSchema } = this;
+ const rawThis = ProxyObservable.getRaw(this);
- if (jsonSchema && typeof jsonSchema === 'object' && !disabled) {
- const ajv = rawThis.#initAjv(jsonSchema.$schema as string, ajvOptions);
+ if (disabled) return;
- addFormats(ajv);
- let validate: ValidateFunction;
- let schema: SchemaNode;
-
- try {
- schema = compileSchema(rawJsonSchema as JsonSchema);
- } catch (e) {
- console.error('json-schema-library validator compilation error', e);
- }
-
- try {
- validate = ajv.compile(rawJsonSchema);
- } catch (e) {
- console.error('AJV validator compilation error', e);
- }
+ if (jsonSchema && typeof jsonSchema === 'object') {
+ const schema = rawThis.#initJsonSchemaLibrary();
return (json: unknown): ValidationError[] => {
// do not validate empty documents
- if (json === undefined) return [];
+ if (json === undefined || !schema) return [];
- let allErrors: ValidationError[] = [];
-
- if (schema) {
- const { errors } = schema.validate(json);
-
- allErrors = rawThis.#processErrors(errors, json);
- }
+ const { errors } = schema.validate(json);
- if (validate) {
- validate(json);
- allErrors = allErrors.concat(rawThis.#processErrorsAjv(validate.errors, json));
- }
-
- return allErrors;
+ return rawThis.#processErrors(errors, json);
};
- } else if (schemaVersion != null && !disabled) {
- const ajv = rawThis.#initAjv(schemaVersion, ajvOptions);
+ } else if (schemaVersion != null) {
+ const ajv = rawThis.#initAjv(schemaVersion);
return (json: unknown): ValidationError[] => {
// do not validate empty documents
if (json === undefined) return [];
- void ajv.validateSchema(json);
+ void ajv.validateSchema(json as AnySchema);
return rawThis.#processErrorsAjv(ajv.errors, json);
};
@@ -216,7 +189,7 @@ export class BsJsonInput {
}
}
- #initAjv($schema: string, ajvOptions: Options): Ajv {
+ #initAjv($schema: string): Ajv {
// some regexp's in 2019-09/2020-12 meta-schemas are not compatible with 'v' flag, so update them
const regExp = (pattern: string) => new RegExp(patternMap[pattern] ?? pattern, 'v');
@@ -228,7 +201,7 @@ export class BsJsonInput {
code: {
regExp,
},
- ...ajvOptions,
+ allErrors: true,
};
let ajv: Ajv;
@@ -245,14 +218,33 @@ export class BsJsonInput {
ajv = new Ajv(options);
}
+ addFormats(ajv);
+
return ajv;
}
+ #initJsonSchemaLibrary(): SchemaNode | undefined {
+ const { jsonSchema } = this;
+
+ // already a SchemaNode
+ if ((jsonSchema as SchemaNode).evaluationPath) {
+ return jsonSchema as SchemaNode;
+ }
+
+ try {
+ return compileSchema(jsonSchema as JSONSchema, this.validatorOptions);
+ } catch (e) {
+ console.error('json-schema-library validator compilation error', e);
+
+ return;
+ }
+ }
+
#getSchemaDefinitions(schema: JSONSchema): JSONSchemaDefinitions {
return (schema.$defs ?? schema.definitions) as JSONSchemaDefinitions;
}
- #processErrorsAjv(errors: ErrorObject[] | null, json: unknown): ValidationError[] {
+ #processErrorsAjv(errors: ErrorObject[] | null | undefined, json: unknown): ValidationError[] {
const message = this.jsonSchema === true ? 'JSON is not a valid JSONSchema' : 'JSON does not match schema';
this.input.setCustomValidity(errors?.length ? message : '');
@@ -272,7 +264,7 @@ export class BsJsonInput {
return (errors || []).map((error) => ({
path: parsePath(json, error.data.pointer),
message: error.message || 'Unknown error',
- severity: 'error' as ValidationSeverity,
+ severity: 'warning' as ValidationSeverity,
}));
}
}
diff --git a/packages/bootstrap-addons/src/forms/select-dropdown/README.md b/packages/bootstrap-addons/src/forms/select-dropdown/README.md
new file mode 100644
index 0000000..b9c4d84
--- /dev/null
+++ b/packages/bootstrap-addons/src/forms/select-dropdown/README.md
@@ -0,0 +1,324 @@
+# Select Dropdown
+
+An enhanced select component with improved styling and functionality, extending the base `BsSelect` component.
+
+## Overview
+
+The Select Dropdown component provides a feature-rich alternative to native select elements with Bootstrap styling, multi-select support, and performance optimization for large datasets.
+
+## Features
+
+- Single and multi-select support
+- Multiple option formats (array, object, entries)
+- Empty value handling
+- Bootstrap form styling
+- Floating label support
+- Size variants (sm, lg)
+- Performance optimized for large datasets
+- Custom value matching
+- Keyboard navigation
+- Full Bootstrap form integration
+- Inherits all BaseField functionality
+
+## Basic Usage
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ selectedValue = 'us';
+
+ countries = [
+ { value: 'us', text: 'United States' },
+ { value: 'uk', text: 'United Kingdom' },
+ { value: 'ca', text: 'Canada' }
+ ];
+}
+```
+
+## Examples
+
+### Options as Array
+
+```typescript
+export class MyComponent {
+ options = [
+ { value: 1, text: 'Option 1' },
+ { value: 2, text: 'Option 2', disabled: true },
+ { value: 3, text: 'Option 3' }
+ ];
+}
+```
+
+### Options as Object
+
+```typescript
+export class MyComponent {
+ options = {
+ '1': 'Option 1',
+ '2': 'Option 2',
+ '3': 'Option 3'
+ };
+}
+```
+
+### Options as Entries
+
+```typescript
+export class MyComponent {
+ options = [
+ ['us', 'United States'],
+ ['uk', 'United Kingdom'],
+ ['ca', 'Canada']
+ ];
+}
+```
+
+### Multi-Select
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ selectedValues = ['red', 'blue'];
+
+ colors = [
+ { value: 'red', text: 'Red' },
+ { value: 'blue', text: 'Blue' },
+ { value: 'green', text: 'Green' }
+ ];
+}
+```
+
+### With Floating Label
+
+```html
+
+```
+
+### With Validation
+
+```html
+
+```
+
+### With Grouped Options
+
+```typescript
+export class MyComponent {
+ options = [
+ { value: 'usa', text: 'United States', group: 'North America' },
+ { value: 'can', text: 'Canada', group: 'North America' },
+ { value: 'mex', text: 'Mexico', group: 'North America' },
+ { value: 'uk', text: 'United Kingdom', group: 'Europe' },
+ { value: 'de', text: 'Germany', group: 'Europe' }
+ ];
+}
+```
+
+### Large Dataset (Performance)
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ largeDataset = Array.from({ length: 1000 }, (_, i) => ({
+ value: i,
+ text: `Item ${i + 1}`
+ }));
+}
+```
+
+### With Custom Matcher
+
+```html
+
+```
+
+```typescript
+export class MyComponent {
+ selectedUser = { id: 1, name: 'John' };
+
+ users = [
+ { value: { id: 1, name: 'John' }, text: 'John Doe' },
+ { value: { id: 2, name: 'Jane' }, text: 'Jane Smith' }
+ ];
+
+ userMatcher = (a, b) => a?.id === b?.id;
+}
+```
+
+### With Empty Value
+
+```html
+
+```
+
+## Bindable Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `value` | `any \| any[]` | - | Selected value(s), array for multiple mode |
+| `options` | `ISelectOption[] \| [any, string][] \| Record` | `[]` | Options as array of objects, entries, or key-value object |
+| `multiple` | `boolean` | `false` | Enable multi-select mode |
+| `floatingLabel` | `boolean` | `false` | Enable floating label style |
+| `size` | `number` | - | Number of visible options (HTML size attribute) |
+| `bsSize` | `'sm' \| 'lg'` | - | Bootstrap size variant |
+| `autocomplete` | `string` | - | Autocomplete attribute value |
+| `matcher` | `(a: any, b: any) => boolean` | - | Custom function to compare values |
+| `emptyValue` | `any` | - | Value to use when no option is selected |
+
+### Inherited from BaseField
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `name` | `string` | - | Input name attribute |
+| `label` | `string` | - | Label text |
+| `title` | `string` | - | Title attribute |
+| `disabled` | `boolean` | `false` | Disable the select |
+| `required` | `boolean` | `false` | Mark as required |
+| `valid` | `boolean` | - | Validation state |
+| `validFeedback` | `string` | - | Valid feedback message |
+| `invalidFeedback` | `string` | - | Invalid feedback message |
+| `form` | `string` | - | Associated form id |
+| `text` | `string \| HTMLElement` | - | Helper text |
+
+## ISelectOption Interface
+
+```typescript
+interface ISelectOption {
+ value: T; // The actual value
+ text: string; // Display text
+ disabled?: boolean; // Whether option is disabled
+ group?: string; // Optional group name
+}
+```
+
+## Value Matching
+
+By default, values are compared using strict equality (`===`). For complex objects, provide a custom `matcher` function:
+
+```typescript
+// Match by ID
+matcher = (a, b) => a?.id === b?.id;
+
+// Match by multiple properties
+matcher = (a, b) => a?.type === b?.type && a?.id === b?.id;
+
+// Deep equality (use with caution for performance)
+matcher = (a, b) => JSON.stringify(a) === JSON.stringify(b);
+```
+
+## Multi-Select Mode
+
+When `multiple` is true:
+
+- Value is an array
+- Multiple options can be selected
+- Ctrl/Cmd+Click to toggle selection
+- Shift+Click for range selection
+
+```typescript
+export class MyComponent {
+ selectedValues: string[] = ['option1', 'option2'];
+}
+```
+
+## Empty Value Handling
+
+Use `emptyValue` to specify what value should be used when no option is selected:
+
+```html
+
+
+
+
+
+
+
+
+```
+
+## Styling
+
+The component uses standard Bootstrap form classes and can be styled using Bootstrap utilities:
+
+```html
+
+```
+
+## Accessibility
+
+The component follows accessibility best practices:
+
+- Proper label association
+- ARIA attributes for validation states
+- Keyboard navigation (Arrow keys, Enter, Space)
+- Screen reader friendly
+- Focus management
+
+## Performance Optimization
+
+For large datasets:
+
+1. Use the `size` attribute to limit visible options
+2. Consider virtual scrolling for very large lists
+3. Avoid re-rendering by using stable option references
+4. Use primitive values instead of objects when possible
+
+## Browser Support
+
+Requires browsers that support:
+
+- ES2015+
+- Custom Elements
+- HTML5 form controls
diff --git a/packages/bootstrap-addons/src/forms/select/filter.ts b/packages/bootstrap-addons/src/forms/select-dropdown/filter.ts
similarity index 100%
rename from packages/bootstrap-addons/src/forms/select/filter.ts
rename to packages/bootstrap-addons/src/forms/select-dropdown/filter.ts
diff --git a/packages/bootstrap-addons/src/forms/select-dropdown/index.ts b/packages/bootstrap-addons/src/forms/select-dropdown/index.ts
new file mode 100644
index 0000000..a94fd49
--- /dev/null
+++ b/packages/bootstrap-addons/src/forms/select-dropdown/index.ts
@@ -0,0 +1 @@
+export * from './select-dropdown';
diff --git a/packages/bootstrap-addons/src/forms/select/select.html b/packages/bootstrap-addons/src/forms/select-dropdown/select-dropdown.html
similarity index 75%
rename from packages/bootstrap-addons/src/forms/select/select.html
rename to packages/bootstrap-addons/src/forms/select-dropdown/select-dropdown.html
index 8618317..366feb0 100644
--- a/packages/bootstrap-addons/src/forms/select/select.html
+++ b/packages/bootstrap-addons/src/forms/select-dropdown/select-dropdown.html
@@ -1,16 +1,11 @@
- ${label}
+ ${label}
- ${option.text || ' '}
+ ${option.text || ' '}
@@ -49,12 +44,14 @@ ${k}
class="form-check-input"
type="checkbox"
checked.bind="value"
- model.bind="option.value"
- disabled.bind="option.disabled"
matcher.bind="matcher"
- id="${optionId($index, $parent.$index)}"
+ value.one-time="option.value"
+ disabled.one-time="option.disabled"
+ id.one-time="optionId($index, $parent.$index)"
/>
- ${option.text || ' '}
+ ${option.text || ' '}
@@ -64,7 +61,7 @@