357 lines
7.4 KiB
TypeScript
357 lines
7.4 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
computed,
|
|
input,
|
|
output
|
|
} from '@angular/core';
|
|
|
|
export type ButtonType = 'button' | 'submit' | 'reset';
|
|
|
|
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
|
|
export type IconPosition = 'left' | 'right';
|
|
|
|
export type ButtonVariant =
|
|
| 'primary'
|
|
| 'primary-full'
|
|
| 'outline-primary'
|
|
| 'secondary'
|
|
| 'secondary-full'
|
|
| 'success'
|
|
| 'success-full'
|
|
| 'danger'
|
|
| 'danger-full'
|
|
| 'warning'
|
|
| 'warning-full'
|
|
| 'info'
|
|
| 'info-full'
|
|
| 'light'
|
|
| 'dark'
|
|
| 'transparent'
|
|
| 'custom';
|
|
|
|
export type ButtonAction =
|
|
| 'add'
|
|
| 'save'
|
|
| 'submit'
|
|
| 'update'
|
|
| 'edit'
|
|
| 'delete'
|
|
| 'cancel'
|
|
| 'close'
|
|
| 'upload'
|
|
| 'download'
|
|
| 'import'
|
|
| 'export'
|
|
| 'view'
|
|
| 'search'
|
|
| 'refresh'
|
|
| 'reset'
|
|
| 'approve'
|
|
| 'reject'
|
|
| 'login'
|
|
| 'next'
|
|
| 'previous'
|
|
| 'custom';
|
|
|
|
interface ButtonActionConfig {
|
|
label: string;
|
|
icon: string | null;
|
|
variant: ButtonVariant;
|
|
type: ButtonType;
|
|
}
|
|
|
|
const BUTTON_ACTION_CONFIG: Record<ButtonAction, ButtonActionConfig> = {
|
|
add: {
|
|
label: 'Add',
|
|
icon: 'ri-add-line',
|
|
variant: 'primary-full',
|
|
type: 'button'
|
|
},
|
|
save: {
|
|
label: 'Save',
|
|
icon: 'ri-save-line',
|
|
variant: 'primary-full',
|
|
type: 'submit'
|
|
},
|
|
submit: {
|
|
label: 'Submit',
|
|
icon: 'ri-send-plane-line',
|
|
variant: 'primary-full',
|
|
type: 'submit'
|
|
},
|
|
update: {
|
|
label: 'Update',
|
|
icon: 'ri-save-3-line',
|
|
variant: 'primary-full',
|
|
type: 'submit'
|
|
},
|
|
edit: {
|
|
label: 'Edit',
|
|
icon: 'ri-edit-line',
|
|
variant: 'primary-full',
|
|
type: 'button'
|
|
},
|
|
delete: {
|
|
label: 'Delete',
|
|
icon: 'ri-delete-bin-line',
|
|
variant: 'danger-full',
|
|
type: 'button'
|
|
},
|
|
cancel: {
|
|
label: 'Cancel',
|
|
icon: 'ri-close-line',
|
|
variant: 'light',
|
|
type: 'button'
|
|
},
|
|
close: {
|
|
label: 'Close',
|
|
icon: 'ri-close-line',
|
|
variant: 'light',
|
|
type: 'button'
|
|
},
|
|
upload: {
|
|
label: 'Upload',
|
|
icon: 'ri-upload-cloud-2-line',
|
|
variant: 'primary-full',
|
|
type: 'button'
|
|
},
|
|
download: {
|
|
label: 'Download',
|
|
icon: 'ri-download-cloud-2-line',
|
|
variant: 'success-full',
|
|
type: 'button'
|
|
},
|
|
import: {
|
|
label: 'Import',
|
|
icon: 'ri-file-upload-line',
|
|
variant: 'info-full',
|
|
type: 'button'
|
|
},
|
|
export: {
|
|
label: 'Export',
|
|
icon: 'ri-file-download-line',
|
|
variant: 'success-full',
|
|
type: 'button'
|
|
},
|
|
view: {
|
|
label: 'View',
|
|
icon: 'ri-eye-line',
|
|
variant: 'info-full',
|
|
type: 'button'
|
|
},
|
|
search: {
|
|
label: 'Search',
|
|
icon: 'ri-search-line',
|
|
variant: 'primary-full',
|
|
type: 'button'
|
|
},
|
|
refresh: {
|
|
label: 'Refresh',
|
|
icon: 'ri-refresh-line',
|
|
variant: 'light',
|
|
type: 'button'
|
|
},
|
|
reset: {
|
|
label: 'Reset',
|
|
icon: 'ri-restart-line',
|
|
variant: 'light',
|
|
type: 'reset'
|
|
},
|
|
approve: {
|
|
label: 'Approve',
|
|
icon: 'ri-check-line',
|
|
variant: 'success-full',
|
|
type: 'button'
|
|
},
|
|
reject: {
|
|
label: 'Reject',
|
|
icon: 'ri-close-circle-line',
|
|
variant: 'danger-full',
|
|
type: 'button'
|
|
},
|
|
login: {
|
|
label: 'Login',
|
|
icon: 'ri-login-box-line',
|
|
variant: 'primary-full',
|
|
type: 'submit'
|
|
},
|
|
next: {
|
|
label: 'Next',
|
|
icon: 'ri-arrow-right-line',
|
|
variant: 'primary-full',
|
|
type: 'button'
|
|
},
|
|
previous: {
|
|
label: 'Previous',
|
|
icon: 'ri-arrow-left-line',
|
|
variant: 'light',
|
|
type: 'button'
|
|
},
|
|
custom: {
|
|
label: '',
|
|
icon: null,
|
|
variant: 'custom',
|
|
type: 'button'
|
|
}
|
|
};
|
|
|
|
@Component({
|
|
selector: 'app-button',
|
|
standalone: true,
|
|
templateUrl: './button.html',
|
|
styleUrl: './button.scss',
|
|
host: {
|
|
'[class.w-full]': 'fullWidth()'
|
|
},
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
export class Button {
|
|
/*
|
|
* Action provides sensible defaults.
|
|
* Explicit inputs below override action configuration.
|
|
*/
|
|
readonly action = input<ButtonAction | null>(null);
|
|
|
|
readonly label = input<string | null>(null);
|
|
readonly icon = input<string | null>(null);
|
|
|
|
readonly type = input<ButtonType | null>(null);
|
|
readonly variant = input<ButtonVariant | null>(null);
|
|
readonly size = input<ButtonSize>('md');
|
|
|
|
readonly iconPosition = input<IconPosition>('left');
|
|
readonly showIcon = input(true);
|
|
readonly iconOnly = input(false);
|
|
|
|
readonly loading = input(false);
|
|
readonly loadingLabel = input<string | null>(null);
|
|
readonly disabled = input(false);
|
|
|
|
readonly fullWidth = input(false);
|
|
readonly rounded = input(false);
|
|
|
|
readonly className = input('');
|
|
readonly iconClass = input('');
|
|
|
|
readonly ariaLabel = input<string | null>(null);
|
|
readonly ariaExpanded = input<boolean | null>(null);
|
|
readonly ariaControls = input<string | null>(null);
|
|
readonly title = input<string | null>(null);
|
|
|
|
readonly buttonClicked = output<MouseEvent>();
|
|
|
|
private readonly actionConfig = computed<ButtonActionConfig>(() => {
|
|
const action = this.action();
|
|
|
|
return action
|
|
? BUTTON_ACTION_CONFIG[action]
|
|
: BUTTON_ACTION_CONFIG.custom;
|
|
});
|
|
|
|
readonly resolvedLabel = computed(() => {
|
|
return this.label() ?? this.actionConfig().label;
|
|
});
|
|
|
|
readonly resolvedIcon = computed(() => {
|
|
// Explicit icon overrides the action icon.
|
|
return this.icon() ?? this.actionConfig().icon;
|
|
});
|
|
|
|
readonly resolvedType = computed<ButtonType>(() => {
|
|
return this.type() ?? this.actionConfig().type ?? 'button';
|
|
});
|
|
|
|
readonly resolvedVariant = computed<ButtonVariant>(() => {
|
|
const explicitVariant = this.variant();
|
|
|
|
if (explicitVariant) {
|
|
return explicitVariant;
|
|
}
|
|
|
|
const action = this.action();
|
|
|
|
if (action) {
|
|
return BUTTON_ACTION_CONFIG[action].variant;
|
|
}
|
|
|
|
return 'primary-full';
|
|
});
|
|
|
|
readonly isDisabled = computed(() => {
|
|
return this.disabled() || this.loading();
|
|
});
|
|
|
|
readonly resolvedIconClass = computed(() => {
|
|
return [
|
|
this.resolvedIcon(),
|
|
'align-middle leading-none',
|
|
this.iconClass()
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
});
|
|
|
|
readonly buttonClass = computed(() => {
|
|
const variants: Record<ButtonVariant, string> = {
|
|
primary: 'ti-btn-primary',
|
|
'primary-full': 'ti-btn-primary-full',
|
|
'outline-primary': 'ti-btn-outline-primary',
|
|
|
|
secondary: 'ti-btn-secondary',
|
|
'secondary-full': 'ti-btn-secondary-full',
|
|
|
|
success: 'ti-btn-success',
|
|
'success-full': 'ti-btn-success-full',
|
|
|
|
danger: 'ti-btn-danger',
|
|
'danger-full': 'ti-btn-danger-full',
|
|
|
|
warning: 'ti-btn-warning',
|
|
'warning-full': 'ti-btn-warning-full',
|
|
|
|
info: 'ti-btn-info',
|
|
'info-full': 'ti-btn-info-full',
|
|
|
|
light: 'ti-btn-light',
|
|
dark: 'ti-btn-dark',
|
|
transparent: 'bg-transparent',
|
|
custom: ''
|
|
};
|
|
|
|
const sizes: Record<ButtonSize, string> = {
|
|
xs: '!px-2 !py-1 !text-[0.6875rem]',
|
|
sm: '!px-3 !py-1.5 !text-[0.75rem]',
|
|
md: '',
|
|
lg: 'ti-btn-lg'
|
|
};
|
|
|
|
return [
|
|
'ti-btn',
|
|
'inline-flex items-center justify-center',
|
|
'cursor-pointer',
|
|
variants[this.resolvedVariant()],
|
|
sizes[this.size()],
|
|
this.fullWidth() ? 'w-full' : '',
|
|
this.iconOnly() ? '!p-0' : '',
|
|
this.rounded() ? '!rounded-full' : '',
|
|
this.isDisabled()
|
|
? 'cursor-not-allowed opacity-60'
|
|
: 'cursor-pointer',
|
|
this.className()
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
});
|
|
|
|
onClick(event: MouseEvent): void {
|
|
if (this.isDisabled()) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
this.buttonClicked.emit(event);
|
|
}
|
|
} |