import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
Output,
ViewChild
} from "@angular/core";
import { SdInputValidate } from "@simplysm/sd-angular";
import { DateOnly, DateTime, StringUtil, Time } from "@simplysm/sd-core-common";
@Component({
selector: "sdm-textfield",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
`,
styles: [/* language=SCSS */ `
:host {
display: block;
> input {
width: 100%;
padding: calc(var(--gap-sm) + 1px) 0 calc(var(--gap-sm) - 1px);
border: none;
border-bottom: 2px solid var(--border-color);
background: transparent;
font-size: var(--font-size-default);
font-family: var(--font-family);
line-height: var(--line-height);
color: var(--text-brightness-default);
&::-webkit-input-placeholder {
color: var(--text-brightness-lighter);
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&::-webkit-calendar-picker-indicator {
margin: auto;
}
transition: border-color 0.3s;
}
&[sd-invalid=true] {
> input {
border-bottom-color: var(--theme-color-danger-default);
}
}
> input {
&:focus {
border-color: var(--theme-color-primary-default);
}
&:disabled {
border-bottom-color: transparent;
color: var(--text-brightness-light);
}
}
&[sd-type=number] {
> input {
text-align: right;
}
}
&[sd-size=sm] {
> input {
padding: calc(var(--gap-xs) + 1px) 0 calc(var(--gap-xs) - 1px);
}
}
&[sd-size=lg] {
> input {
font-size: var(--font-size-lg);
padding: calc(var(--gap-default) + 1px) 0 calc(var(--gap-default) - 1px);
}
}
&[sd-inline=true] {
display: inline-block;
> input {
display: inline-block;
width: auto;
}
}
&[sd-inset=true] {
> input {
border: none;
&:focus {
outline: 1px solid var(--theme-color-primary-default);
}
padding: var(--gap-sm) 0;
}
&[sd-size=sm] {
> input {
padding: var(--gap-xs) 0;
}
}
&[sd-size=lg] {
> input {
padding: var(--gap-default) 0;
}
}
}
}`]
})
export class SdmTextfieldControl {
@Input()
@SdInputValidate({
type: String,
includes: ["number", "text", "password", "date", "datetime", "datetime-sec", "time", "time-sec", "month", "year", "color", "email", "brn"],
notnull: true
})
@HostBinding("attr.sd-type")
public type: "number" | "text" | "password" | "date" | "datetime" | "datetime-sec" | "time" | "time-sec" | "month" | "year" | "color" | "email" | "brn" = "text";
@Input()
@SdInputValidate(String)
public placeholder?: string;
@Input()
@SdInputValidate(String)
public title?: string;
@Input()
@SdInputValidate([Number, String, DateOnly, DateTime, Time])
public value?: number | string | DateOnly | DateTime | Time;
@Output()
public readonly valueChange = new EventEmitter();
@Input()
@SdInputValidate(Boolean)
public disabled?: boolean;
@Input()
@SdInputValidate(Boolean)
public required?: boolean;
@Input()
@SdInputValidate(Number)
public min?: number;
@Input()
@SdInputValidate(Number)
public max?: number;
@Input()
@SdInputValidate(Number)
public minlength?: number;
@Input()
@SdInputValidate(Number)
public maxlength?: number;
/**
* 10, 1, 0.1, 0.01, 0.01 방식으로 입력
*/
@Input()
@SdInputValidate(Number)
public step?: number;
@Input()
@SdInputValidate(String)
public pattern?: string;
@Input()
@SdInputValidate(Boolean)
@HostBinding("attr.sd-inline")
public inline?: boolean;
@Input()
@SdInputValidate(Boolean)
@HostBinding("attr.sd-inset")
public inset?: boolean;
@Input()
@SdInputValidate({
type: String,
includes: ["sm", "lg"]
})
@HostBinding("attr.sd-size")
public size?: "sm" | "lg";
@Input()
@SdInputValidate(Function)
public validatorFn?: (value: number | string | DateOnly | DateTime | Time | undefined) => string | undefined;
@Input()
@SdInputValidate(String)
public inputStyle?: string;
@Input()
@SdInputValidate(String)
public inputClass?: string;
@HostBinding("attr.sd-invalid")
public get isInvalid(): boolean {
return Boolean(this.errorMessage);
}
@Input()
@SdInputValidate(Boolean)
public useNumberComma = true;
@ViewChild("input", { static: false, read: ElementRef })
public inputElRef?: ElementRef;
public get controlType(): string {
return this.type === "number" ? "text"
: this.type === "brn" ? "text"
: this.type === "datetime" ? "datetime-local"
: this.type === "datetime-sec" ? "datetime-local"
: this.type === "time-sec" ? "time"
: this.type;
}
public get controlInputMode(): string | undefined {
return this.type === "number" ? "numeric" : undefined;
}
public get controlValue(): string {
if (this.value == null) {
return "";
}
if (this.type === "number" && typeof this.value === "number") {
return this.useNumberComma ? this.value.toLocaleString(undefined, { maximumFractionDigits: 10 }) : this.value.toString(10);
}
if (this.type === "brn" && typeof this.value === "string") {
const str = this.value.replace(/[^0-9]/g, "");
const first = str.substring(0, 3);
const second = str.substring(3, 5);
const third = str.substring(5, 10);
return first
+ (
StringUtil.isNullOrEmpty(second) ? "" : "-" + second
+ (
StringUtil.isNullOrEmpty(third) ? "" : "-" + third
)
);
}
if (this.type === "datetime" && this.value instanceof DateTime) {
return this.value.toFormatString("yyyy-MM-ddTHH:mm");
}
if (this.type === "datetime-sec" && this.value instanceof DateTime) {
return this.value.toFormatString("yyyy-MM-ddTHH:mm:ss");
}
if (this.type === "year" && this.value instanceof DateOnly) {
return this.value.toFormatString("yyyy");
}
if (this.type === "month" && this.value instanceof DateOnly) {
return this.value.toFormatString("yyyy-MM");
}
if (this.type === "date" && this.value instanceof DateOnly) {
return this.value.toFormatString("yyyy-MM-dd");
}
if (this.type === "time" && (this.value instanceof DateTime || this.value instanceof Time)) {
return this.value.toFormatString("HH:mm");
}
if (this.type === "time-sec" && (this.value instanceof DateTime || this.value instanceof Time)) {
return this.value.toFormatString("HH:mm:ss");
}
if (typeof this.value === "string") {
return this.value;
}
throw new Error(`'sd-textfield'에 대한 'value'가 잘못되었습니다. (입력값: ${this.value.toString()})`);
}
public get controlStep(): number | string {
if (this.step !== undefined) {
return this.step;
}
if (this.type === "datetime-sec" || this.type === "time-sec") {
return 1;
}
return "any";
}
public get errorMessage(): string {
const errorMessages: string[] = [];
if (this.value == null) {
if (this.required) {
errorMessages.push("값을 입력하세요.");
}
}
else if (this.type === "number") {
if (typeof this.value !== "number") {
errorMessages.push("숫자를 입력하세요");
}
if (this.min !== undefined && this.min > this.value) {
errorMessages.push(`${this.min}보다 크거나 같아야 합니다.`);
}
if (this.max !== undefined && this.max < this.value) {
errorMessages.push(`${this.max}보다 작거나 같아야 합니다.`);
}
}
else if (this.type === "brn") {
if (typeof this.value !== "string" || !(/^[0-9]{10}$/).test(this.value)) {
errorMessages.push("BRN 형식이 잘못되었습니다.");
}
}
else if (["year", "month", "date"].includes(this.type)) {
if (!(this.value instanceof DateOnly)) {
errorMessages.push("날짜를 입력하세요");
}
}
else if (["datetime", "datetime-sec"].includes(this.type)) {
if (!(this.value instanceof DateTime)) {
errorMessages.push("날짜 및 시간을 입력하세요");
}
}
else if (["time", "time-sec"].includes(this.type)) {
if (!(this.value instanceof Time)) {
errorMessages.push("시간을 입력하세요");
}
}
else if (this.type === "text") {
if (this.minlength !== undefined && this.minlength > (this.value as string).length) {
errorMessages.push(`문자의 길이가 ${this.minlength}보다 길거나 같아야 합니다.`);
}
if (this.maxlength !== undefined && this.maxlength > (this.value as string).length) {
errorMessages.push(`문자의 길이가 ${this.maxlength}보다 짧거나 같아야 합니다.`);
}
}
if (this.validatorFn) {
const message = this.validatorFn(this.value);
if (message !== undefined) {
errorMessages.push(message);
}
}
const errorMessage = errorMessages.join("\r\n");
const inputEl = this.inputElRef?.nativeElement;
if (inputEl) {
inputEl.setCustomValidity(errorMessage);
}
return errorMessage;
}
public onInput(): void {
const inputEl = this.inputElRef!.nativeElement;
if (inputEl.value === "") {
this._setValue(undefined);
}
else if (this.type === "number") {
const inputValue = inputEl.value.replace(/[^0-9-.]/g, "");
if (
Number.isNaN(Number(inputValue))
|| inputValue.endsWith(".")
|| (
inputValue.includes(".")
&& Number(inputValue) === 0
)
) {
}
else {
this._setValue(Number(inputValue));
}
}
else if (this.type === "brn") {
this._setValue(inputEl.value.replace(/[^0-9]/g, ""));
}
else if (["year", "month", "date"].includes(this.type)) {
try {
this._setValue(DateOnly.parse(inputEl.value));
}
catch (err) {
}
}
else if (["datetime", "datetime-sec"].includes(this.type)) {
try {
this._setValue(DateTime.parse(inputEl.value));
}
catch (err) {
}
}
else if (["time", "time-sec"].includes(this.type)) {
try {
this._setValue(Time.parse(inputEl.value));
}
catch (err) {
}
}
else {
this._setValue(inputEl.value);
}
}
private _setValue(newValue: any): void {
if (this.valueChange.observed) {
this.valueChange.emit(newValue);
}
else {
this.value = newValue;
}
}
}