diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f32a089 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.{js,html,css}] +charset = utf-8 +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true diff --git a/Changelog.md b/Changelog.md index f2336f2..515b61f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,20 @@ Changelog ========= +## version 23.2 + +### Added: + ++ Support for regional holidays - thank you [koterpillar](https://github.com/koterpillar) [(#11)](https://github.com/hvianna/desktopCal.js/pull/11); ++ Holidays for Australia - thank you [koterpillar](https://github.com/koterpillar) [(#9)](https://github.com/hvianna/desktopCal.js/pull/9); ++ Regional holidays for Brazil. + +### Fixed and improved: + ++ Observation of consecutive holidays in UK - thank you [koterpillar](https://github.com/koterpillar) [(#7)](https://github.com/hvianna/desktopCal.js/issues/7); ++ Add, not replace, holidays in lieu (for UK and AU) - thank you [koterpillar](https://github.com/koterpillar) [(#10)](https://github.com/hvianna/desktopCal.js/pull/10); ++ Updated US holidays: added Juneteenth and renamed Washington's Birthday to Presidents' Day. + ## version 23.1 ### Added: diff --git a/README.md b/README.md index 38095ba..a39da6e 100644 --- a/README.md +++ b/README.md @@ -32,18 +32,20 @@ You can add your own custom holidays. These will be saved in your browser's loca + [Saving canvas as image](https://weworkweplay.com/play/saving-html5-canvas-as-image/) and [solution to CORS issue on canvas.toDataURL()](https://stackoverflow.com/a/30517793/2370385) + [W3Schools Canvas Reference](https://www.w3schools.com/tags/ref_canvas.asp) + [How to draw a rounded Rectangle on HTML Canvas?](https://stackoverflow.com/a/7838871/2370385) -+ Public holidays in [Argentina](https://en.wikipedia.org/wiki/Public_holidays_in_Argentina), -[Brazil](https://pt.wikipedia.org/wiki/Feriados_no_Brasil), -[Canada](https://en.wikipedia.org/wiki/Public_holidays_in_Canada), -[France](https://en.wikipedia.org/wiki/Public_holidays_in_France), -Germany ([here](https://en.wikipedia.org/wiki/Public_holidays_in_Germany), [here](https://en.wikipedia.org/wiki/Bu%C3%9F-_und_Bettag) and [here](https://www.schulferien.org/deutschland/feiertage/)), -[Mexico](https://en.wikipedia.org/wiki/Public_holidays_in_Mexico), -[Portugal](https://en.wikipedia.org/wiki/Public_holidays_in_Portugal), -[Spain](https://en.wikipedia.org/wiki/Public_holidays_in_Spain), -[United Kingdom](https://en.wikipedia.org/wiki/Public_holidays_in_the_United_Kingdom), -[United States](https://en.wikipedia.org/wiki/Federal_holidays_in_the_United_States), -[Uruguay](https://en.wikipedia.org/wiki/Public_holidays_in_Uruguay). + [Paper sizes](https://papersizes.io/) ++ References for public holidays: + + [Argentina](https://en.wikipedia.org/wiki/Public_holidays_in_Argentina) + + [Australia](https://www.fairwork.gov.au/employment-conditions/public-holidays) + + [Brazil](https://pt.wikipedia.org/wiki/Feriados_no_Brasil) - [AC](https://agencia.ac.gov.br/governo-do-acre-divulga-calendario-de-feriados-e-pontos-facultativos-de-2023/), [AL](https://www.correiodosmunicipios-al.com.br/2022/01/governo-de-alagoas-divulga-calendario-de-feriados-previstos-para-2022/), [PR](https://www.legislacao.pr.gov.br/legislacao/pesquisarAto.do?action=exibir&codAto=251964&indice=1&totalRegistros=28&dt=29.0.2023.11.16.25.855) + + [Canada](https://en.wikipedia.org/wiki/Public_holidays_in_Canada) + + [France](https://en.wikipedia.org/wiki/Public_holidays_in_France) + + [Germany](https://en.wikipedia.org/wiki/Public_holidays_in_Germany), also [here](https://en.wikipedia.org/wiki/Bu%C3%9F-_und_Bettag) and [here](https://www.schulferien.org/deutschland/feiertage/) + + [Mexico](https://en.wikipedia.org/wiki/Public_holidays_in_Mexico) + + [Portugal](https://en.wikipedia.org/wiki/Public_holidays_in_Portugal) + + [Spain](https://en.wikipedia.org/wiki/Public_holidays_in_Spain) + + [United Kingdom](https://en.wikipedia.org/wiki/Public_holidays_in_the_United_Kingdom) + + [United States](https://en.wikipedia.org/wiki/Federal_holidays_in_the_United_States) + + [Uruguay](https://en.wikipedia.org/wiki/Public_holidays_in_Uruguay) ## License diff --git a/img/icons8-ru-flag.png b/img/icons8-ru-flag.png new file mode 100644 index 0000000..b48f19b Binary files /dev/null and b/img/icons8-ru-flag.png differ diff --git a/js/desktopCal.js b/js/desktopCal.js index 7cb59d7..b110c5f 100644 --- a/js/desktopCal.js +++ b/js/desktopCal.js @@ -21,13 +21,13 @@ */ 'use strict'; -var _VERSION = '23.1'; +const VERSION = '23.2'; -var cropper = [], +let cropper = [], colorPresets; function getVersion() { - return `v${_VERSION}`; + return `v${VERSION}`; } /** @@ -35,7 +35,7 @@ function getVersion() { */ function changeLayout() { - var layout = document.querySelector('input[name="layout"]:checked').value; + const layout = document.querySelector('input[name="layout"]:checked').value; // set layout document.getElementById('config').className = layout; @@ -61,10 +61,10 @@ function changeLayout() { // Cropper.js won't change the aspect ratio of preview elements, so we need to recreate them.. - for ( let i of [0,1] ) { + for ( const i of [0,1] ) { // save loaded image - let imgEl = document.getElementById( `image${i}` ); - let imgSrc = imgEl.src; + const imgEl = document.getElementById( `image${i}` ), + imgSrc = imgEl.src; // destroy cropper instance if ( cropper[ i ] ) @@ -74,7 +74,7 @@ function changeLayout() { imgEl.src = imgSrc; // clear preview element style - let pvwEl = document.getElementById( `preview${i}` ); + const pvwEl = document.getElementById( `preview${i}` ); pvwEl.style = ''; // create new cropper instance with proper aspect ratio @@ -119,8 +119,8 @@ function changeInitialWeekday() { * Set CSS classnames for the preview to match selected calendar layout and settings */ function changeStyle() { - let layout = document.querySelector('input[name="layout"]:checked').value, - previewEl = document.getElementById('preview'); + const layout = document.querySelector('input[name="layout"]:checked').value, + previewEl = document.getElementById('preview'); previewEl.className = layout; if ( layout != 'digital' ) { @@ -168,8 +168,8 @@ function configUIElements() { // Cropper.js action buttons document.querySelectorAll('.cropper-action').forEach( el => { el.addEventListener('click', e => { - let n = e.target.dataset.obj; - let action = e.target.dataset.action; + const n = e.target.dataset.obj, + action = e.target.dataset.action; switch ( action ) { case 'rotR': cropper[ n ].rotate(90); @@ -214,7 +214,7 @@ function deleteColorPreset( index ) { } function listColorPresets() { - var html = ''; + let html = ''; colorPresets.forEach( ( preset, index ) => { html += '' + @@ -246,8 +246,7 @@ function loadColorPreset( index ) { */ function loadImage( obj, n ) { - var reader = new FileReader(), - layout = document.getElementById('preview').className; + const reader = new FileReader(); reader.onload = function() { document.getElementById( `image${n}` ).src = reader.result; @@ -518,11 +517,10 @@ function generateCalendar( month, year, canvas = null ) { */ function updatePreview() { - var area = [ document.getElementById('top-half'), document.getElementById('bottom-half') ], - year = [ document.getElementById('top-year').value, document.getElementById('bottom-year').value ], - month = [ document.getElementById('top-month').value, document.getElementById('bottom-month').value ], - country = document.getElementById('country').value, - layout = document.querySelector('input[name="layout"]:checked').value; + const area = [ document.getElementById('top-half'), document.getElementById('bottom-half') ], + year = [ document.getElementById('top-year').value, document.getElementById('bottom-year').value ], + month = [ document.getElementById('top-month').value, document.getElementById('bottom-month').value ], + layout = document.querySelector('input[name="layout"]:checked').value; // set lang attribute on html element document.getElementsByTagName('html')[0].lang = `${lang}-${country.toUpperCase()}`; @@ -532,7 +530,7 @@ function updatePreview() { document.getElementById('v-align').disabled = document.getElementById('cal-size').value == 'col'; if ( layout != 'digital' ) { - for ( let i of [0,1] ) { + for ( const i of [0,1] ) { if ( month[ i ] > 0 && year[ i ] > 0 ) { area[ i ].querySelector('.cal-title').innerText = msg[ lang ].monthNames[ month[ i ] ] + ' ' + year[ i ]; area[ i ].querySelector('.calendar').innerHTML = generateCalendar( month[ i ], year[ i ] ); @@ -540,12 +538,13 @@ function updatePreview() { } } else { - let canvas = document.getElementById('canvas'); + const canvas = document.getElementById('canvas'), + ctx = canvas.getContext('2d'), + img = cropper[0].getCroppedCanvas(); + canvas.width = document.getElementById('canvas-width').value; canvas.height = document.getElementById('canvas-height').value; - let ctx = canvas.getContext('2d'); - let img = cropper[0].getCroppedCanvas(); if ( img ) ctx.drawImage( img, 0, 0, canvas.width, canvas.height ); generateCalendar( month[ 1 ], year[ 1 ], canvas ); @@ -565,10 +564,10 @@ function renderCredits() { */ function rotateCanvas() { - var tmp = document.getElementById('canvas-width').value; + const canvasWidth = document.getElementById('canvas-width'), + canvasHeight = document.getElementById('canvas-height'); - document.getElementById('canvas-width').value = document.getElementById('canvas-height').value; - document.getElementById('canvas-height').value = tmp; + [ canvasWidth.value, canvasHeight.value ] = [ canvasHeight.value, canvasWidth.value ]; changeLayout(); } @@ -578,7 +577,7 @@ function rotateCanvas() { */ function downloadCalendar( obj ) { - var format = document.querySelector('input[name="file-format"]:checked').value; + const format = document.querySelector('input[name="file-format"]:checked').value; obj.download = 'desktopCal-' + document.getElementById('bottom-year').value + '-' + document.getElementById('bottom-month').value + '.' + format; obj.href = document.getElementById('canvas').toDataURL(`image/${format}`); } @@ -609,12 +608,12 @@ CanvasRenderingContext2D.prototype.roundRect = function ( x, y, w, h, r ) { */ async function prepareForPrinting() { - for ( let i of [0,1] ) { + for ( const i of [0,1] ) { document.getElementById( `preview${i}` ).style.display = 'none'; // hide cropper.js preview area - let img = cropper[ i ].getCroppedCanvas(); + const img = cropper[ i ].getCroppedCanvas(); if ( img ) { - let blob = await new Promise( resolve => img.toBlob( resolve ) ); - let url = URL.createObjectURL( blob ); + const blob = await new Promise( resolve => img.toBlob( resolve ) ); + const url = URL.createObjectURL( blob ); document.getElementById( `cal-image${i}` ).style = `background-image: url(${url})`; } } @@ -626,7 +625,7 @@ async function prepareForPrinting() { * Restore preview areas and clear background images used for printing */ function restoreFromPrinting() { - for ( let i of [0,1] ) { + for ( const i of [0,1] ) { document.getElementById( `preview${i}` ).style.display = 'block'; document.getElementById( `cal-image${i}` ).style = ''; } @@ -642,8 +641,8 @@ function initialize() { // try to get preferred language and country const [ browserLang, browserCountry ] = navigator.language.split('-'), - prefLang = localStorage.getItem('lang') || browserLang, - prefCountry = localStorage.getItem('country') || browserCountry && browserCountry.toLowerCase(); + prefLang = localStorage.getItem('lang') || browserLang, + prefCountry = localStorage.getItem('country') || browserCountry && browserCountry.toLowerCase(); if ( Object.keys( msg ).includes( prefLang ) ) lang = prefLang; @@ -655,6 +654,11 @@ function initialize() { else country = msg[ lang ].defCountry; + const prefRegion = localStorage.getItem('region'); + if ( prefRegion && Object.keys( countries[country].regions ).includes( prefRegion ) ) { + region = prefRegion; + } + // load color presets colorPresets = JSON.parse( localStorage.getItem( 'color-presets' ) ) || []; @@ -696,8 +700,8 @@ function initialize() { // init canvas width and height fields with the display's dimensions - let w = window.screen.width * window.devicePixelRatio, - h = window.screen.height * window.devicePixelRatio; + const w = window.screen.width * window.devicePixelRatio, + h = window.screen.height * window.devicePixelRatio; document.getElementById('canvas-width').value = w; document.getElementById('canvas-height').value = h; @@ -706,12 +710,12 @@ function initialize() { let loaded = 0; - for ( let i of [0,1] ) { + for ( const i of [0,1] ) { fetch( `https://picsum.photos/${w}/${w*.75}/?random` ) .then( response => response.blob() ) .then( blob => { - let url = URL.createObjectURL( blob ); - let imgEl = document.getElementById( `image${i}` ); + const url = URL.createObjectURL( blob ), + imgEl = document.getElementById( `image${i}` ); imgEl.src = url; // adjust paper layout and initialize croppable areas when both images finish loading diff --git a/js/holidays.js b/js/holidays.js index 1ca3e2c..68d94b4 100644 --- a/js/holidays.js +++ b/js/holidays.js @@ -9,53 +9,130 @@ */ function checkHoliday( year, month, day ) { - let holidays = [], - easterHolidays = []; + const easterSunday = computus( year ), + holidays = getCustomHolidays(); switch ( country ) { case 'ar': - holidays = [ + holidays.push( { date: '1-1', name: 'Año Nuevo' }, { date: '3-24', name: 'Memoria por la Verdad y la Justicia' }, { date: '4-2', name: 'Día del Veterano' }, { date: '5-1', name: 'Día del Trabajador' }, { date: '5-25', name: 'Revolución de Mayo' }, - { date: calcObservation( year, 6, 17, country ), name: 'General Martín Miguel de Güemes' }, + ...observed( year, 6, 17, country, 'General Martín Miguel de Güemes' ), { date: '6-20', name: 'General Manuel Belgrano' }, { date: '7-9', name: 'Día de la Independencia' }, - { date: calcObservation( year, 8, 17, country ), name: 'General José de San Martín' }, - { date: calcObservation( year, 10, 12, country ), name: 'Respeto a la Diversidad Cultural' }, - { date: calcObservation( year, 11, 20, country ), name: 'Soberanía Nacional' }, + ...observed( year, 8, 17, country, 'General José de San Martín' ), + ...observed( year, 10, 12, country, 'Respeto a la Diversidad Cultural' ), + ...observed( year, 11, 20, country, 'Soberanía Nacional' ), { date: '12-8', name: 'Inmaculada Concepción de María' }, - { date: '12-25', name: 'Navidad' } - ]; - easterHolidays = [ - { days: -48, name: 'Carnaval' }, - { days: -47, name: 'Carnaval' }, - { days: -2, name: 'Viernes Santo' } - ]; + { date: '12-25', name: 'Navidad' }, + { date: dateToMonthDay( dateAdd( easterSunday, -48 ) ), name: 'Carnaval' }, + { date: dateToMonthDay( dateAdd( easterSunday, -47 ) ), name: 'Carnaval' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Viernes Santo' } + ); + break; + + case 'au': + holidays.push( + ...observed( year, 1, 1, country, 'New Year\'s Day' ), + ...observed( year, 1, 26, country, 'Australia Day' ), + ...inRegions(['tas'], { date: floatingDoW( 1, year, 2, 8 ), name: 'Royal Hobart Regatta' }), + ...inRegions(['vic', 'wa'], { date: floatingDoW( 1, year, 3, 1 ), name: 'Labour Day' }), + ...inRegions(['act'], { date: floatingDoW( 1, year, 3, 8 ), name: 'Canberra Day' }), + ...inRegions(['sa'], { date: floatingDoW( 1, year, 3, 8 ), name: 'Adelaide Cup Day' }), + ...inRegions(['tas'], { date: floatingDoW( 1, year, 3, 8 ), name: 'Eight Hours Day' }), + { date: '4-25', name: 'Anzac Day' }, + ...inRegions(['nt'], { date: floatingDoW( 1, year, 5, 1 ), name: 'May Day' }), + ...inRegions(['qld'], { date: floatingDoW( 1, year, 5, 1 ), name: 'Labour Day' }), + ...inRegions(['act'], { date: floatingDoW( 1, year, 5, 27 ), name: 'Reconciliation Day' }), + ...inRegions(['wa'], { date: floatingDoW( 1, year, 6, 1 ), name: 'Western Australia Day' }), + ...inRegions(['vic', 'nsw', 'sa', 'tas'], { date: floatingDoW( 1, year, 6, 8 ), name: 'King\'s Birthday' }), + ...inRegions(['act'], { date: floatingDoW( 1, year, 6, 8 ), name: 'Sovereign\'s Birthday' }), + ...inRegions(['nt'], { date: floatingDoW( 1, year, 6, 8 ), name: 'June public holiday' }), + ...inRegions(['nt'], { date: floatingDoW( 1, year, 8, 1 ), name: 'Picnic Day' }), + ...inRegions(['qld'], { date: floatingDoW( 3, year, 8, 10 ), name: 'Royal Queensland Show (Brisbane)' }), + ...inRegions(['wa'], { date: floatingDoW( 1, year, 9, 22 ), name: 'King\'s Birthday' }), + ...inRegions(['qld'], { date: floatingDoW( 1, year, 10, 1 ), name: 'King\'s Birthday' }), + ...inRegions(['act', 'nsw', 'sa'], { date: floatingDoW( 1, year, 10, 1 ), name: 'Labour Day' }), + ...inRegions(['vic'], { date: floatingDoW( 2, year, 11, 1 ), name: 'Melbourne Cup' }), + ...inRegions(['tas'], { date: floatingDoW( 1, year, 11, 1 ), name: 'Recreation Day' }), + ...observed( year, 12, 25, country, 'Christmas Day', { consecutive: 2 } ), + ...observed( year, 12, 26, country, 'Boxing Day', { consecutive: 2 } ), + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Good Friday' }, + ...inRegions( ['act', 'nsw', 'nt', 'sa'], { date: dateToMonthDay( dateAdd( easterSunday, -1 ) ), name: 'Easter Saturday' } ), + ...inRegions( ['qld'], { date: dateToMonthDay( dateAdd( easterSunday, -1 ) ), name: 'The day after Good Friday' } ), + ...inRegions( ['vic'], { date: dateToMonthDay( dateAdd( easterSunday, -1 ) ), name: 'Saturday before Easter Sunday' } ), + { date: dateToMonthDay( dateAdd( easterSunday, 0 ) ), name: 'Easter Sunday' }, + { date: dateToMonthDay( dateAdd( easterSunday, 1 ) ), name: 'Easter Monday' }, + ...inRegions( ['tas'], { date: dateToMonthDay( dateAdd( easterSunday, 2 ) ), name: 'Easter Tuesday (Public Service only)' } ) + ); break; case 'br': - holidays = [ + holidays.push( { date: '1-1', name: 'Confraternização Universal' }, + ...inRegions( ['ro'], { date: '1-4', name: 'Criação do estado de Rondônia' } ), + ...inRegions( ['ac'], ...observed( year, 1, 20, 'br-ac', 'Dia do Católico' ) ), + ...inRegions( ['ac'], ...observed( year, 1, 23, 'br-ac', 'Dia do Evangélico' ) ), + ...inRegions( ['pe'], { date: '3-6', name: 'Revolução Pernambucana' } ), + ...inRegions( ['ac'], ...observed( year, 3, 8, 'br-ac', 'Dia Internacional da Mulher' ) ), + ...inRegions( ['to'], { date: '3-18', name: 'Criação da Comarca do Norte' } ), + ...inRegions( ['ap','ce'], { date: '3-19', name: 'São José' } ), + ...inRegions( ['ce'], { date: '3-25', name: 'Abolição da escravidão no Ceará' } ), { date: '4-21', name: 'Tiradentes' }, + ...inRegions( ['df'], { date: '4-21', name: 'Fundação de Brasília' } ), + ...inRegions( ['mg'], { date: '4-21', name: 'Data Magna do estado de Minas Gerais' } ), + ...inRegions( ['rj'], { date: '4-23', name: 'São Jorge' } ), { date: '5-1', name: 'Dia do Trabalhador' }, + ...inRegions( ['ac'], { date: '6-15', name: 'Aniversário do estado do Acre' } ), + ...inRegions( ['ro'], { date: '6-18', name: 'Dia do Evangélico' } ), + ...inRegions( ['al','pe'], { date: '6-24', name: 'São João' } ), + ...inRegions( ['al'], { date: '6-29', name: 'São Pedro' } ), + ...inRegions( ['ba'], { date: '7-2', name: 'Independência da Bahia' } ), + ...inRegions( ['se'], { date: '7-8', name: 'Emancipação política de Sergipe' } ), + ...inRegions( ['sp'], { date: '7-9', name: 'Revolução Constitucionalista' } ), + ...inRegions( ['go'], { date: '7-26', name: 'Fundação da cidade de Goiás' } ), + ...inRegions( ['ma'], { date: '7-28', name: 'Adesão do Maranhão à independência do Brasil' } ), + ...inRegions( ['pb'], { date: '8-5', name: 'Nossa Senhora das Neves' } ), + ...inRegions( ['rn'], { date: '8-7', name: 'Fixação do Marco Colonial de Touros' } ), + ...inRegions( ['sc'], ...observed( year, 8, 11, 'br-sc', 'Data Magna do estado de Santa Catarina' ) ), + ...inRegions( ['ce'], { date: '8-15', name: 'Nossa Senhora da Assunção' } ), + ...inRegions( ['pa'], { date: '8-15', name: 'Adesão do Pará à independência do Brasil' } ), + ...inRegions( ['pr'], { date: '8-29', name: 'Dia do Paraná (ponto facultativo)' } ), + ...inRegions( ['ac'], ...observed( year, 9, 5, 'br-ac', 'Dia da Amazônia' ) ), + ...inRegions( ['am'], { date: '9-5', name: 'Elevação do Amazonas à categoria de província' } ), { date: '9-7', name: 'Proclamação da Independência' }, + ...inRegions( ['to'], { date: '9-8', name: 'Nossa Senhora da Natividade' } ), + ...inRegions( ['ap'], { date: '9-13', name: 'Data Magna do estado do Amapá' } ), + ...inRegions( ['al'], { date: '9-16', name: 'Emancipação Política de Alagoas' } ), + ...inRegions( ['rs'], { date: '9-20', name: 'Revolução Farroupilha' } ), + ...inRegions( ['rn'], { date: '10-3', name: 'Mártires de Cunhaú e Uruaçu' } ), + ...inRegions( ['rr'], { date: '10-5', name: 'Criação do estado de Roraima' } ), + ...inRegions( ['to'], { date: '10-5', name: 'Criação do estado de Tocantins' } ), + ...inRegions( ['ms'], { date: '10-11', name: 'Criação do estado do Mato Grosso do Sul' } ), { date: '10-12', name: 'Nossa Senhora Aparecida' }, + ...inRegions( ['rj'], { date: floatingDoW( 1, year, 10, 15 ), name: 'Dia do Comércio' } ), + ...inRegions( ['pi'], { date: '10-19', name: 'Dia do Piauí' } ), + ...inRegions( ['go'], { date: '10-24', name: 'Pedra fundamental de Goiânia' } ), { date: '11-2', name: 'Finados' }, { date: '11-15', name: 'Proclamação da República' }, - { date: '12-25', name: 'Natal' } - ]; - easterHolidays = [ - { days: -47, name: 'Carnaval' }, - { days: -2, name: 'Sexta-feira Santa' }, - { days: 60, name: 'Corpus Christi' } - ]; + ...inRegions( ['ac'], ...observed( year, 11, 17, 'br-ac', 'Tratado de Petrópolis' ) ), + ...inRegions( ['al','am','mt','rj'], { date: '11-20', name: 'Dia da Consciência Negra' } ), + ...inRegions( ['al','df'], { date: '11-30', name: 'Dia do Evangélico' } ), + ...inRegions( ['am'], { date: '12-8', name: 'Nossa Senhora da Conceição' } ), + { date: '12-25', name: 'Natal' }, + { date: dateToMonthDay( dateAdd( easterSunday, -47 ) ), name: 'Carnaval' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Paixão de Cristo' }, + { date: dateToMonthDay( dateAdd( easterSunday, 0 ) ), name: 'Páscoa' }, + { date: dateToMonthDay( dateAdd( easterSunday, 60 ) ), name: 'Corpus Christi' }, + ...inRegions( ['es'], { date: dateToMonthDay( dateAdd( easterSunday, 8 ) ), name: 'Nossa Senhora da Penha' } ) + ); break; case 'ca': - holidays = [ + holidays.push( { date: '1-1', name: 'New Year\'s Day' }, { date: floatingDoW( 1, year, 5, 18 ), name: 'Victoria Day' }, { date: '7-1', name: 'Canada Day' }, @@ -64,16 +141,14 @@ function checkHoliday( year, month, day ) { { date: floatingDoW( 1, year, 10, 8 ), name: 'Thanksgiving' }, { date: '11-11', name: 'Remembrance Day' }, { date: '12-25', name: 'Christmas Day' }, - { date: '12-26', name: 'Boxing Day' } - ]; - easterHolidays = [ - { days: -2, name: 'Good Friday' }, - { days: 1, name: 'Easter Monday' } - ]; + { date: '12-26', name: 'Boxing Day' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Good Friday' }, + { date: dateToMonthDay( dateAdd( easterSunday, 1 ) ), name: 'Easter Monday' } + ); break; case 'de': - holidays = [ + holidays.push( { date: '1-1', name: 'Neujahr' }, { date: '1-6', name: 'Heilige Drei Könige' }, { date: '3-8', name: 'Internationaler Frauentag' }, @@ -85,21 +160,19 @@ function checkHoliday( year, month, day ) { { date: '11-1', name: 'Allerheiligen' }, { date: floatingDoW( 3, year, 11, 16 ), name: 'Buß- und Bettag' }, // Wednesday between November 16-22 { date: '12-25', name: '1. Weihnachtsfeiertag' }, - { date: '12-26', name: '2. Weihnachtsfeiertag' } - ]; - easterHolidays = [ - { days: -2, name: 'Karfreitag' }, - { days: 0, name: 'Ostersonntag' }, - { days: 1, name: 'Ostermontag' }, - { days: 39, name: 'Christi Himmelfahrt' }, - { days: 49, name: 'Pfingstsonntag' }, - { days: 50, name: 'Pfingstmontag' }, - { days: 60, name: 'Fronleichnam' }, - ]; + { date: '12-26', name: '2. Weihnachtsfeiertag' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Karfreitag' }, + { date: dateToMonthDay( dateAdd( easterSunday, 0 ) ), name: 'Ostersonntag' }, + { date: dateToMonthDay( dateAdd( easterSunday, 1 ) ), name: 'Ostermontag' }, + { date: dateToMonthDay( dateAdd( easterSunday, 39 ) ), name: 'Christi Himmelfahrt' }, + { date: dateToMonthDay( dateAdd( easterSunday, 49 ) ), name: 'Pfingstsonntag' }, + { date: dateToMonthDay( dateAdd( easterSunday, 50 ) ), name: 'Pfingstmontag' }, + { date: dateToMonthDay( dateAdd( easterSunday, 60 ) ), name: 'Fronleichnam' } + ); break; case 'es': - holidays = [ + holidays.push( { date: '1-1', name: 'Año Nuevo' }, { date: '1-6', name: 'Día de Reyes' }, { date: '5-1', name: 'Día del Trabajador' }, @@ -108,16 +181,14 @@ function checkHoliday( year, month, day ) { { date: '11-1', name: 'Día de todos los Santos' }, { date: '12-6', name: 'Día de la Constitución' }, { date: '12-8', name: 'Inmaculada Concepción' }, - { date: '12-25', name: 'Navidad' } - ]; - easterHolidays = [ - { days: -3, name: 'Jueves Santo' }, - { days: -2, name: 'Viernes Santo' } - ]; + { date: '12-25', name: 'Navidad' }, + { date: dateToMonthDay( dateAdd( easterSunday, -3 ) ), name: 'Jueves Santo' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Viernes Santo' } + ); break; case 'fr': - holidays = [ + holidays.push( { date: '1-1', name: 'Nouvel an' }, { date: '5-1', name: 'Fête des Travailleurs' }, { date: '5-8', name: 'Fête de la Victoire' }, @@ -126,30 +197,28 @@ function checkHoliday( year, month, day ) { { date: '11-1', name: 'Toussaint' }, { date: '11-11', name: 'Armistice de 1918' }, { date: '12-25', name: 'Noël' }, - { date: '12-26', name: 'Deuxième jour de Noël' } - ]; - easterHolidays = [ - { days: -2, name: 'Vendredi saint' }, - { days: 1, name: 'Lundi de Pâques' }, - { days: 39, name: 'Ascension' }, - { days: 50, name: 'Lundi de Pentecôte' } - ]; + { date: '12-26', name: 'Deuxième jour de Noël' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Vendredi saint' }, + { date: dateToMonthDay( dateAdd( easterSunday, 1 ) ), name: 'Lundi de Pâques' }, + { date: dateToMonthDay( dateAdd( easterSunday, 39 ) ), name: 'Ascension' }, + { date: dateToMonthDay( dateAdd( easterSunday, 50 ) ), name: 'Lundi de Pentecôte' } + ); break; case 'mx': - holidays = [ - { date: calcObservation( year, 1, 1, country ), name: 'Año Nuevo' }, + holidays.push( + ...observed( year, 1, 1, country, 'Año Nuevo' ), { date: floatingDoW( 1, year, 2, 1 ), name: 'Día de la Constitución' }, { date: floatingDoW( 1, year, 3, 15 ), name: 'Natalicio de Benito Juárez' }, - { date: calcObservation( year, 5, 1, country ), name: 'Día del Trabajo' }, - { date: calcObservation( year, 9, 16, country ), name: 'Día de la Independencia' }, + ...observed( year, 5, 1, country, 'Día del Trabajo' ), + ...observed( year, 9, 16, country, 'Día de la Independencia' ), { date: floatingDoW( 1, year, 11, 15 ), name: 'Día de la Revolución' }, - { date: calcObservation( year, 12, 25, country ), name: 'Navidad' } - ]; + ...observed( year, 12, 25, country, 'Navidad') + ); break; case 'pt': - holidays = [ + holidays.push( { date: '1-1', name: 'Ano Novo' }, { date: '4-25', name: 'Dia da Liberdade' }, { date: '5-1', name: 'Dia do Trabalhador' }, @@ -159,73 +228,59 @@ function checkHoliday( year, month, day ) { { date: '11-1', name: 'Dia de Todos-os-Santos' }, { date: '12-1', name: 'Restauração da Independência' }, { date: '12-8', name: 'Imaculada Conceição' }, - { date: '12-25', name: 'Natal' } - ]; - easterHolidays = [ - { days: -47, name: 'Carnaval' }, - { days: -2, name: 'Sexta-feira Santa' }, - { days: 60, name: 'Corpo de Deus' } - ]; + { date: '12-25', name: 'Natal' }, + { date: dateToMonthDay( dateAdd( easterSunday, -47 ) ), name: 'Carnaval' }, + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Sexta-feira Santa' }, + { date: dateToMonthDay( dateAdd( easterSunday, 60 ) ), name: 'Corpo de Deus' } + ); break; case 'uk': - holidays = [ - { date: calcObservation( year, 1, 1, country ), name: 'New Year\'s Day' }, + holidays.push( + ...observed( year, 1, 1, country, 'New Year\'s Day' ), { date: floatingDoW( 1, year, 5, 1 ), name: 'May Day Bank Holiday' }, { date: floatingDoW( 1, year, 5, 25 ), name: 'Spring Bank Holiday' }, { date: floatingDoW( 1, year, 8, 25 ), name: 'Late Summer Bank Holiday' }, - { date: calcObservation( year, 12, 25, country ), name: 'Christmas Day' }, - { date: calcObservation( year, 12, 26, country ), name: 'Boxing Day' } - ]; - easterHolidays = [ - { days: -2, name: 'Good Friday' }, - { days: 1, name: 'Easter Monday' } - ]; + ...observed( year, 12, 25, country, 'Christmas Day', { consecutive: 2 } ), + ...observed( year, 12, 26, country, 'Boxing Day', { consecutive: 2 } ), + { date: dateToMonthDay( dateAdd( easterSunday, -2 ) ), name: 'Good Friday' }, + { date: dateToMonthDay( dateAdd( easterSunday, 1 ) ), name: 'Easter Monday' } + ); break; case 'us': - holidays = [ + holidays.push( { date: '1-1', name: 'New Year\'s Day' }, { date: floatingDoW( 1, year, 1, 15 ), name: 'Birthday of Martin Luther King Jr.' }, - { date: floatingDoW( 1, year, 2, 15 ), name: 'Washington\'s Birthday' }, + { date: floatingDoW( 1, year, 2, 15 ), name: 'Presidents\' Day' }, { date: floatingDoW( 1, year, 5, 25 ), name: 'Memorial Day' }, + { date: '6-19', name: 'Juneteenth' }, { date: '7-4', name: 'Independence Day' }, { date: floatingDoW( 1, year, 9, 1 ), name: 'Labor Day' }, { date: floatingDoW( 1, year, 10, 8 ), name: 'Columbus Day' }, { date: '11-11', name: 'Veterans Day' }, { date: floatingDoW( 4, year, 11, 22 ), name: 'Thanksgiving Day' }, { date: '12-25', name: 'Christmas Day' } - ]; + ); break; case 'uy': - holidays = [ + holidays.push( { date: '1-1', name: 'Año Nuevo' }, { date: '1-6', name: 'Día de Reyes' }, - { date: calcObservation( year, 4, 19, country ), name: 'Desembarco de los 33 Orientales' }, + ...observed( year, 4, 19, country, 'Desembarco de los 33 Orientales' ), { date: '5-1', name: 'Día de los Trabajadores' }, - { date: calcObservation( year, 5, 18, country ), name: 'Batalla de las Piedras' }, + ...observed( year, 5, 18, country, 'Batalla de las Piedras' ), { date: '6-19', name: 'Natalicio de Artigas y Día del Nunca Más' }, { date: '7-18', name: 'Jura de la Constitución' }, { date: '8-25', name: 'Declaratoria de la Independencia' }, - { date: calcObservation( year, 10, 12, country ), name: 'Día de la Raza' }, + ...observed( year, 10, 12, country, 'Día de la Raza' ), { date: '11-2', name: 'Día de los Difuntos' }, { date: '12-25', name: 'Navidad' } - ]; + ); break; } - // calculates floating holidays based on Easter Day - if ( easterHolidays.length ) { - let easter = computus( year ); - easterHolidays.forEach( d => { - let date = dateAdd( easter, d.days ); - holidays.push( { date: `${ date.getMonth() + 1 }-${ date.getDate() }`, name: d.name } ); - }); - } - - holidays = holidays.concat( getCustomHolidays() ); - // https://stackoverflow.com/questions/7364150/find-object-by-id-in-an-array-of-javascript-objects return holidays.filter( i => i.date == `${month}-${day}` ).map( i => i.name ); } @@ -318,50 +373,92 @@ function getMonthDays( year ) { } /** - * Calculates the observation date for a given holiday according to a country's rules + * Returns the holiday only if it's applicable in the current region * - * @param {number} year - * @param {number} month - * @param {number} day - * @param {string} country + * @param {array} holidayRegions Regions the holiday is observed in + * @param {object} holiday Holiday definition + * @returns Array (zero or one) of holiday definitions applicable + */ +function inRegions ( holidayRegions, holiday ) { + if ( holidayRegions.includes( region ) ) { + return [ holiday ]; + } + else { + return []; + } +} + +/** + * Returns an array of actual and/or observed holiday dates according to a country's rules * - * @returns {string} holiday date in 'month-day' format + * @param {number} year Holiday year + * @param {number} month Holiday month + * @param {number} day Holiday day + * @param {string} country Country (or Country-Region) code + * @param {string} name Holiday name + * @param {Object} options Additional options + * @param {number} options.consecutive How many consecutive holidays is this one a part of (to avoid shifting observed date onto them) + * @returns {array} array of event objects */ -function calcObservation( year, month, day, country ) { +function observed( year, month, day, country, name, options ) { + options = options || {}; - var diff = 0, + let diffs = [0], date = new Date( year, month - 1, day ), - dow = date.getDay(); + dow = date.getDay(), + observedName = name; switch ( country ) { case 'mx' : if ( dow == 0 ) - diff = 1; + diffs = [1]; else if ( dow == 6 ) - diff = -1; + diffs = [-1]; break; + case 'au' : case 'uk' : + // if a holiday falls on a Sunday and Monday becomes a public holiday instead, both Sunday and Monday dates will be returned + var consecutive = options.consecutive || 1; if ( dow == 0 ) - diff = 1; + diffs.push( consecutive ); else if ( dow == 6 ) - diff = 2; + diffs.push( 2 ); + observedName = `${name} (in lieu)`; break; case 'ar' : case 'uy' : if ( dow == 2 ) - diff = -1; + diffs = [-1]; else if ( dow == 3 ) - diff = -2; + diffs = [-2]; else if ( dow == 4 ) - diff = 4; + diffs = [4]; else if ( dow == 5 ) - diff = 3; + diffs = [3]; + break; + + case 'br-ac' : + // holidays between Tuesday and Thursday are postponed to Friday + if ( dow >= 2 && dow <= 4 ) + diffs = [ 5 - dow ]; + break; + + case 'br-sc' : + // holidays on working days are postponed to Sunday + if ( dow > 0 ) + diffs = [ 7 - dow ]; break; } - return dateToMonthDay( dateAdd( date, diff ) ); + return diffs.map(diff => { + var newDate = dateToMonthDay( dateAdd( date, diff ) ); + return { + date: newDate, + name: diff == 0 ? name : observedName + } + }); } /** @@ -474,4 +571,4 @@ function deleteCustomHoliday( i ) { localStorage.setItem( 'custom-holidays', JSON.stringify( holidays ) ); document.querySelector('#custom-holidays-table tbody').innerHTML = listCustomHolidays(); updatePreview(); -} \ No newline at end of file +} diff --git a/js/i18n.js b/js/i18n.js index 0c6012d..3eb6085 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -3,12 +3,56 @@ */ // current language and country -var lang, country; +let lang, country, region; -// countries for holiday selection list -var countries = { +// countries and regions for holiday selection list +const countries = { ar: { name: 'Argentina' }, - br: { name: 'Brasil' }, + au: { + name: 'Australia', + regions: { + act: { name: 'Australian Capital Territory' }, + nsw: { name: 'New South Wales' }, + nt: { name: 'Northern Territory' }, + qld: { name: 'Queensland' }, + sa: { name: 'South Australia' }, + tas: { name: 'Tasmania' }, + vic: { name: 'Victoria' }, + wa: { name: 'Western Australia' } + } + }, + br: { + name: 'Brasil', + regions: { + ac: { name: 'Acre' }, + al: { name: 'Alagoas' }, + ap: { name: 'Amapá' }, + am: { name: 'Amazonas' }, + ba: { name: 'Bahia' }, + ce: { name: 'Ceará' }, + df: { name: 'Distrito Federal' }, + es: { name: 'Espírito Santo' }, + go: { name: 'Goiás' }, + ma: { name: 'Maranhão' }, + mt: { name: 'Mato Grosso' }, + ms: { name: 'Mato Grosso do Sul' }, + mg: { name: 'Minas Gerais' }, + pa: { name: 'Pará' }, + pb: { name: 'Paraíba' }, + pr: { name: 'Paraná' }, + pe: { name: 'Pernambuco' }, + pi: { name: 'Piauí' }, + rj: { name: 'Rio de Janeiro' }, + rn: { name: 'Rio Grande do Norte' }, + rs: { name: 'Rio Grande do Sul' }, + ro: { name: 'Rondônia' }, + rr: { name: 'Roraima' }, + sc: { name: 'Santa Catarina' }, + sp: { name: 'São Paulo' }, + se: { name: 'Sergipe' }, + to: { name: 'Tocantins' } + } + }, ca: { name: 'Canada' }, de: { name: 'Deutschland' }, es: { name: 'España' }, @@ -21,7 +65,7 @@ var countries = { } // translations of messages -var msg = { +const msg = { en: { langName: 'English', defCountry: 'us', @@ -480,51 +524,151 @@ var msg = { loadingTip: 'Se demorar muito, experimente carregar outra imagem.', preview: 'Pré-visualização:', fold: 'dobre nas linhas tracejadas', + }, + + ru: { + langName: 'Русский', + defCountry: 'us', + monthNames: [ 'Месяц', 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' ], + weekDays: [ 'Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб' ], + weekStart: 'Неделя начинается с', + sunday: 'Воскресенья', + monday: 'Понедельника', + year: 'Год', + month: 'Месяц', + day: 'День', + credits: 'Создано с desktopCal.js', + creditTitle:'Уведомление', + creditDescr:'Вы можете изменить эту строку, чтобы включить в нее информацию, например, об авторе фотографии.', + front: 'Сторона 1', + back: 'Сторона 2', + design: 'Выберите макет календаря', + edit: 'Выберите изображение, месяц и год', + language: 'Язык', + layout: 'Макет', + desktopCal: 'Настольный календарь', + wallSingle: 'Настенный календарь', + digitalBg: 'Цифровые обои', + screenConf: 'Конфигурация экрана', + screenRes: 'Разрешение экрана', + chgOrient: 'Изменить ориентацию', + calStyle: 'Стиль календаря', + small: 'Маленький блок', + medium: 'Средний блок', + large: 'Большой блок', + column: 'Вертикальная полоса', + row: 'Горизонтальная полоса', + calSettings: 'Настройки календаря', + modern: 'Современный', + classic: 'Классический', + showHolidays: 'Показывать описание праздников', + yes: 'Да', + no: 'Нет', + horAlign: 'Выравнивание по горизонтали', + verAlign: 'Выравнивание по вертикали', + left: 'Влево', + horCenter: 'По центру', + right: 'Вправо', + top: 'По верхнему краю', + verCenter: 'По центру', + bottom: 'По нижнему краю', + width: 'Ширина', + height: 'Высота', + pixels: 'пикселей', + rotateR: 'Повернуть по часовой стрелке', + rotateL: 'Повернуть против часовой стрелки', + flipH: 'Отразить по горизонтали', + flipV: 'Отразить по вертикали', + reset: 'Сбросить', + colors: 'Цвета', + colorPresets: 'Цветовые схемы', + saveColors: 'Сохранить как новую цветовую схему', + deletePreset: 'Вы действительно хотите удалить эту цветовую схему?\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ!', + bgColor: 'Цвет фона', + bgOpacity: 'Прозрачность фона', + textColor: 'Цвет текста', + holidayColor:'Цвет праздников', + loadImage: 'Загрузить изображение', + holidays: 'Праздники', + countryHolidays: 'Национальные праздники', + customHolidays: 'Свои праздники', + description: 'Описание', + none: 'Нет', + add: 'Добавить', + delete: 'Удалить', + load: 'Загрузить', + imgNotice: 'Изображения НЕ покидают ваш компьютер. Вся обработка происходит в вашем браузере.', + printIt: 'Напечатать!', + paperSize: 'Формат бумаги / соотношение сторон', + paperIso: 'A3 или A4', + paperLegal: 'Legal', + paperLetter:'Letter', + paperTabloid:'Tabloid', + print: 'Сгенерировать и напечатать', + tipBgImg: 'Включите печать фоновых изображений в настройках принтера;', + tipPortrait:'Установите портретный режим печати;', + tipMargins: 'Задайте минимальный возможный размер полей;', + tipHeaders: 'Отключите верхний и нижний колонтитулы.', + downloadIt: 'Скачать ваши обои', + download: 'Скачать', + fileFormat: 'Формат файла', + loading: 'Загрузка, пожалуйста, подождите...', + loadingTip: 'Если это занимает слишком много времени, попробуйте загрузить другое изображение.', + preview: 'Предпросмотр:', + fold: 'сложите по пунктирным линиям', } } - function langOptions() { + const keys = Object.keys( msg ); - var html = '', - keys = Object.keys( msg ); + let html = ''; - for ( var i = 0; i < keys.length; i++ ) + for ( let i = 0; i < keys.length; i++ ) html += `
  • `; return html; } function monthOptions() { + let html = ''; - var html = ''; - - for ( var i = 0; i < 13; i++ ) + for ( let i = 0; i < 13; i++ ) html += ``; return html; } function countryOptions() { + let html = ``; - var html = ``, - keys = Object.keys( countries ); - - for ( var i = 0; i < keys.length; i++ ) - html += ``; + for ( const [ key, { name, regions } ] of Object.entries( countries ) ) { + if ( regions ) { + html += ``; + } + html += ``; + if ( regions ) { + for ( const [ regionKey, { name: regionName } ] of Object.entries( regions )) { + html += ``; + } + html += ``; + } + } return html; } -function changeCountry( newCountry ) { - - country = newCountry; +function changeCountry( newValue ) { + [ country, region ] = newValue.split('-'); localStorage.setItem( 'country', country ); + if ( region ) + localStorage.setItem( 'region', region ); + else + localStorage.removeItem( 'region' ); updatePreview(); } function changeLang( newLang ) { - if ( ! Object.keys( msg ).includes( newLang ) ) // invalid language? return false; @@ -536,8 +680,7 @@ function changeLang( newLang ) { } function translatePage() { - - var values = []; + let values = []; // save values from select elements document.querySelectorAll('select').forEach( ( el, i ) => values[ i ] = el.selectedIndex ); @@ -564,5 +707,4 @@ function translatePage() { if ( values[ i ] >= 0 ) el.selectedIndex = values[ i ] }); - } diff --git a/styles.css b/styles.css index ce84630..7f90bb3 100644 --- a/styles.css +++ b/styles.css @@ -66,6 +66,10 @@ button, .button { min-width: 100px; } +optgroup option { + font-size: 90%; +} + .action-button::before, .icon-button::before { background-size: 24px;