calendar

Updated:

FinalProject에서 calendar 기능을 구현할 때 https://codepen.io/peanav/pen/ulkof 를 가져와서 구현했다.

!function() {
  // !function은 자기호출 익명함수이다.
  // 함수가 만들어지자 마자 즉시 실행되며, 반환하는 것의 반대 값을 반환한다.(!반환값)
  // 실제 반한되는 값을 얻으려면 !function(){}()이 아닌 (function(){})();을 써야한다.
  // !function(){}('이 부분을 통해 매개변수를 받는다.')

  var today = moment();
  // moment.js는 자바 스크립트에서 dates를 다루기 위한 Swiss Army knife이다.
  // 깔끔하고 간결한 API를 이용해서 날짜와 시간을 분석, 검증, 조작, 표시할 수 있다.
  // <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"> 
  // </script>를 추가하면 사용할 수 있다.

  function Calendar(selector, events) {
    // selector : #calendar
    // events : data(event가 저장되어 있다.)
    this.el = document.querySelector(selector);
    // new 키워드를 통해 생성자 함수를 만들었기 때문에 이 경우 this는 window가 아니라 빈 객체이고,
    // this.el => 빈 객체에 el이라는 속성을 추가한다.
    // (strict mode에서는 this에 디폴트 바인딩이 없기 때문에 undefined에러 발생)
    this.events = events;
    // events들을 this의 속성으로 추가한다.
    this.current = moment().date(1);
    // Sun Aug 01 2021 13:06:39 GMT+0900 현재 month에서 1일
    this.draw(); // Calendar.prototype.draw 호출
    var current = document.querySelector('.today');
    if(current) {
      var self = this;
      
      window.setTimeout(function() {
        self.openDay(current);
      }, 500);
    }
  }

  Calendar.prototype.draw = function() {
  // Function.prototype 속성 : 이전에 정의된 객체 타입에 공유 속성을 추가한다.
  // 이것은 객체 타입의 인스턴스 하나에만 적용되는 것이 아니라 이 함수로 생성하는 모든 객체와 공유한다.
    //Create Header
    this.drawHeader();

    //Draw Month
    this.drawMonth();

    this.drawLegend();
  }

  Calendar.prototype.drawHeader = function() {
    var self = this;
    // this => Calendar {el: div#calendar, events: Array(16), current: f}
    // Calendar 함수에서 this.el ... 으로 추가한 속성들이 들어가있음
    if(!this.header) {
      // this(Calendar)에 header라는 속성이 없다면 if문 실행
      // Create the header elements
      this.header = createElement('div', 'header');
      // 기본적으로 새로운 요소를 추가하려면, 
      // this.header = document.createElement('div');
      // this.header.className = 'header';
      // 위와 같은 방법으로 추가 가능하지만 새롭게 createElement 함수를 생성하여
      // function createElement(tagName, className, innerText)
      // 간단하게 요소를 추가하였다.
     
      this.header.className = 'header';
      // header의 className을 header로 지정한다.

      this.title = createElement('h1');
      // this.title에 'h1'태그를 담는다. <h1></h1>

      var right = createElement('div', 'right');
      // <div class="right"></div>
      right.addEventListener('click', function() { self.nextMonth(); });
      // class name="right"인 요소를 클릭하면, Calendar.prototype.nextMonth 실행

      var left = createElement('div', 'left');
      // <div class="left"></div>
      left.addEventListener('click', function() { self.prevMonth(); });
      // class name="left"인 요소를 클릭하면, Calendar.prototype.prevMonth 실행
        
      //Append the Elements
      this.header.appendChild(this.title);
      // header 요소에 title요소 추가
      this.header.appendChild(right);
      // header 요소에 right요소 추가
      this.header.appendChild(left);
      // header 요소에 left요소 추가
      this.el.appendChild(this.header);
      // this.el(#calendar) 요소에 header 요소 추가
    }

    this.title.innerHTML = this.current.format('MMMM YYYY');
    // title의 html = this.current(현재 월의 1일).format(형식지정 => 'MMMM YYYYY');
  }

  Calendar.prototype.drawMonth = function() {
    var self = this;
    
    this.events.forEach(function(ev) {
     ev.date = self.current.clone().date(Math.random() * (29 - 1) + 1);
    });
    // events Array에 담겨져 있는 수 만큼 반복실행하며
    // 반복실행될 때마다 ev.date => events에 담겨진 각각의 event들의 
    // self.current.clone() => current를 복사한 것에 date(Math.random() * (29 - 1) + 1);
    // Math.random() 실행시켜서 1일 ~ 29일 중 랜덤한 일자에 event를 넣는다.
      
    // 예) ev => ev {eventName: "Lunch Meeting w/ Mark", calendar: "Work", color: "orange"}
   	// ev.date => Fri Aug 27 2021 12:50:21 GMT+0900 (한국 표준시)
    // 8월 27일에 ev가 들어간다.
      
    
    if(this.month) {
      // this.month라는 변수가 true면 아래 if문 실행
      this.oldMonth = this.month;
      this.oldMonth.className = 'month out ' + (self.next ? 'next' : 'prev');
      this.oldMonth.addEventListener('webkitAnimationEnd', function() {
        self.oldMonth.parentNode.removeChild(self.oldMonth);
        // oaddEventListener function}{ ... } 안에서 this는 이 함수 밖 this와 다르다.
        // 우리가 원하는 this는 함수 밖 this이기 때문에 this를 self 변수에 담아서 함수 안에서 사용한다.                                     
        self.month = createElement('div', 'month');
        // self.month = <div class='month'></div>
        self.backFill();
        // Calendar.prototype.backFill 호출
        self.currentMonth();
        // Calendar.prototype.currentMonth 호출
        self.fowardFill();
        // Calendar.prototype.fowardFill 호출
        self.el.appendChild(self.month);
        // self(this).el의 자식요소로 self(this).month요소를 추가
        window.setTimeout(function() {
          self.month.className = 'month in ' + (self.next ? 'next' : 'prev');
        }, 16);
		// self(this).month.className을 month in + next or prev로 설정(16/1000);
      });
    } else {
       	// this.month라는 변수가 false면 아래 else문 실행
        this.month = createElement('div', 'month');
        // this.month 변수에 <div class="month"></div>를 담았다.
        this.el.appendChild(this.month);
        // this.el.appendChild(this.month); this.el의 자식요소로 this.month를 설정한다.
        this.backFill();
        // Calendar.prototype.backFill 속성 호출 
        this.currentMonth();
        // Calendar.prototype.currentMonth 호출
        this.fowardFill();
        // Calendar.prototype.fowardFill 호출
        this.month.className = 'month new';
        // backFill(), currentMonth(), forwardFill() 를 호출한 후에 month였던 className을 month new로 변경
    }
  }

  Calendar.prototype.backFill = function() { 
    var clone = this.current.clone();
    // clone 변수에 this.current(월의 1일 ex)2021-09-01)
    var dayOfWeek = clone.day();
    // dayOfWeek 변수에 clone(월의 1일).day() => .day()는 요일의 수 => 일(0), 월(1), 화(2), 수(3), 목(4), 금(5), 토(6)
    if(!dayOfWeek) { return; }
    // dayOfWeek이 있으면 아무것도 하지 않는다.
    clone.subtract('days', dayOfWeek+1);
    // clone(ex.2021-09-01).substract() => 해당 일에서 dayOfweek의 수만큼 'days' 일 단위로 뺀다.

    for(var i = dayOfWeek; i > 0 ; i--) {
      this.drawDay(clone.add('days', 1));
      // 예) 2021-09-01에서 dayOfWeek은 3
      // Calendar.prototype.drawDay를 호출하며
      // clone.add('days', 1) => dayOfWeek 수만큼 'days' 일 단위로 더한다.
    }
  }

  Calendar.prototype.fowardFill = function() {
    var clone = this.current.clone().add('months', 1).subtract('days', 1);
    // clone = this.current.clone() -> 현재 월의 1일 .add('months', 1) -> 다음 월의 1일
    // .subtract('days', 1) -> 다음 월의 1일에서 하루를 빼면 현재 월의 마지막 날
    var dayOfWeek = clone.day();
    // dayOfWeek = clone.day() => 현재 월의 마지막 날의 요일(0~6)

    if(dayOfWeek === 6) { return; }
    // dayOfWeek이 6이면 토요일, 한 주가 끝난 것이기 때문에 아무것도 하지 않고 리턴

    for(var i = dayOfWeek; i < 6 ; i++) {
      this.drawDay(clone.add('days', 1));
      // 만약 dayOfWeek이 6이 아닐경우 6이 될 때까지 days를 추가
      // 예를 들어 8월인 경우 31일 화요일이 마지막 날이다.
      // 이렇게 끝나면 너무 허전하니 9월 1일, 2일, 3일, 4일 토요일까지
      // 보여주게 하는 것이 forwardFill이다.
    }
  }

  Calendar.prototype.currentMonth = function() {
    var clone = this.current.clone();

    while(clone.month() === this.current.month()) {
      // clone의 월과 this.current의 월이 달라질 때 False
      this.drawDay(clone);
      clone.add('days', 1);
      // 다음 달로 넘어갈 때까지 하루씩 증가
    }
  }

  Calendar.prototype.getWeek = function(day) {
    if(!this.week || day.day() === 0) {
      // this.week이 존재하지 않거나 or day.day() -> 인자 값의 요일 값이 0 이면 실행
      this.week = createElement('div', 'week');
      // this.week = <div className='week'></div>
      this.month.appendChild(this.week);
      // this.month의 자식 요소로 추가
    }
  }

  Calendar.prototype.drawDay = function(day) {
    
    var self = this;
    this.getWeek(day);

    //Outer Day
    var outer = createElement('div', this.getDayClass(day));
    // outer = <div className='this.getDayclass(day)로 받은 리턴값'></div>
    outer.addEventListener('click', function() { 
      self.openDay(this);
      // this => detail in
    });

    //Day Name
    var name = createElement('div', 'day-name', day.format('ddd'));
    // name = <div className='day-name'>day.format('ddd')<div>
    // 여기서 day.format은 형식을 지정하는 것으로, 월요일이면 MON 화요일이면 TUE

    //Day Number
    var number = createElement('div', 'day-number', day.format('DD'));
    // number = <div className='day-number'>day.format('DD')</div>
    // day.format('DD') -> 31일이면 31
 
    //Events
    var events = createElement('div', 'day-events');
    // events = <div className='day-events'></div>
    this.drawEvents(day, events);
    // Calendar.prototype.drawEvents 호출

    outer.appendChild(name);
    // day 요소에 자식요소로 추가
    outer.appendChild(number);
    // day 요소에 자식요소로 추가
    outer.appendChild(events);
    // day 요소에 자식요소로 추가
    this.week.appendChild(outer);
    // day 요소를 this.week 의 자식 요소로 추가(week 클래스 요소)
  }

  Calendar.prototype.drawEvents = function(day, element) {
    if(day.month() === this.current.month()) {
      // day.month() 와 this.current.month()가 같으면 실행
      var todaysEvents = this.events.reduce(function(memo, ev) {
        // todaysEvents 변수에 this.events.reduce() 함수 실행
        if(ev.date.isSame(day, 'day')) {
          // isSame은 momentjs에서 사용하는 함수로,
          // 예) console.log(moment('2010-10-20').isSame('2010-10-21')); // false
          // console.log(moment('2009-10-20').isSame('2010-10-21', 'year')); // false
            
          memo.push(ev);
         // memo.push(ev); 입력
         // ev.date가 인자값인 day와 같으면 if문이 실행되고,
         // memo라는 배열 변수에 해당 event가 들어간다.
        }
        return memo;
        // 이런 형태로 리턴이 되면 todaysEvents 배열 변수 안에 memo가 담아진다
          
        // 예) : day - Wed Sep 01 2021 18:31:40 GMT+0900 (한국 표준시)
        // ev - {eventName: "Parent/Teacher Conference", ... date: f}
        // memo - 0: {eventName: "Parent/Teacher Conference", ... date: f}
		//        length: 1
        // todaysEvents - 0: {eventName: "Parent/Teacher Conference", ... date: f}
		//        		  length: 1

      }, []);
        

      todaysEvents.forEach(function(ev) {
        var evSpan = createElement('span', ev.color);
        // evSpan = <span className='ev.color'></span>
        // ev.color는 랜덤으로 부여하고, className에 따라 색 부여
        element.appendChild(evSpan);
      });
    }
  }

  Calendar.prototype.getDayClass = function(day) {
    classes = ['day'];
    if(day.month() !== this.current.month()) {
      classes.push('other');
      // 전 월 또는 다음 월이면 day other
    } else if (today.isSame(day, 'day')) {
      // 오늘이면 day today
      classes.push('today');
    }
    return classes.join(' ');
  }

  Calendar.prototype.openDay = function(el) {
    var details, arrow;
    var dayNumber = +el.querySelectorAll('.day-number')[0].innerText || +el.querySelectorAll('.day-number')[0].textContent;
    // dayNumber => 선택한 Day의 일수, 처음 실행했을땐 day today 값이 저장
    var day = this.current.clone().date(dayNumber);
    // day => 현재 시각 예) _d: Tue Aug 31 2021 20:45:32 GMT+0900 (한국 표준시) {}

    var currentOpened = document.querySelector('.details');
    // currentOpened 변수에 'details'의 클래스 이름을 가진 요소를 담음

    //Check to see if there is an open detais box on the current row
    if(currentOpened && currentOpened.parentNode === el.parentNode) {
      // currentOpend and currentOpened의 부모 노드가
      // el의 부모 노트와 같으면 if문 실행
      // 부모 노드는 <div class="week">...</div>
      details = currentOpened;
      // details 변수에 currentOpend를 담는다.
      // <div class='details in'>...</div>
      arrow = document.querySelector('.arrow');
    } else {
      //Close the open events on differnt week row
      //currentOpened && currentOpened.parentNode.removeChild(currentOpened);
      // 아래 조건문은 event를 닫는 구문
      if(currentOpened) {
        currentOpened.addEventListener('webkitAnimationEnd', function() {
          currentOpened.parentNode.removeChild(currentOpened);
        });
        currentOpened.addEventListener('oanimationend', function() {
          currentOpened.parentNode.removeChild(currentOpened);
        });
        currentOpened.addEventListener('msAnimationEnd', function() {
          currentOpened.parentNode.removeChild(currentOpened);
        });
        currentOpened.addEventListener('animationend', function() {
          currentOpened.parentNode.removeChild(currentOpened);
        });
        currentOpened.className = 'details out';
        // details in이었던 className을 details out로 변경
      }

      //Create the Details Container
      details = createElement('div', 'details in');
      

      //Create the arrow
      var arrow = createElement('div', 'arrow');

      //Create the event wrapper

      details.appendChild(arrow);
      el.parentNode.appendChild(details);
    }

    var todaysEvents = this.events.reduce(function(memo, ev) {
      if(ev.date.isSame(day, 'day')) {
        // ev.date() => 이벤트 날짜와 isSame(day, 'day') day의 'day'=> 날짜가 같으면 실행
        memo.push(ev);
        // memo 배열 변수에 ev를 push
      }
      return memo;
      // todaysEvents가 리턴된 memo 값을 가진다.
    }, []);

    this.renderEvents(todaysEvents, details);
    // todaysEvents : 해당(선택된) 일의 events를 담고 있다.
    // 예) (2) [{…}, {…}]
	// 0: {eventName: "School Play", calendar: "Kids", color: "yellow", date: f}
	// 1: {eventName: "Ice Cream Night", calendar: "Kids", color: "yellow", date: f}

    // details : <div class="details in">...</div>

    arrow.style.left = el.offsetLeft - el.parentNode.offsetLeft + 27 + 'px';
  }

  Calendar.prototype.renderEvents = function(events, ele) {
    //Remove any events in the current details element : 현재 세부 정보 요소에서 이벤트 제거
    var currentWrapper = ele.querySelector('.events');
    // var currentWrapper 변수에 ele.querySelector('.events');
    var wrapper = createElement('div', 'events in' + (currentWrapper ? ' new' : ''));
    // wrapper = <div className='events in + new or '' '>

    events.forEach(function(ev) {
      // events에 들어있는 ev를 하나씩 꺼내서,
      var div = createElement('div', 'event');
      var square = createElement('div', 'event-category ' + ev.color);
      var span = createElement('span', '', ev.eventName);

      div.appendChild(square);
      div.appendChild(span);
      wrapper.appendChild(div);
     
     // 를 추가한다.
    });

    if(!events.length) {
      // events.length가 0일때
      var div = createElement('div', 'event empty');
      // div = <div className='event empty'></div>
      var span = createElement('span', '', 'No Events');
      // span = <span>No Events</span>

      div.appendChild(span);
      wrapper.appendChild(div);
    }

    if(currentWrapper) {
      // 현재 event 페이지
      currentWrapper.className = 'events out';
      // className을 event out으로 변경
      currentWrapper.addEventListener('webkitAnimationEnd', function() {
      // detail in event 애니메이션 종료
        currentWrapper.parentNode.removeChild(currentWrapper);
        ele.appendChild(wrapper);
      });
      currentWrapper.addEventListener('oanimationend', function() {
        currentWrapper.parentNode.removeChild(currentWrapper);
        ele.appendChild(wrapper);
      });
      currentWrapper.addEventListener('msAnimationEnd', function() {
        currentWrapper.parentNode.removeChild(currentWrapper);
        ele.appendChild(wrapper);
      });
      currentWrapper.addEventListener('animationend', function() {
        currentWrapper.parentNode.removeChild(currentWrapper);
        ele.appendChild(wrapper);
      });
    } else {
      ele.appendChild(wrapper);
    }
  }

  Calendar.prototype.drawLegend = function() {
    var legend = createElement('div', 'legend');
    // legend = <div className='legend'></div>
    var calendars = this.events.map(function(e) {
      return e.calendar + '|' + e.color;
    }).reduce(function(memo, e) {
      if(memo.indexOf(e) === -1) {
        memo.push(e);
      }
      return memo;
    }, []).forEach(function(e) {
      var parts = e.split('|');
      var entry = createElement('span', 'entry ' +  parts[1], parts[0]);
      legend.appendChild(entry);
    });
    this.el.appendChild(legend);
  }

  Calendar.prototype.nextMonth = function() {
    // 다음 월
    this.current.add('months', 1);
    this.next = true;
    this.draw();
  }

  Calendar.prototype.prevMonth = function() {
    // 전 월
    this.current.subtract('months', 1);
    this.next = false;
    this.draw();
  }

  window.Calendar = Calendar;
  // Calendar 함수를 window 객체의 전역 함수로 지정한다.
  // 이 과정을 거쳐야 !function(){}(); 밖에서도 Calendar 함수를 사용할 수 있다.
  // 전역 변수 및 함수 = window 객체

  function createElement(tagName, className, innerText) {
    var ele = document.createElement(tagName);
    if(className) {
      ele.className = className;
    }
    if(innerText) {
      ele.innderText = ele.textContent = innerText;
    }
    return ele;
  }
}();

!function() {
  var data = [
    { eventName: 'Lunch Meeting w/ Mark', calendar: 'Work', color: 'orange' },
    { eventName: 'Interview - Jr. Web Developer', calendar: 'Work', color: 'orange' },
    { eventName: 'Demo New App to the Board', calendar: 'Work', color: 'orange' },
    { eventName: 'Dinner w/ Marketing', calendar: 'Work', color: 'orange' },

    { eventName: 'Game vs Portalnd', calendar: 'Sports', color: 'blue' },
    { eventName: 'Game vs Houston', calendar: 'Sports', color: 'blue' },
    { eventName: 'Game vs Denver', calendar: 'Sports', color: 'blue' },
    { eventName: 'Game vs San Degio', calendar: 'Sports', color: 'blue' },

    { eventName: 'School Play', calendar: 'Kids', color: 'yellow' },
    { eventName: 'Parent/Teacher Conference', calendar: 'Kids', color: 'yellow' },
    { eventName: 'Pick up from Soccer Practice', calendar: 'Kids', color: 'yellow' },
    { eventName: 'Ice Cream Night', calendar: 'Kids', color: 'yellow' },

    { eventName: 'Free Tamale Night', calendar: 'Other', color: 'green' },
    { eventName: 'Bowling Team', calendar: 'Other', color: 'green' },
    { eventName: 'Teach Kids to Code', calendar: 'Other', color: 'green' },
    { eventName: 'Startup Weekend', calendar: 'Other', color: 'green' }
  ];

  

  function addDate(ev) {
    
  }

  var calendar = new Calendar('#calendar', data);
  // new operator : new 연산자는 사용자 정의 객체 타입 또는 내장 객체 타입의 인스턴스를 생성한다.

}();

Leave a comment