Skip to content

Instantly share code, notes, and snippets.

@olehmelnyk
Last active November 25, 2018 15:26
Show Gist options
  • Save olehmelnyk/27eb353bacd28fe32e32cb2a1d6f0030 to your computer and use it in GitHub Desktop.
Save olehmelnyk/27eb353bacd28fe32e32cb2a1d6f0030 to your computer and use it in GitHub Desktop.
Calendar-v4

Calendar-v4

A Pen by Oleh Melnyk on CodePen.

License.

Full page demo

Features

  • OOP Inheritance, MVC code splitting
  • Today's date updates after midnight (00:00)
  • The user can change:
  1. locale (ex. Ukraine, English, Russian...) so month name and week name change;
  2. week day name length (long, short, narrow - ex. Monday / Mon / M);
  3. if Sundays need to be highlighted
  4. year - user can set ANY year - in past or future; Javascript works with years in the range -200000 to + 200000; you can try to set the year to -13.8e9 or 5e9 and see what happens in console ;)
  • when the user makes changes - they are stored in localStorage, so after page reload calendar will use those settings; the user can reset its settings;
<div class="wrapper">
<div class="content">
<div class="calendar" id="calendar"></div>
</div>
<div class="options">
<form action="" method="" class="calendar-options" id="calendar-options">
<div>
<label for="locale">Locale: </label>
<select name="locale" id="locale">
<option value="af">Afrikaans</option>
<option value="sq">Albanian</option>
<option value="am">Amharic</option>
<option value="ar-dz">Arabic - Algeria</option>
<option value="ar-bh">Arabic - Bahrain</option>
<option value="ar-eg">Arabic - Egypt</option>
<option value="ar-iq">Arabic - Iraq</option>
<option value="ar-jo">Arabic - Jordan</option>
<option value="ar-kw">Arabic - Kuwait</option>
<option value="ar-lb">Arabic - Lebanon</option>
<option value="ar-ly">Arabic - Libya</option>
<option value="ar-ma">Arabic - Morocco</option>
<option value="ar-om">Arabic - Oman</option>
<option value="ar-qa">Arabic - Qatar</option>
<option value="ar-sa">Arabic - Saudi Arabia</option>
<option value="ar-sy">Arabic - Syria</option>
<option value="ar-tn">Arabic - Tunisia</option>
<option value="ar-ae">Arabic - United Arab Emirates</option>
<option value="ar-ye">Arabic - Yemen</option>
<option value="hy">Armenian</option>
<option value="as">Assamese</option>
<option value="az">Azeri</option>
<option value="eu">Basque</option>
<option value="be">Belarusian</option>
<option value="bn">Bengali</option>
<option value="bs">Bosnian</option>
<option value="bg">Bulgarian</option>
<option value="my">Burmese</option>
<option value="ca">Catalan</option>
<option value="zh-cn">Chinese - China</option>
<option value="zh-hk">Chinese - Hong Kong SAR</option>
<option value="zh-mo">Chinese - Macau SAR</option>
<option value="zh-sg">Chinese - Singapore</option>
<option value="zh-tw">Chinese - Taiwan</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl-be">Dutch - Belgium</option>
<option value="nl-nl">Dutch - Netherlands</option>
<option value="en-au">English - Australia</option>
<option value="en-bz">English - Belize</option>
<option value="en-ca">English - Canada</option>
<option value="en-cb">English - Caribbean</option>
<option value="en-gb">English - Great Britain</option>
<option value="en-in">English - India</option>
<option value="en-ie">English - Ireland</option>
<option value="en-jm">English - Jamaica</option>
<option value="en-nz">English - New Zealand</option>
<option value="en-ph">English - Phillippines</option>
<option value="en-za">English - Southern Africa</option>
<option value="en-tt">English - Trinidad</option>
<option value="en-us">English - United States</option>
<option value="en">English - Zimbabwe</option>
<option value="et">Estonian</option>
<option value="mk">FYRO Macedonia</option>
<option value="fo">Faroese</option>
<option value="fa">Farsi - Persian</option>
<option value="fi">Finnish</option>
<option value="fr-be">French - Belgium</option>
<option value="fr">French - Cameroon</option>
<option value="fr-ca">French - Canada</option>
<option value="fr">French - Congo</option>
<option value="fr">French - Cote d'Ivoire</option>
<option value="fr-fr">French - France</option>
<option value="fr-lu">French - Luxembourg</option>
<option value="fr">French - Mali</option>
<option value="fr">French - Monaco</option>
<option value="fr">French - Morocco</option>
<option value="fr">French - Senegal</option>
<option value="fr-ch">French - Switzerland</option>
<option value="fr">French - West Indies</option>
<option value="gd-ie">Gaelic - Ireland</option>
<option value="gd">Gaelic - Scotland</option>
<option value="gl">Galician</option>
<option value="ka">Georgian</option>
<option value="de-at">German - Austria</option>
<option value="de-de">German - Germany</option>
<option value="de-li">German - Liechtenstein</option>
<option value="de-luz">German - Luxembourg</option>
<option value="de-ch">German - Switzerland</option>
<option value="el">Greek</option>
<option value="gn">Guarani - Paraguay</option>
<option value="gu">Gujarati</option>
<option value="he">Hebrew</option>
<option value="hi">Hindi</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="id">Indonesian</option>
<option value="it-it">Italian - Italy</option>
<option value="it-ch">Italian - Switzerland</option>
<option value="ja">Japanese</option>
<option value="kn">Kannada</option>
<option value="ks">Kashmiri</option>
<option value="kk">Kazakh</option>
<option value="km">Khmer</option>
<option value="ko">Korean</option>
<option value="lo">Lao</option>
<option value="la">Latin</option>
<option value="lv">Latvian</option>
<option value="lt">Lithuanian</option>
<option value="ms-bn">Malay - Brunei</option>
<option value="ms-my">Malay - Malaysia</option>
<option value="ml">Malayalam</option>
<option value="mt">Maltese</option>
<option value="mi">Maori</option>
<option value="mr">Marathi</option>
<option value="mn">Mongolian</option>
<option value="ne">Nepali</option>
<option value="no-no">Norwegian - Bokml</option>
<option value="no-no">Norwegian - Nynorsk</option>
<option value="or">Oriya</option>
<option value="pl">Polish</option>
<option value="pt-br">Portuguese - Brazil</option>
<option value="pt-pt">Portuguese - Portugal</option>
<option value="pa">Punjabi</option>
<option value="rm">Raeto-Romance</option>
<option value="ro-mo">Romanian - Moldova</option>
<option value="ro">Romanian - Romania</option>
<option value="ru">Russian</option>
<option value="ru-mo">Russian - Moldova</option>
<option value="sa">Sanskrit</option>
<option value="sr-sp">Serbian</option>
<option value="tn">Setsuana</option>
<option value="sd">Sindhi</option>
<option value="si">Sinhala</option>
<option value="sk">Slovak</option>
<option value="sl">Slovenian</option>
<option value="so">Somali</option>
<option value="sb">Sorbian</option>
<option value="es-ar">Spanish - Argentina</option>
<option value="es-bo">Spanish - Bolivia</option>
<option value="es-cl">Spanish - Chile</option>
<option value="es-co">Spanish - Colombia</option>
<option value="es-cr">Spanish - Costa Rica</option>
<option value="es-do">Spanish - Dominican Republic</option>
<option value="es-ec">Spanish - Ecuador</option>
<option value="es-sv">Spanish - El Salvador</option>
<option value="es-gt">Spanish - Guatemala</option>
<option value="es-hn">Spanish - Honduras</option>
<option value="es-mx">Spanish - Mexico</option>
<option value="es-ni">Spanish - Nicaragua</option>
<option value="es-pa">Spanish - Panama</option>
<option value="es-py">Spanish - Paraguay</option>
<option value="es-pe">Spanish - Peru</option>
<option value="es-pr">Spanish - Puerto Rico</option>
<option value="es-es">Spanish - Spain (Traditional)</option>
<option value="es-uy">Spanish - Uruguay</option>
<option value="es-ve">Spanish - Venezuela</option>
<option value="sw">Swahili</option>
<option value="sv-fi">Swedish - Finland</option>
<option value="sv-se">Swedish - Sweden</option>
<option value="tg">Tajik</option>
<option value="ta">Tamil</option>
<option value="tt">Tatar</option>
<option value="te">Telugu</option>
<option value="th">Thai</option>
<option value="bo">Tibetan</option>
<option value="ts">Tsonga</option>
<option value="tr">Turkish</option>
<option value="tk">Turkmen</option>
<option value="uk">Ukrainian</option>
<option value="ur">Urdu</option>
<option value="uz">Uzbek</option>
<option value="vi">Vietnamese</option>
<option value="cy">Welsh</option>
<option value="xh">Xhosa</option>
<option value="yi">Yiddish</option>
<option value="zu">Zulu</option>
</select>
</div>
<div>
<label for="weekdayLength">Week day length: </label>
<select name="weekdayLength" id="weekdayLength">
<option value="short">Short</option>
<option value="narrow">Narrow</option>
<option value="long">Long</option>
</select>
<p>Hint: "Long" option does not look good with this UI</p>
</div>
<div>
<label for="year">Year:</label>
<input type="number" name="year" id="year" min="-13800000000" max="5000000000" maxlength="12" />
<p>Hint: you can set any year - past or future.</p>
<p>JS can work with years in range -200000 to 200000</p>
<p>You can try to enter -13.8e9 or 5e9 and see what happens in console ;)</p>
</div>
<div>
<input type="checkbox" name="weekend" id="weekend">
<label for="weekend">Highlight weekend</label>
</div>
<div>
<button id="reset-user-settings">Reset user settings</button>
</div>
</form>
<footer>
<p class="made-by">
Made with <span class="heart">❤</span> by Oleh Melnyk
</p>
</footer>
</div>
</div>
// class that works with data (dates)
class CalendarModel {
constructor(calendarSettings) {
if (!(calendarSettings instanceof CalendarSettings)) {
throw new TypeError(
`First argument of ${
this.constructor.name
} should be instance of CalendarSettings`
);
}
this.calendarSettings = calendarSettings;
this.params = calendarSettings.params;
// this is today/current date, and we should not modify it (unless current date change after noon)
this.currentDate = new Date();
// this is the date we currently display - it updates once we switch month
this.date = new Date(this.params.initialDate);
/*
There are some countries where week starts on Sun, Mon, Sat or even Fri
This object describes when week should start based on locale
Attention! This list might be incomplete
*/
this.weekStarts = {
// weekday 1
mon: [
"af",
"ar-tn",
"az",
"be",
"bg",
"bm",
"br",
"bs",
"ca",
"cs",
"cv",
"cy",
"da",
"de-at",
"de-ch",
"de",
"el",
"en",
"en-au",
"en-gb",
"en-ie",
"en-nz",
"eo",
"es-do",
"es",
"et",
"eu",
"fi",
"fo",
"fr-ca",
"fr-ch",
"fr",
"fy",
"gd",
"gl",
"gom-latn",
"he",
"hr",
"hu",
"hy-am",
"id",
"is",
"it",
"ja",
"jv",
"ka",
"kk",
"km",
"ko",
"ky",
"lb",
"lo",
"lt",
"lv",
"me",
"mi",
"mk",
"ml",
"mn",
"ms-my",
"ms",
"mt",
"my",
"nb",
"nl-be",
"nl",
"nn",
"pl",
"pt-br",
"pt",
"ro",
"ru",
"sd",
"se",
"si",
"sk",
"sl",
"sq",
"sr-cyrl",
"sr",
"ss",
"sv",
"sw",
"tet",
"tg",
"th",
"tl-ph",
"tlh",
"tr",
"tzl",
"ug-cn",
"uk",
"ur",
"uz-latn",
"uz",
"vi",
"x-pseudo",
"yo",
"zh-cn",
"zh-hk",
"zh-tw",
"en-ca"
],
// weekday 0
sun: [
"ar-dz",
"ar-kw",
"ar-sa",
"bn",
"bo",
"dv",
"en-us",
"en-il",
"es-us",
"gu",
"hi",
"kn",
"mr",
"ne",
"pa-in",
"ta",
"te"
],
// weekday 6
sat: ["ar-ly", "ar-ma", "ar", "fa", "tzm-latn", "tzm"],
// weekday 5
fri: ["mv"]
};
// init
this.init();
}
init() {
this.updateDate();
this.setWeekStartDay();
this.setDayNames();
}
/**
* Checks if argument 'day' is valid day (0-6)
* @param {number} day
* @returns {boolean}
*/
isValidDay(day) {
return (
typeof day === "number" &&
!Number.isNaN(day) &&
Number.isFinite(day) &&
day >= 0 &&
day <= 6
);
}
/**
* Checks if argument 'month' is unmber in correct range (0-11)
* @param {number} month 0-11
* @returns {boolean} if month is number in range between 0 and 11
*/
isValidMonth(month) {
return (
typeof month === "number" &&
!Number.isNaN(month) &&
Number.isFinite(month) &&
month >= 0 &&
month <= 11
);
}
/**
* Checks if argument 'year' is number
* In theory, we might work with past or future years so we don't have any limits here
* @param {number} year
* @returns {boolean} if argument 'year' is number
*/
isValidYear(year) {
return (
typeof year === "number" && !Number.isNaN(year) && Number.isFinite(year)
);
}
/**
* Check if argument 'date' is insatce of Date
* @param {Date} date
*/
isDate(date) {
return date instanceof Date;
}
/**
* Updates year and month values (when we switch prev/next month in UI)
*/
updateDate() {
this.year = this.date.getFullYear();
this.month = this.date.getMonth();
}
updateParams() {
this.params = this.calendarSettings.params;
this.init();
}
/**
* Returns next week date after dayOfWeek
* @param {Date} date
* @param {number} dayOfWeek (0-6)
* @returns {Date}
*/
getNextWeekStart(date = this.date, dayOfWeek = 0) {
if (this.isDate(date) && this.isValidDay(dayOfWeek)) {
const resultDate = new Date(date.getTime());
resultDate.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7);
return resultDate;
}
}
/**
* Returns day of the week (0 to 6)
* @param {number} month number in range between 0 and 11
* @param {number} year any number (is case of past or future years)
* @returns {number} day of the week (0 is Sunday, 1 is Monday, ... 6 is Saturday)
*/
getFirstDayOfTheMonth(month = this.month, year = this.year) {
if (this.isValidMonth(month) && this.isValidYear(year)) {
return new Date(year, month, 1).getDay();
}
}
/**
* Returns month matrix (array of Sundays, Mondays, etc.)
* @param {number} month 0-11
* @param {number} year any number
* @returns {Array} month matrix
* @example
* [
* [4, 11, 18, 25, 2, 9], // sun
* [29, 5, 12, 19, 26, 3], // mon
* [30, 6, 13, 20, 27, 4], // tue
* [31, 7, 14, 21, 28, 5], // wed
* [1, 8, 15, 22, 29, 6], // thu
* [2, 9, 16, 23, 30, 7], // fri
* [3, 10, 17, 24, 1, 8] // sat
* ]
*/
getMonthMatrix(month = this.month, year = this.year) {
if (this.isValidMonth(month) && this.isValidYear(year)) {
const monthStartDay = this.getFirstDayOfTheMonth(); // 0-6 where 0 is Sun, 1 is Mon, etc.
const prevMonthOffset = -monthStartDay + 2; // gets day offset from prev month before the first day of the current month
const prevSixDays = -5;
const offsetDate = new Date(
year,
month,
monthStartDay === 0 ? prevSixDays : prevMonthOffset
);
const mon = [];
const tue = [];
const wed = [];
const thu = [];
const fri = [];
const sat = [];
const sun = [];
for (let i = 1, sixWeeks = 42; i <= sixWeeks; i++) {
const date = offsetDate.getDate();
const day = i % 7; // 0-6 where 0 is Sun, 1 is Mon, etc.
switch (day) {
case 0:
sun.push(date);
break;
case 1:
mon.push(date);
break;
case 2:
tue.push(date);
break;
case 3:
wed.push(date);
break;
case 4:
thu.push(date);
break;
case 5:
fri.push(date);
break;
case 6:
sat.push(date);
break;
}
offsetDate.setDate(date + 1);
}
return [sun, mon, tue, wed, thu, fri, sat];
}
}
/**
* Returns a string with localised month name and year
* @returns {sring}
* @example November 2018
*/
getCalendarMonthAndYear() {
const monthName = this.date.toLocaleString(this.params.locale, {
month: "long"
});
return `${monthName} ${this.year}`;
}
/**
* Sets array of localised short day names
* @example ['нд', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб']
*/
setDayNames() {
const weekStart = this.getNextWeekStart();
const dayNames = [];
for (let i = 0; i < 7; i++) {
dayNames.push(
new Date(this.year, this.month, weekStart.getDate() + i).toLocaleString(
this.params.locale,
{ weekday: this.params.weekdayLength }
)
);
}
this.dayNames = dayNames;
}
/**
* Sets this.date to prev month (when we switch to prev month in UI)
*/
setPrevMonth() {
this.date.setMonth(this.month - 1);
this.updateDate();
}
/**
* Sets this.date to next month (when we switch to next month in UI)
*/
setNextMonth() {
this.date.setMonth(this.month + 1);
this.updateDate();
}
/**
* Sets week start day code (0 is Sun, 1 is Mon, ... 6 is Sat)
* In some countries week starts on Mon, Sun, Sat or even Fri
*/
setWeekStartDay() {
if (this.weekStarts.mon.includes(this.locale)) {
this.weekStartDay = 1;
} else if (this.weekStarts.sun.includes(this.locale)) {
this.weekStartDay = 0;
} else if (this.weekStarts.sat.includes(this.locale)) {
this.weekStartDay = 6;
} else if (this.weekStarts.fri.includes(this.locale)) {
this.weekStartDay = 5;
} else {
this.weekStartDay = 1;
}
}
}
// class that works with UI/DOM
class CalendarView {
constructor(calendarElement, calendarModel) {
if (!(calendarElement instanceof HTMLElement)) {
throw new TypeError(
`First argument of ${
this.constructor.name
} should be instance of HTMLElement.`
);
}
if (!(calendarModel instanceof CalendarModel)) {
throw new TypeError(
`Second argument of ${
this.constructor.name
} should be instance of CalendarModel`
);
}
this.calendar = calendarElement;
this.model = calendarModel;
// init
this.updateUI();
this.updateTodayDayAfterMidnight();
this.selectedDate = new Date();
}
updateUI() {
const matrix = this.model.getMonthMatrix();
const month = this.createElement("div", { class: "month" });
// calendar header
const header = this.createElement("header", { class: "month-header" });
const h3 = this.createElement(
"h3",
{ class: "month-name" },
{ textContent: this.model.getCalendarMonthAndYear() },
header
);
const prev = this.createElement(
"button",
{ class: "prev-month" },
{ textContent: "➜" },
header
);
const next = this.createElement(
"button",
{ class: "next-month" },
{ textContent: "➜" },
header
);
month.appendChild(header);
// calendar days
const days = this.createElement("div", { class: "days" }, null, month);
const dayClassNames = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
for (let week = 0; week < 7; week++) {
const dayRow = this.createElement(
"div",
{ class: dayClassNames[week] },
null,
days
);
if (this.model.params.highlightSundays && week === 0) {
dayRow.classList.add("weekend");
}
const dayName = this.createElement(
"div",
{ class: "day-name" },
{ textContent: this.model.dayNames[week] },
dayRow
);
for (let day = 0; matrix[day + 1]; day++) {
const newDay = this.createElement("div", null, {
textContent: matrix[week][day]
});
newDay.classList.add("day");
if (
(day < 3 && matrix[week][day] > 23) ||
(day > 3 && matrix[week][day] < 23)
) {
newDay.classList.add("day-dummy");
newDay.classList.remove("day");
} else if (
this.model.year === this.model.currentDate.getFullYear() &&
this.model.month === this.model.currentDate.getMonth() &&
matrix[week][day] === this.model.date.getDate()
) {
newDay.classList.add("day-today");
}
dayRow.appendChild(newDay);
}
}
this.calendar.innerHTML = "";
this.calendar.appendChild(month);
// switch to prev month
prev.onclick = () => {
this.model.setPrevMonth();
this.updateUI();
};
// switch to next month
next.onclick = () => {
this.model.setNextMonth();
this.updateUI();
};
// select day on click
month.onclick = event => {
const classList = event.target.classList;
if (classList.contains("day")) {
if (!this.selectMultipleDays) {
[...month.querySelectorAll(".day-selected")].forEach(el =>
el.classList.remove("day-selected")
);
}
classList.toggle("day-selected");
}
};
}
/**
* Creates HTMLElement
* @param {string} tag name
* @param {object} attributes for tag element
* @param {object} properties for tag element
* @param {HTMLElement} append tag element to provided HTMLElement
* @example this.createElement('div', {class: 'myClassName', id: 'myID'}, {innerHTML: '<b>hello</b>'}, document.body)
*/
createElement(tag = "div", attributes, properties, append) {
const element = document.createElement(tag);
if (
attributes &&
typeof attributes === "object" &&
Object.keys(attributes).length
) {
for (let attribute in attributes) {
if (attributes.hasOwnProperty(attribute)) {
element.setAttribute(attribute, attributes[attribute]);
}
}
}
if (
properties &&
typeof properties === "object" &&
Object.keys(properties).length
) {
for (let property in properties) {
if (properties.hasOwnProperty(property)) {
element[property] = properties[property];
}
}
}
if (append && append instanceof HTMLElement) {
append.appendChild(element);
}
return element;
}
/**
* Updates today date after midnight (00:00) to highlight next day
* and set's inteval to update on next days
*/
updateTodayDayAfterMidnight() {
const today = new Date();
const tomorrow = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 1,
0,
0,
0
);
setTimeout(() => {
this.model.date = new Date();
this.model.updateDate();
this.updateUI();
const nextDay = 86400000;
setInterval(() => {
this.model.date = new Date();
this.model.updateDate();
this.updateUI();
}, nextDay);
}, tomorrow - Date.now());
}
/**
* Returns selected day or today date
*/
getSelectedDate() {
const selectedDay = this.calendar.querySelector(".day-selected");
return selectedDay
? new Date(this.model.currentDate.setDate(selectedDay.textContent))
: new Date();
}
}
// advdanced/additinal calendar features
class AdvdancedCalendarModel extends CalendarModel {
constructor(...props) {
super(...props);
}
/**
* Check if argument 'locale' is a valid (exists)
* @param {string} locale
* @return {boolean}
*/
isValidLocale(locale) {
const _ = this.weekStarts;
const locales = [].concat(_.mon, _.sun, _.sat, _.fri);
return typeof locale === "string" && locales.includes(locale);
}
/**
* Sets locale to display month name, start week from Monday or Sunday, etc.
* @param {string} locale
*/
setLocale(locale) {
if (this.isValidLocale(locale)) {
this.params.locale = locale.toLowerCase();
this.setWeekStartDay();
this.setDayNames();
this.calendarSettings.setLocale(locale);
}
}
/**
* Sets year
* You can set any year in past or future (at least in range -200000 to + 200000)
* @param {number} year
*/
setYear(year) {
if (this.isValidYear(year)) {
const BIG_BANG = -13.8e9;
const SUN_TURN_INTO_A_RED_STAR = 5e9;
if (year <= BIG_BANG) {
return console.info("There was no time and dates before the Big Bang!");
}
if (year >= SUN_TURN_INTO_A_RED_STAR) {
return console.info(
"Our sun became a giant red star. There is no life on planet Earth, so forget about Earth dates..."
);
}
if (year > 2e5 || year < -2e5) {
// JS can't create/calculate dates years before or after 2e5, lol
return console.info(
"JS can calculate dates only in range of [-200000 - +200000]"
);
}
this.year = year;
this.date = new Date(year, this.month);
if (new Date().getFullYear() === year) {
this.date = new Date();
} else {
// JS has a bug (or feature?) - new Date(0) -> 1970 and new Date(0, 0) -> 1900, so we need to setFullYear
this.date = new Date(this.date.setFullYear(year));
}
this.updateDate();
}
}
}
// class that works with default/user settings
class CalendarSettings {
constructor(customParams = {}) {
if (typeof customParams !== "object") {
throw new TypeError(
`First argument for ${
this.constructor.name
} should be object with params.`
);
}
this.defaults = {
locale: navigator.language,
initialDate: new Date(),
weekdayLength: "short",
highlightSundays: true
};
// the puruse of defaults is that we can roll back to default settings any time, so don't change it...
Object.freeze(this.defaults);
this.customParams = customParams; // params passed to constructor
Object.freeze(this.customParams); // don't modify customParams
// stored in localStorage, and yes - we update it when user change settings via console or UI
this.userSettings = {};
this.getStorageSettings(); // init user settings - load from local storage
// merge multile sources of params into single object <- this object params will be used elsewhere
this.params = {};
this.updateParams();
}
/**
* Reads user settings for calendar from local storage and set this.userSettigs
*/
getStorageSettings() {
if (localStorage) {
const storageString = localStorage.getItem("calendar");
this.userSettings =
storageString !== null ? JSON.parse(storageString) : {};
}
}
/**
* Writes user settings for calendar from this.userSettings to local storage
*/
updateStorageSettings() {
if (localStorage && Object.keys(this.userSettings).length) {
if (Object.keys(this.userSettings).length) {
localStorage.setItem("calendar", JSON.stringify(this.userSettings));
} else {
this.clearStorageSettings();
}
}
}
/**
* Clears user settings from local storage and set userSettings to empty object
*/
clearStorageSettings() {
if (localStorage) {
localStorage.removeItem("calendar");
this.userSettings = {};
}
}
/**
* Resets this.params to defaults
*/
resetToDefaults() {
this.clearStorageSettings();
this.updateParams();
}
/**
* Resets this.params to mix of defaults and customParams (ommiting user settings),
* if there were any customParams - othervise reset to defaults
*/
resetToCustromParams() {
this.clearStorageSettings();
if (Object.keys(this.customParams).length) {
this.params = Object.assign({}, this.defaults, this.customParams);
} else {
this.resetToDefaults();
}
this.updateParams();
}
/**
* Updates this.params by re-merging defaults, custromParams (from constructor), and user settings
*/
updateParams() {
this.params = Object.assign(
{},
this.defaults,
this.customParams,
this.userSettings
);
}
/**
* Updates user settings in class and local storage
* @param {object} object
*/
updateUserSetting(object) {
if (typeof object === "object") {
this.userSettings = Object.assign({}, this.userSettings, object);
this.updateStorageSettings();
this.updateParams();
}
}
/**
* Stores locale into user settings
* @param {string} locale
*/
setLocale(locale) {
if (typeof locale === "string") {
this.updateUserSetting({ locale });
}
}
/**
* Stores weekdayLength in user settings
* @param {string} weekdayLength
*/
setWeekDayLength(weekdayLength) {
if (
typeof weekdayLength === "string" &&
["narrow", "short", "long"].includes(weekdayLength)
) {
this.updateUserSetting({ weekdayLength });
}
}
/**
* Stores highlightSundays in user settings
* @param {boolean} bool
*/
setHighlightSundays(bool) {
this.updateUserSetting({ highlightSundays: !!bool });
}
/**
* Stores initialDate in user settings
* @param {Date} date
*/
setInitialDate(date) {
if (date instanceof Date) {
this.updateUserSetting({ initialDate: date });
}
}
}
// initialization
const calendarTag = document.getElementById("calendar");
const calendarSettings = new CalendarSettings();
const calendarModel = new AdvdancedCalendarModel(calendarSettings);
const myCalendar = new CalendarView(calendarTag, calendarModel);
/*
View has access to Model, and
Model has access to Settngs,
so we have access to Settings from View
*/
class CalendarOptions {
constructor(calendarView, form) {
if (!calendarView instanceof CalendarView) {
throw new ReferenceError(
`First argument for ${
this.constructor.name
} should be instance of CalendarView.`
);
}
if (!form instanceof HTMLFormElement) {
throw new ReferenceError(
`First argument for ${
this.constructor.name
} should be instance of HTMLForm.`
);
}
this.form = form;
this.view = calendarView;
this.model = calendarView.model;
this.settings = calendarView.model.calendarSettings;
this.locale = form.querySelector("#locale");
this.locale.onchange = () => {
this.settings.setLocale(this.locale.value);
this.model.updateParams();
this.view.updateUI();
};
this.weekdayLength = form.querySelector("#weekdayLength");
this.weekdayLength.onchange = () => {
this.settings.setWeekDayLength(this.weekdayLength.value);
this.model.updateParams();
this.model.setDayNames();
this.view.updateUI();
};
this.year = form.querySelector("#year");
this.year.oninput = () => {
const year = +this.year.value;
const newYear = new Date(new Date().setFullYear(year));
/**
* sometimes year is set the wrong way, when it sets 1920 instead of 20
*/
this.model.setYear(year);
this.settings.setInitialDate(newYear);
this.view.updateUI();
};
this.weekend = form.querySelector("#weekend");
this.weekend.onchange = () => {
this.settings.setHighlightSundays(this.weekend.checked);
this.model.updateParams();
this.view.updateUI();
};
this.reset = form.querySelector("#reset-user-settings");
this.reset.onclick = () => {
this.settings.resetToDefaults();
this.view.updateUI();
};
window.onload = () => {
document.title = new Date().toLocaleString(navigator.language, {
year: "numeric",
month: "long",
day: "numeric"
});
this.locale.value = this.settings.params.locale;
this.weekdayLength.value = this.settings.params.weekdayLength;
this.year.value = this.model.year;
this.weekend.checked = this.settings.params.highlightSundays;
};
}
}
const optionsForm = document.getElementById("calendar-options");
myCalendarOptions = new CalendarOptions(myCalendar, optionsForm);
/*
main.css
*/
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
font-family: sans-serif;
background: rgba(0, 0, 0, 0.1);
}
.content {
width: 100%;
height: 100%;
}
.wrapper {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
/*
Options
*/
.options {
display: flex;
flex-direction: column;
height: 100%;
width: 400px;
background: #fff;
padding: 20px;
}
.calendar-options {
flex: 1;
}
.calendar-options label {
margin-bottom: 5px;
}
.calendar-options div {
margin: 10px 0;
}
.calendar-options button {
background: #f00;
color: #fff;
border: none;
outline: none;
padding: 5px 10px;
border-radius: 10em;
cursor: pointer;
}
.calendar-options button:hover {
opacity: 0.5;
}
.calendar-options p {
font-size: 13px;
font-style: italic;
color: #999;
padding-top: 5px;
}
.calendar-options div:not(:last-child) {
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
margin-bottom: 10px;
}
/*
Footer
*/
footer {
bottom: 20px;
width: 100%;
color: #ccc;
overflow: hidden;
text-align: center;
cursor: pointer;
user-select: none;
}
.made-by:hover .heart {
color: #f00;
animation: beat 0.25s infinite alternate;
transform-origin: center;
}
.heart {
display: inline-block;
padding: 0 3px;
}
/* Heart beat animation */
@keyframes beat {
to {
transform: scale(1.4);
}
}
/*
calendar.css
*/
.calendar {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
user-select: none;
transition: all 0.5s ease-in-out;
}
.month {
width: 400px;
height: 400px;
background: #fff;
overflow: hidden;
border-radius: 40px;
box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.1);
}
.month-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 50px;
padding-bottom: 10px;
background: #f00;
color: #fff;
height: 80px;
}
.month-name,
.day-name {
text-transform: capitalize;
overflow: hidden;
text-overflow: ellipsis;
}
.prev-month,
.next-month {
width: 30px;
height: 30px;
color: #fff;
background: none;
border: none;
outline: none;
margin: 0 10px 5px;
font-size: 1.4em;
}
.prev-month:hover,
.next-month:hover {
cursor: pointer;
opacity: 0.7;
}
.prev-month {
order: -1;
/*
use scale instead of rotate beacuse arrow symbol is not perfectly centerd
so we actually mirror-flipped arrow symbol instaed of rotation
*/
transform: scale(-1, 1);
}
.days {
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 50px 10px;
}
.day,
.day-dummy,
.day-name {
display: flex;
justify-content: center;
align-items: center;
width: 35px;
height: 35px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1);
margin: 7px;
}
.day:hover {
transform: scale(1.2);
opacity: 0.5;
cursor: pointer;
}
.day-name {
font-weight: bold;
border-color: transparent;
overflow: visible;
text-overflow: ellipsis;
}
.day-today {
background: #f00;
color: #fff;
}
.day-dummy {
opacity: 0.4;
border-color: transparent;
}
.day-dummy:hover {
cursor: not-allowed;
}
.day-selected {
background: #eee;
}
.weekend {
color: #f00;
}
.sun {
order: 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment