[TypeScript][Moment] 월 선택기 만들기

68775 단어 typescriptmoment

소개



나는 피카데이를 좋아한다. 이는 프레임워크 독립적이고 훌륭한 브라우저 호환성(IE7 포함!)이기 때문입니다.
  • Pikaday - GitHub

  • 그러나 날짜만 선택할 수 있습니다. 이번에는 월을 선택하고 싶습니다.

    Chrome 및 Edge 또는 일부 다른 브라우저의 경우 "input type="month""를 사용할 수 있습니다.


  • input type="month" - HTML: HyperText Markup Language | MDN

  • 그러나 모든 최신 브라우저에서 사용할 수 없고 연도 디자인이 마음에 들지 않기 때문에 사용하고 싶지 않습니다.

    jQuery, React 등의 월 선택기를 밖에 찾을 수 없었기 때문에 만들어 보도록 하겠습니다.

    이 샘플에서는 이전 브라우저에 대해 신경 쓰지 않습니다.
    호환성을 원하는 경우 Autoprefixer를 사용하거나 Flexbox 사용을 중지하고 레이아웃용 테이블로 변경할 수 있습니다.

    환경


  • Node.js 버전 16.3.0
  • TypeScript 버전 4.3.4
  • 모먼트 ver.2.29.1
  • ts-로더 ver.9.2.3
  • 웹팩 ver.5.42.0
  • webpack-cli ver.4.7.2

  • 결과




    형세



    대상 입력 요소 아래에 창을 놓고 싶습니다.
    그래서 "getBoundingClientRect"를 사용하여 대상 rect를 얻습니다.

    monthPicker.ts



    export class MonthPicker {
        private inputTarget: HTMLInputElement|null = null;
        private pickerArea: HTMLElement;
    
        public constructor(target: HTMLInputElement) {
            this.pickerArea = document.createElement('div');
            if(target == null) {
                console.error('target was null');
                return;
            }
            this.inputTarget = target;
            const parentElement = (target.parentElement == null)? document.body: target.parentElement;
            this.addMonthPickerArea(parentElement, target.getBoundingClientRect());
        }
        private addMonthPickerArea(parent: HTMLElement, targetRect: DOMRect) {
            this.pickerArea.className = 'monthpicker_area';
            this.pickerArea.style.position = 'absolute';
            this.pickerArea.style.left = `${targetRect.left}px`;
            this.pickerArea.style.top = `${targetRect.top + targetRect.height}px`;
            this.pickerArea.hidden = true;
            parent.appendChild(this.pickerArea);
        }
    }
    

  • Element.getBoundingClientRect() - Web APIs | MDN

  • 월 picker("pickerArea")의 루트 요소를 생성한 후 요소를 추가하기만 하면 됩니다.

    표시|숨기기



    대상 입력 요소를 클릭하면 월 선택기 영역이 표시됩니다.
    그런 다음 다른 곳을 클릭하면 숨겨집니다.

    표시한 후 해당 영역을 클릭하면 이 순서대로 클릭 이벤트가 발생합니다.
  • 월 선택기 영역 클릭 이벤트가 실행됩니다
  • .
  • document.body 클릭 이벤트가 실행됩니다
  • .

    그래서 1.에 "setTimeout"을 사용하고 월 선택기 영역을 클릭하면 2.를 무시합니다.

    monthPicker.ts



    import { MonthPickerOption } from './monthPicker.type';
    
    export class MonthPicker {
    ...
        public constructor(target: HTMLInputElement, option?: MonthPickerOption) {
    ...        
            target.addEventListener('click', _ => this.show());
            window.addEventListener('click', _ => this.hide());
        }
        private addMonthPickerArea(parent: HTMLElement, targetRect: DOMRect) {
    ...
            this.pickerArea.onclick = () => this.setIgnoreHiding();
            parent.appendChild(this.pickerArea);
        }
    ...
        private setIgnoreHiding() {
            if(this.ignoreHiding === true) {
                return;
            }
            this.ignoreHiding = true;
            setTimeout((_) => this.ignoreHiding = false, 500);
        }
        private show() {
            this.selectSelectedMonth();
            this.setIgnoreHiding();
            this.pickerArea.hidden = false;
        }
    ...
        private hide() {
            if(this.ignoreHiding === true) {
                return;
            }
            this.pickerArea.hidden = true;
        }
    ...
    }
    

    선택한 달 가져오기



    "날짜"유형을 사용하여 대상 입력 요소 텍스트를 설정하면 값이 아래와 같기 때문입니다.

    Mon Jul 05 2021
    


    "${date.getFullYear()} ${date.getMonth()}"와 같이 고정된 형식을 사용하는 경우도 있지만 "Moment"를 사용하기로 했습니다.

    monthPicker.type.ts




    export type MonthPickerOption = {
        months?: string[],
        outputFormat?: string,
    }
    


    monthPicker.ts




    import moment from 'moment';
    import { MonthPickerOption } from './monthPicker.type';
    
    export class MonthPicker {
        public constructor(target: HTMLInputElement, option?: MonthPickerOption) {
    ...
            this.addMonthPickerArea(parentElement, target.getBoundingClientRect());
            this.addMonthPickerFrame(this.pickerArea, (option)? option: null);
    ...
        }
    ...
        private addMonthArea(parent: HTMLElement, option: MonthPickerOption|null) {
            const pickerMonthArea = document.createElement('div');
            pickerMonthArea.className = 'monthpicker_month_area';
            parent.appendChild(pickerMonthArea);
    
            const monthRow1 = document.createElement('div');
            monthRow1.className = 'monthpicker_month_row';
            pickerMonthArea.appendChild(monthRow1);
    
            const monthRow2 = document.createElement('div');
            monthRow2.className = 'monthpicker_month_row';
            pickerMonthArea.appendChild(monthRow2);
    
            const format = (option?.outputFormat)? option.outputFormat: null;
            const defaultMonths = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
            const months = (option?.months)? option.months: defaultMonths;
    
            for(let i = 0; i < defaultMonths.length; i++) {
                const pickerMonth = document.createElement('button');
                const month = (months.length > i)? months[i]: defaultMonths[i];
                pickerMonth.textContent = month;
                pickerMonth.className = 'monthpicker_month_input';
                pickerMonth.onclick = (ev) => this.setSelectedDate(ev, month, format);
                if(this.pickerMonths.length < 6) {
                    monthRow1.appendChild(pickerMonth);
                } else {
                    monthRow2.appendChild(pickerMonth);
                }
                this.pickerMonths.push(pickerMonth);
            }
        }
    ...
        private setSelectedDate(ev: MouseEvent, month: string, format: string|null) {
            if(this.inputTarget == null) {
                return;
            }
            const selectedDate = new Date(`${this.currentYear} ${month}`);        
            this.inputTarget.value = moment(selectedDate).format((format == null)? 'YYYY-MM': format);
            this.hide();
        }
    ...
    


    전체 코드



    index.html




    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>Month picker sample</title>
            <meta charset="utf-8">
            <link rel="stylesheet" href="../css/month_picker.css" />
        </head>
        <body>
            <div class="input_area">
                <input type="text" id="month_picker_target">        
            </div>
            <input type="month">
            <script src="js/main.page.js"></script>
        </body>
    </html>
    


    month_picker.css




    .monthpicker_frame {
        border: 1px solid black;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-around;
        z-index: 9999;
        box-shadow: 0 5px 15px -5px rgba(0,0,0,.5);
    
        width: 400px;
        height: 160px;
        background-color: white;
    }
    .monthpicker_year_area {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    
        width: 90%;
        height: 20%;
    }
    .monthpicker_year_move_input {
        background-color: white;
        border: 0;
    }
    .monthpicker_month_area {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-around;
        width: 90%;
        height: 65%;
    
    }
    .monthpicker_month_row {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    
        width: 100%;
        height: 40%;
    }
    .monthpicker_month_input {
        background-color: #f5f5f5;
        border: 0;
        border-radius: 0.4rem;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 16%;
        height: 100%;
        outline: none;
    }
    
    .monthpicker_month_input:hover{
        background-color: #ff8000;
        color: white;
    }
    .monthpicker_month_input:disabled {
        background-color: rgb(51, 170, 255);
        color: white;
    }
    


    main.page.ts




    import { MonthPicker } from "./monthPicker";
    
    export function init() {
        const inputElement = document.getElementById('month_picker_target') as HTMLInputElement;
        const monthPicker = new MonthPicker(inputElement,{
            months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            outputFormat: 'MMMM-YYYY'
        });
    
    }
    init();
    


    monthPicker.ts




    import moment from 'moment';
    import { MonthPickerOption } from './monthPicker.type';
    
    export class MonthPicker {
        private inputTarget: HTMLInputElement|null = null;
        private ignoreHiding: boolean = false;
        private pickerArea: HTMLElement;
        private pickerMonths: HTMLButtonElement[] = [];
    
        private currentYear: number;
        private currentYearElement: HTMLElement;
    
        public constructor(target: HTMLInputElement, option?: MonthPickerOption) {
            this.currentYearElement = document.createElement('div');
            this.currentYear = (new Date()).getFullYear();
            this.pickerArea = document.createElement('div');
            if(target == null) {
                console.error('target was null');
                return;
            }
            this.inputTarget = target;
            const parentElement = (target.parentElement == null)? document.body: target.parentElement;
            this.addMonthPickerArea(parentElement, target.getBoundingClientRect());
            this.addMonthPickerFrame(this.pickerArea, (option)? option: null);
    
            target.addEventListener('click', _ => this.show());
            window.addEventListener('click', _ => this.hide());
        }
        private addMonthPickerArea(parent: HTMLElement, targetRect: DOMRect) {
            this.pickerArea.className = 'monthpicker_area';
            this.pickerArea.style.position = 'absolute';
            this.pickerArea.style.left = `${targetRect.left}px`;
            this.pickerArea.style.top = `${targetRect.top + targetRect.height}px`;
            this.pickerArea.hidden = true;
            this.pickerArea.onclick = () => this.setIgnoreHiding();
            parent.appendChild(this.pickerArea);
        }
        private addMonthPickerFrame(area: HTMLElement, option: MonthPickerOption|null) {
            const pickerFrame = document.createElement('div');
            pickerFrame.className = 'monthpicker_frame';
            area.appendChild(pickerFrame);
    
            this.addYearArea(pickerFrame);
            this.addMonthArea(pickerFrame, option);
        }
        private addYearArea(parent: HTMLElement) {
            const pickerYearArea = document.createElement('div');
            pickerYearArea.className = 'monthpicker_year_area';
            parent.appendChild(pickerYearArea);
    
            const moveBackward = document.createElement('button');
            moveBackward.className = 'monthpicker_year_move_input';
            moveBackward.textContent = '';
            pickerYearArea.appendChild(moveBackward);
            moveBackward.onclick = () => this.changeYear(-1);
    
            this.currentYearElement.className = 'monthpicker_year_current';
            this.currentYearElement.textContent = `${this.currentYear}`;
            pickerYearArea.appendChild(this.currentYearElement);
    
            const moveForward = document.createElement('button');
            moveForward.className = 'monthpicker_year_move_input';
            moveForward.textContent = '';
            pickerYearArea.appendChild(moveForward);
            moveForward.onclick = () => this.changeYear(1);
        }
        private addMonthArea(parent: HTMLElement, option: MonthPickerOption|null) {
            const pickerMonthArea = document.createElement('div');
            pickerMonthArea.className = 'monthpicker_month_area';
            parent.appendChild(pickerMonthArea);
    
            const monthRow1 = document.createElement('div');
            monthRow1.className = 'monthpicker_month_row';
            pickerMonthArea.appendChild(monthRow1);
    
            const monthRow2 = document.createElement('div');
            monthRow2.className = 'monthpicker_month_row';
            pickerMonthArea.appendChild(monthRow2);
    
            const format = (option?.outputFormat)? option.outputFormat: null;
            const defaultMonths = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
            const months = (option?.months)? option.months: defaultMonths;
    
            for(let i = 0; i < defaultMonths.length; i++) {
                const pickerMonth = document.createElement('button');
                const month = (months.length > i)? months[i]: defaultMonths[i];
                pickerMonth.textContent = month;
                pickerMonth.className = 'monthpicker_month_input';
                pickerMonth.onclick = (ev) => this.setSelectedDate(ev, month, format);
                if(this.pickerMonths.length < 6) {
                    monthRow1.appendChild(pickerMonth);
                } else {
                    monthRow2.appendChild(pickerMonth);
                }
                this.pickerMonths.push(pickerMonth);
            }
        }
        private setIgnoreHiding() {
            if(this.ignoreHiding === true) {
                return;
            }
            this.ignoreHiding = true;
            setTimeout((_) => this.ignoreHiding = false, 500);
        }
        private show() {
            this.selectSelectedMonth();
            this.setIgnoreHiding();
            this.pickerArea.hidden = false;
        }
        private getSelectedMonthIndex(currentValue: string): number {
            const currentDate = new Date(currentValue);
            if(currentDate.getFullYear() !== this.currentYear) {
                return -1;
            }
            return currentDate.getMonth();
        }
        private selectSelectedMonth() {
            if(this.inputTarget?.value == null) {
                return;
            }
            const selectedMonthIndex = this.getSelectedMonthIndex(this.inputTarget.value);
            for(let i = 0; i < this.pickerMonths.length; i++) {
                this.pickerMonths[i].disabled =  (i === selectedMonthIndex);
            }
        }
        private hide() {
            if(this.ignoreHiding === true) {
                return;
            }
            this.pickerArea.hidden = true;
        }
        private setSelectedDate(ev: MouseEvent, month: string, format: string|null) {
            if(this.inputTarget == null) {
                return;
            }
            const selectedDate = new Date(`${this.currentYear} ${month}`);        
            this.inputTarget.value = moment(selectedDate).format((format == null)? 'YYYY-MM': format);
            this.hide();
        }
        private changeYear(addYear: number) {
            this.currentYear += addYear;
            this.currentYearElement.textContent = `${this.currentYear}`;
            this.selectSelectedMonth();
        }
    }
    

    좋은 웹페이지 즐겨찾기